 c9760e8d64
			
		
	
	c9760e8d64
	
	
	
		
			
			Summary: Ref T13242. See PHI1039. Maniphest subtypes generally seem to be working well. I designed them as a general capability that might be extended to other `EditEngine` objects later, and PHI1039 describes a situation where extending subtypes to projects would give us some reasonable tools. (Some installs also already use icons/colors as a sort of lightweight version of subtypes, so I believe this is generally useful capability.) Some of this is a little bit copy-pasted and could probably be shared, but I'd like to wait a bit longer before merging it. For example, both configs have exactly the same structure right now, but Projects should possibly have some different flags (for example: to disable creating subprojects / milestones). This implementation is pretty basic for now: notably, subprojects/milestones don't get the nice "choose from among subtype forms" treatment that tasks do. If this ends up being part of a solution to PHI1039, I'd plan to fill that in later on. Test Plan: Defined multiple subtypes, created subtype forms, created projects with appropriate subtypes. Filtered them by subtype. Saw subtype information on list/detail views. Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13242 Differential Revision: https://secure.phabricator.com/D20040
		
			
				
	
	
		
			879 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			879 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| 
 | |
| final class PhabricatorProjectQuery
 | |
|   extends PhabricatorCursorPagedPolicyAwareQuery {
 | |
| 
 | |
|   private $ids;
 | |
|   private $phids;
 | |
|   private $memberPHIDs;
 | |
|   private $watcherPHIDs;
 | |
|   private $slugs;
 | |
|   private $slugNormals;
 | |
|   private $slugMap;
 | |
|   private $allSlugs;
 | |
|   private $names;
 | |
|   private $namePrefixes;
 | |
|   private $nameTokens;
 | |
|   private $icons;
 | |
|   private $colors;
 | |
|   private $ancestorPHIDs;
 | |
|   private $parentPHIDs;
 | |
|   private $isMilestone;
 | |
|   private $hasSubprojects;
 | |
|   private $minDepth;
 | |
|   private $maxDepth;
 | |
|   private $minMilestoneNumber;
 | |
|   private $maxMilestoneNumber;
 | |
|   private $subtypes;
 | |
| 
 | |
|   private $status       = 'status-any';
 | |
|   const STATUS_ANY      = 'status-any';
 | |
|   const STATUS_OPEN     = 'status-open';
 | |
|   const STATUS_CLOSED   = 'status-closed';
 | |
|   const STATUS_ACTIVE   = 'status-active';
 | |
|   const STATUS_ARCHIVED = 'status-archived';
 | |
|   private $statuses;
 | |
| 
 | |
|   private $needSlugs;
 | |
|   private $needMembers;
 | |
|   private $needAncestorMembers;
 | |
|   private $needWatchers;
 | |
|   private $needImages;
 | |
| 
 | |
|   public function withIDs(array $ids) {
 | |
|     $this->ids = $ids;
 | |
|     return $this;
 | |
|   }
 | |
| 
 | |
|   public function withPHIDs(array $phids) {
 | |
|     $this->phids = $phids;
 | |
|     return $this;
 | |
|   }
 | |
| 
 | |
|   public function withStatus($status) {
 | |
|     $this->status = $status;
 | |
|     return $this;
 | |
|   }
 | |
| 
 | |
|   public function withStatuses(array $statuses) {
 | |
|     $this->statuses = $statuses;
 | |
|     return $this;
 | |
|   }
 | |
| 
 | |
|   public function withMemberPHIDs(array $member_phids) {
 | |
|     $this->memberPHIDs = $member_phids;
 | |
|     return $this;
 | |
|   }
 | |
| 
 | |
|   public function withWatcherPHIDs(array $watcher_phids) {
 | |
|     $this->watcherPHIDs = $watcher_phids;
 | |
|     return $this;
 | |
|   }
 | |
| 
 | |
|   public function withSlugs(array $slugs) {
 | |
|     $this->slugs = $slugs;
 | |
|     return $this;
 | |
|   }
 | |
| 
 | |
|   public function withNames(array $names) {
 | |
|     $this->names = $names;
 | |
|     return $this;
 | |
|   }
 | |
| 
 | |
|   public function withNamePrefixes(array $prefixes) {
 | |
|     $this->namePrefixes = $prefixes;
 | |
|     return $this;
 | |
|   }
 | |
| 
 | |
|   public function withNameTokens(array $tokens) {
 | |
|     $this->nameTokens = array_values($tokens);
 | |
|     return $this;
 | |
|   }
 | |
| 
 | |
|   public function withIcons(array $icons) {
 | |
|     $this->icons = $icons;
 | |
|     return $this;
 | |
|   }
 | |
| 
 | |
|   public function withColors(array $colors) {
 | |
|     $this->colors = $colors;
 | |
|     return $this;
 | |
|   }
 | |
| 
 | |
|   public function withParentProjectPHIDs($parent_phids) {
 | |
|     $this->parentPHIDs = $parent_phids;
 | |
|     return $this;
 | |
|   }
 | |
| 
 | |
|   public function withAncestorProjectPHIDs($ancestor_phids) {
 | |
|     $this->ancestorPHIDs = $ancestor_phids;
 | |
|     return $this;
 | |
|   }
 | |
| 
 | |
|   public function withIsMilestone($is_milestone) {
 | |
|     $this->isMilestone = $is_milestone;
 | |
|     return $this;
 | |
|   }
 | |
| 
 | |
|   public function withHasSubprojects($has_subprojects) {
 | |
|     $this->hasSubprojects = $has_subprojects;
 | |
|     return $this;
 | |
|   }
 | |
| 
 | |
|   public function withDepthBetween($min, $max) {
 | |
|     $this->minDepth = $min;
 | |
|     $this->maxDepth = $max;
 | |
|     return $this;
 | |
|   }
 | |
| 
 | |
|   public function withMilestoneNumberBetween($min, $max) {
 | |
|     $this->minMilestoneNumber = $min;
 | |
|     $this->maxMilestoneNumber = $max;
 | |
|     return $this;
 | |
|   }
 | |
| 
 | |
|   public function withSubtypes(array $subtypes) {
 | |
|     $this->subtypes = $subtypes;
 | |
|     return $this;
 | |
|   }
 | |
| 
 | |
|   public function needMembers($need_members) {
 | |
|     $this->needMembers = $need_members;
 | |
|     return $this;
 | |
|   }
 | |
| 
 | |
|   public function needAncestorMembers($need_ancestor_members) {
 | |
|     $this->needAncestorMembers = $need_ancestor_members;
 | |
|     return $this;
 | |
|   }
 | |
| 
 | |
|   public function needWatchers($need_watchers) {
 | |
|     $this->needWatchers = $need_watchers;
 | |
|     return $this;
 | |
|   }
 | |
| 
 | |
|   public function needImages($need_images) {
 | |
|     $this->needImages = $need_images;
 | |
|     return $this;
 | |
|   }
 | |
| 
 | |
|   public function needSlugs($need_slugs) {
 | |
|     $this->needSlugs = $need_slugs;
 | |
|     return $this;
 | |
|   }
 | |
| 
 | |
|   public function newResultObject() {
 | |
|     return new PhabricatorProject();
 | |
|   }
 | |
| 
 | |
|   protected function getDefaultOrderVector() {
 | |
|     return array('name');
 | |
|   }
 | |
| 
 | |
|   public function getBuiltinOrders() {
 | |
|     return array(
 | |
|       'name' => array(
 | |
|         'vector' => array('name'),
 | |
|         'name' => pht('Name'),
 | |
|       ),
 | |
|     ) + parent::getBuiltinOrders();
 | |
|   }
 | |
| 
 | |
|   public function getOrderableColumns() {
 | |
|     return parent::getOrderableColumns() + array(
 | |
|       'name' => array(
 | |
|         'table' => $this->getPrimaryTableAlias(),
 | |
|         'column' => 'name',
 | |
|         'reverse' => true,
 | |
|         'type' => 'string',
 | |
|         'unique' => true,
 | |
|       ),
 | |
|       'milestoneNumber' => array(
 | |
|         'table' => $this->getPrimaryTableAlias(),
 | |
|         'column' => 'milestoneNumber',
 | |
|         'type' => 'int',
 | |
|       ),
 | |
|       'status' => array(
 | |
|         'table' => $this->getPrimaryTableAlias(),
 | |
|         'column' => 'status',
 | |
|         'type' => 'int',
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   protected function getPagingValueMap($cursor, array $keys) {
 | |
|     $project = $this->loadCursorObject($cursor);
 | |
|     return array(
 | |
|       'id' => $project->getID(),
 | |
|       'name' => $project->getName(),
 | |
|       'status' => $project->getStatus(),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   public function getSlugMap() {
 | |
|     if ($this->slugMap === null) {
 | |
|       throw new PhutilInvalidStateException('execute');
 | |
|     }
 | |
|     return $this->slugMap;
 | |
|   }
 | |
| 
 | |
|   protected function willExecute() {
 | |
|     $this->slugMap = array();
 | |
|     $this->slugNormals = array();
 | |
|     $this->allSlugs = array();
 | |
|     if ($this->slugs) {
 | |
|       foreach ($this->slugs as $slug) {
 | |
|         if (PhabricatorSlug::isValidProjectSlug($slug)) {
 | |
|           $normal = PhabricatorSlug::normalizeProjectSlug($slug);
 | |
|           $this->slugNormals[$slug] = $normal;
 | |
|           $this->allSlugs[$normal] = $normal;
 | |
|         }
 | |
| 
 | |
|         // NOTE: At least for now, we query for the normalized slugs but also
 | |
|         // for the slugs exactly as entered. This allows older projects with
 | |
|         // slugs that are no longer valid to continue to work.
 | |
|         $this->allSlugs[$slug] = $slug;
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   protected function loadPage() {
 | |
|     return $this->loadStandardPage($this->newResultObject());
 | |
|   }
 | |
| 
 | |
|   protected function willFilterPage(array $projects) {
 | |
|     $ancestor_paths = array();
 | |
|     foreach ($projects as $project) {
 | |
|       foreach ($project->getAncestorProjectPaths() as $path) {
 | |
|         $ancestor_paths[$path] = $path;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if ($ancestor_paths) {
 | |
|       $ancestors = id(new PhabricatorProject())->loadAllWhere(
 | |
|         'projectPath IN (%Ls)',
 | |
|         $ancestor_paths);
 | |
|     } else {
 | |
|       $ancestors = array();
 | |
|     }
 | |
| 
 | |
|     $projects = $this->linkProjectGraph($projects, $ancestors);
 | |
| 
 | |
|     $viewer_phid = $this->getViewer()->getPHID();
 | |
| 
 | |
|     $material_type = PhabricatorProjectMaterializedMemberEdgeType::EDGECONST;
 | |
|     $watcher_type = PhabricatorObjectHasWatcherEdgeType::EDGECONST;
 | |
| 
 | |
|     $types = array();
 | |
|     $types[] = $material_type;
 | |
|     if ($this->needWatchers) {
 | |
|       $types[] = $watcher_type;
 | |
|     }
 | |
| 
 | |
|     $all_graph = $this->getAllReachableAncestors($projects);
 | |
| 
 | |
|     // NOTE: Although we may not need much information about ancestors, we
 | |
|     // always need to test if the viewer is a member, because we will return
 | |
|     // ancestor projects to the policy filter via ExtendedPolicy calls. If
 | |
|     // we skip populating membership data on a parent, the policy framework
 | |
|     // will think the user is not a member of the parent project.
 | |
| 
 | |
|     $all_sources = array();
 | |
|     foreach ($all_graph as $project) {
 | |
|       // For milestones, we need parent members.
 | |
|       if ($project->isMilestone()) {
 | |
|         $parent_phid = $project->getParentProjectPHID();
 | |
|         $all_sources[$parent_phid] = $parent_phid;
 | |
|       }
 | |
| 
 | |
|       $phid = $project->getPHID();
 | |
|       $all_sources[$phid] = $phid;
 | |
|     }
 | |
| 
 | |
|     $edge_query = id(new PhabricatorEdgeQuery())
 | |
|       ->withSourcePHIDs($all_sources)
 | |
|       ->withEdgeTypes($types);
 | |
| 
 | |
|     $need_all_edges =
 | |
|       $this->needMembers ||
 | |
|       $this->needWatchers ||
 | |
|       $this->needAncestorMembers;
 | |
| 
 | |
|     // If we only need to know if the viewer is a member, we can restrict
 | |
|     // the query to just their PHID.
 | |
|     $any_edges = true;
 | |
|     if (!$need_all_edges) {
 | |
|       if ($viewer_phid) {
 | |
|         $edge_query->withDestinationPHIDs(array($viewer_phid));
 | |
|       } else {
 | |
|         // If we don't need members or watchers and don't have a viewer PHID
 | |
|         // (viewer is logged-out or omnipotent), they'll never be a member
 | |
|         // so we don't need to issue this query at all.
 | |
|         $any_edges = false;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if ($any_edges) {
 | |
|       $edge_query->execute();
 | |
|     }
 | |
| 
 | |
|     $membership_projects = array();
 | |
|     foreach ($all_graph as $project) {
 | |
|       $project_phid = $project->getPHID();
 | |
| 
 | |
|       if ($project->isMilestone()) {
 | |
|         $source_phids = array($project->getParentProjectPHID());
 | |
|       } else {
 | |
|         $source_phids = array($project_phid);
 | |
|       }
 | |
| 
 | |
|       if ($any_edges) {
 | |
|         $member_phids = $edge_query->getDestinationPHIDs(
 | |
|           $source_phids,
 | |
|           array($material_type));
 | |
|       } else {
 | |
|         $member_phids = array();
 | |
|       }
 | |
| 
 | |
|       if (in_array($viewer_phid, $member_phids)) {
 | |
|         $membership_projects[$project_phid] = $project;
 | |
|       }
 | |
| 
 | |
|       if ($this->needMembers || $this->needAncestorMembers) {
 | |
|         $project->attachMemberPHIDs($member_phids);
 | |
|       }
 | |
| 
 | |
|       if ($this->needWatchers) {
 | |
|         $watcher_phids = $edge_query->getDestinationPHIDs(
 | |
|           array($project_phid),
 | |
|           array($watcher_type));
 | |
|         $project->attachWatcherPHIDs($watcher_phids);
 | |
|         $project->setIsUserWatcher(
 | |
|           $viewer_phid,
 | |
|           in_array($viewer_phid, $watcher_phids));
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // If we loaded ancestor members, we've already populated membership
 | |
|     // lists above, so we can skip this step.
 | |
|     if (!$this->needAncestorMembers) {
 | |
|       $member_graph = $this->getAllReachableAncestors($membership_projects);
 | |
| 
 | |
|       foreach ($all_graph as $phid => $project) {
 | |
|         $is_member = isset($member_graph[$phid]);
 | |
|         $project->setIsUserMember($viewer_phid, $is_member);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     return $projects;
 | |
|   }
 | |
| 
 | |
|   protected function didFilterPage(array $projects) {
 | |
|     $viewer = $this->getViewer();
 | |
| 
 | |
|     if ($this->needImages) {
 | |
|       $need_images = $projects;
 | |
| 
 | |
|       // First, try to load custom profile images for any projects with custom
 | |
|       // images.
 | |
|       $file_phids = array();
 | |
|       foreach ($need_images as $key => $project) {
 | |
|         $image_phid = $project->getProfileImagePHID();
 | |
|         if ($image_phid) {
 | |
|           $file_phids[$key] = $image_phid;
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       if ($file_phids) {
 | |
|         $files = id(new PhabricatorFileQuery())
 | |
|           ->setParentQuery($this)
 | |
|           ->setViewer($viewer)
 | |
|           ->withPHIDs($file_phids)
 | |
|           ->execute();
 | |
|         $files = mpull($files, null, 'getPHID');
 | |
| 
 | |
|         foreach ($file_phids as $key => $image_phid) {
 | |
|           $file = idx($files, $image_phid);
 | |
|           if (!$file) {
 | |
|             continue;
 | |
|           }
 | |
| 
 | |
|           $need_images[$key]->attachProfileImageFile($file);
 | |
|           unset($need_images[$key]);
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       // For projects with default images, or projects where the custom image
 | |
|       // failed to load, load a builtin image.
 | |
|       if ($need_images) {
 | |
|         $builtin_map = array();
 | |
|         $builtins = array();
 | |
|         foreach ($need_images as $key => $project) {
 | |
|           $icon = $project->getIcon();
 | |
| 
 | |
|           $builtin_name = PhabricatorProjectIconSet::getIconImage($icon);
 | |
|           $builtin_name = 'projects/'.$builtin_name;
 | |
| 
 | |
|           $builtin = id(new PhabricatorFilesOnDiskBuiltinFile())
 | |
|             ->setName($builtin_name);
 | |
| 
 | |
|           $builtin_key = $builtin->getBuiltinFileKey();
 | |
| 
 | |
|           $builtins[] = $builtin;
 | |
|           $builtin_map[$key] = $builtin_key;
 | |
|         }
 | |
| 
 | |
|         $builtin_files = PhabricatorFile::loadBuiltins(
 | |
|           $viewer,
 | |
|           $builtins);
 | |
| 
 | |
|         foreach ($need_images as $key => $project) {
 | |
|           $builtin_key = $builtin_map[$key];
 | |
|           $builtin_file = $builtin_files[$builtin_key];
 | |
|           $project->attachProfileImageFile($builtin_file);
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     $this->loadSlugs($projects);
 | |
| 
 | |
|     return $projects;
 | |
|   }
 | |
| 
 | |
|   protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
 | |
|     $where = parent::buildWhereClauseParts($conn);
 | |
| 
 | |
|     if ($this->status != self::STATUS_ANY) {
 | |
|       switch ($this->status) {
 | |
|         case self::STATUS_OPEN:
 | |
|         case self::STATUS_ACTIVE:
 | |
|           $filter = array(
 | |
|             PhabricatorProjectStatus::STATUS_ACTIVE,
 | |
|           );
 | |
|           break;
 | |
|         case self::STATUS_CLOSED:
 | |
|         case self::STATUS_ARCHIVED:
 | |
|           $filter = array(
 | |
|             PhabricatorProjectStatus::STATUS_ARCHIVED,
 | |
|           );
 | |
|           break;
 | |
|         default:
 | |
|           throw new Exception(
 | |
|             pht(
 | |
|               "Unknown project status '%s'!",
 | |
|               $this->status));
 | |
|       }
 | |
|       $where[] = qsprintf(
 | |
|         $conn,
 | |
|         'status IN (%Ld)',
 | |
|         $filter);
 | |
|     }
 | |
| 
 | |
|     if ($this->statuses !== null) {
 | |
|       $where[] = qsprintf(
 | |
|         $conn,
 | |
|         'status IN (%Ls)',
 | |
|         $this->statuses);
 | |
|     }
 | |
| 
 | |
|     if ($this->ids !== null) {
 | |
|       $where[] = qsprintf(
 | |
|         $conn,
 | |
|         'id IN (%Ld)',
 | |
|         $this->ids);
 | |
|     }
 | |
| 
 | |
|     if ($this->phids !== null) {
 | |
|       $where[] = qsprintf(
 | |
|         $conn,
 | |
|         'phid IN (%Ls)',
 | |
|         $this->phids);
 | |
|     }
 | |
| 
 | |
|     if ($this->memberPHIDs !== null) {
 | |
|       $where[] = qsprintf(
 | |
|         $conn,
 | |
|         'e.dst IN (%Ls)',
 | |
|         $this->memberPHIDs);
 | |
|     }
 | |
| 
 | |
|     if ($this->watcherPHIDs !== null) {
 | |
|       $where[] = qsprintf(
 | |
|         $conn,
 | |
|         'w.dst IN (%Ls)',
 | |
|         $this->watcherPHIDs);
 | |
|     }
 | |
| 
 | |
|     if ($this->slugs !== null) {
 | |
|       $where[] = qsprintf(
 | |
|         $conn,
 | |
|         'slug.slug IN (%Ls)',
 | |
|         $this->allSlugs);
 | |
|     }
 | |
| 
 | |
|     if ($this->names !== null) {
 | |
|       $where[] = qsprintf(
 | |
|         $conn,
 | |
|         'name IN (%Ls)',
 | |
|         $this->names);
 | |
|     }
 | |
| 
 | |
|     if ($this->namePrefixes) {
 | |
|       $parts = array();
 | |
|       foreach ($this->namePrefixes as $name_prefix) {
 | |
|         $parts[] = qsprintf(
 | |
|           $conn,
 | |
|           'name LIKE %>',
 | |
|           $name_prefix);
 | |
|       }
 | |
|       $where[] = qsprintf($conn, '%LO', $parts);
 | |
|     }
 | |
| 
 | |
|     if ($this->icons !== null) {
 | |
|       $where[] = qsprintf(
 | |
|         $conn,
 | |
|         'icon IN (%Ls)',
 | |
|         $this->icons);
 | |
|     }
 | |
| 
 | |
|     if ($this->colors !== null) {
 | |
|       $where[] = qsprintf(
 | |
|         $conn,
 | |
|         'color IN (%Ls)',
 | |
|         $this->colors);
 | |
|     }
 | |
| 
 | |
|     if ($this->parentPHIDs !== null) {
 | |
|       $where[] = qsprintf(
 | |
|         $conn,
 | |
|         'parentProjectPHID IN (%Ls)',
 | |
|         $this->parentPHIDs);
 | |
|     }
 | |
| 
 | |
|     if ($this->ancestorPHIDs !== null) {
 | |
|       $ancestor_paths = queryfx_all(
 | |
|         $conn,
 | |
|         'SELECT projectPath, projectDepth FROM %T WHERE phid IN (%Ls)',
 | |
|         id(new PhabricatorProject())->getTableName(),
 | |
|         $this->ancestorPHIDs);
 | |
|       if (!$ancestor_paths) {
 | |
|         throw new PhabricatorEmptyQueryException();
 | |
|       }
 | |
| 
 | |
|       $sql = array();
 | |
|       foreach ($ancestor_paths as $ancestor_path) {
 | |
|         $sql[] = qsprintf(
 | |
|           $conn,
 | |
|           '(projectPath LIKE %> AND projectDepth > %d)',
 | |
|           $ancestor_path['projectPath'],
 | |
|           $ancestor_path['projectDepth']);
 | |
|       }
 | |
| 
 | |
|       $where[] = qsprintf($conn, '%LO', $sql);
 | |
| 
 | |
|       $where[] = qsprintf(
 | |
|         $conn,
 | |
|         'parentProjectPHID IS NOT NULL');
 | |
|     }
 | |
| 
 | |
|     if ($this->isMilestone !== null) {
 | |
|       if ($this->isMilestone) {
 | |
|         $where[] = qsprintf(
 | |
|           $conn,
 | |
|           'milestoneNumber IS NOT NULL');
 | |
|       } else {
 | |
|         $where[] = qsprintf(
 | |
|           $conn,
 | |
|           'milestoneNumber IS NULL');
 | |
|       }
 | |
|     }
 | |
| 
 | |
| 
 | |
|     if ($this->hasSubprojects !== null) {
 | |
|       $where[] = qsprintf(
 | |
|         $conn,
 | |
|         'hasSubprojects = %d',
 | |
|         (int)$this->hasSubprojects);
 | |
|     }
 | |
| 
 | |
|     if ($this->minDepth !== null) {
 | |
|       $where[] = qsprintf(
 | |
|         $conn,
 | |
|         'projectDepth >= %d',
 | |
|         $this->minDepth);
 | |
|     }
 | |
| 
 | |
|     if ($this->maxDepth !== null) {
 | |
|       $where[] = qsprintf(
 | |
|         $conn,
 | |
|         'projectDepth <= %d',
 | |
|         $this->maxDepth);
 | |
|     }
 | |
| 
 | |
|     if ($this->minMilestoneNumber !== null) {
 | |
|       $where[] = qsprintf(
 | |
|         $conn,
 | |
|         'milestoneNumber >= %d',
 | |
|         $this->minMilestoneNumber);
 | |
|     }
 | |
| 
 | |
|     if ($this->maxMilestoneNumber !== null) {
 | |
|       $where[] = qsprintf(
 | |
|         $conn,
 | |
|         'milestoneNumber <= %d',
 | |
|         $this->maxMilestoneNumber);
 | |
|     }
 | |
| 
 | |
|     if ($this->subtypes !== null) {
 | |
|       $where[] = qsprintf(
 | |
|         $conn,
 | |
|         'subtype IN (%Ls)',
 | |
|         $this->subtypes);
 | |
|     }
 | |
| 
 | |
|     return $where;
 | |
|   }
 | |
| 
 | |
|   protected function shouldGroupQueryResultRows() {
 | |
|     if ($this->memberPHIDs || $this->watcherPHIDs || $this->nameTokens) {
 | |
|       return true;
 | |
|     }
 | |
|     return parent::shouldGroupQueryResultRows();
 | |
|   }
 | |
| 
 | |
|   protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
 | |
|     $joins = parent::buildJoinClauseParts($conn);
 | |
| 
 | |
|     if ($this->memberPHIDs !== null) {
 | |
|       $joins[] = qsprintf(
 | |
|         $conn,
 | |
|         'JOIN %T e ON e.src = p.phid AND e.type = %d',
 | |
|         PhabricatorEdgeConfig::TABLE_NAME_EDGE,
 | |
|         PhabricatorProjectMaterializedMemberEdgeType::EDGECONST);
 | |
|     }
 | |
| 
 | |
|     if ($this->watcherPHIDs !== null) {
 | |
|       $joins[] = qsprintf(
 | |
|         $conn,
 | |
|         'JOIN %T w ON w.src = p.phid AND w.type = %d',
 | |
|         PhabricatorEdgeConfig::TABLE_NAME_EDGE,
 | |
|         PhabricatorObjectHasWatcherEdgeType::EDGECONST);
 | |
|     }
 | |
| 
 | |
|     if ($this->slugs !== null) {
 | |
|       $joins[] = qsprintf(
 | |
|         $conn,
 | |
|         'JOIN %T slug on slug.projectPHID = p.phid',
 | |
|         id(new PhabricatorProjectSlug())->getTableName());
 | |
|     }
 | |
| 
 | |
|     if ($this->nameTokens !== null) {
 | |
|       $name_tokens = $this->getNameTokensForQuery($this->nameTokens);
 | |
|       foreach ($name_tokens as $key => $token) {
 | |
|         $token_table = 'token_'.$key;
 | |
|         $joins[] = qsprintf(
 | |
|           $conn,
 | |
|           'JOIN %T %T ON %T.projectID = p.id AND %T.token LIKE %>',
 | |
|           PhabricatorProject::TABLE_DATASOURCE_TOKEN,
 | |
|           $token_table,
 | |
|           $token_table,
 | |
|           $token_table,
 | |
|           $token);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     return $joins;
 | |
|   }
 | |
| 
 | |
|   public function getQueryApplicationClass() {
 | |
|     return 'PhabricatorProjectApplication';
 | |
|   }
 | |
| 
 | |
|   protected function getPrimaryTableAlias() {
 | |
|     return 'p';
 | |
|   }
 | |
| 
 | |
|   private function linkProjectGraph(array $projects, array $ancestors) {
 | |
|     $ancestor_map = mpull($ancestors, null, 'getPHID');
 | |
|     $projects_map = mpull($projects, null, 'getPHID');
 | |
| 
 | |
|     $all_map = $projects_map + $ancestor_map;
 | |
| 
 | |
|     $done = array();
 | |
|     foreach ($projects as $key => $project) {
 | |
|       $seen = array($project->getPHID() => true);
 | |
| 
 | |
|       if (!$this->linkProject($project, $all_map, $done, $seen)) {
 | |
|         $this->didRejectResult($project);
 | |
|         unset($projects[$key]);
 | |
|         continue;
 | |
|       }
 | |
| 
 | |
|       foreach ($project->getAncestorProjects() as $ancestor) {
 | |
|         $seen[$ancestor->getPHID()] = true;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     return $projects;
 | |
|   }
 | |
| 
 | |
|   private function linkProject($project, array $all, array $done, array $seen) {
 | |
|     $parent_phid = $project->getParentProjectPHID();
 | |
| 
 | |
|     // This project has no parent, so just attach `null` and return.
 | |
|     if (!$parent_phid) {
 | |
|       $project->attachParentProject(null);
 | |
|       return true;
 | |
|     }
 | |
| 
 | |
|     // This project has a parent, but it failed to load.
 | |
|     if (empty($all[$parent_phid])) {
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     // Test for graph cycles. If we encounter one, we're going to hide the
 | |
|     // entire cycle since we can't meaningfully resolve it.
 | |
|     if (isset($seen[$parent_phid])) {
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     $seen[$parent_phid] = true;
 | |
| 
 | |
|     $parent = $all[$parent_phid];
 | |
|     $project->attachParentProject($parent);
 | |
| 
 | |
|     if (!empty($done[$parent_phid])) {
 | |
|       return true;
 | |
|     }
 | |
| 
 | |
|     return $this->linkProject($parent, $all, $done, $seen);
 | |
|   }
 | |
| 
 | |
|   private function getAllReachableAncestors(array $projects) {
 | |
|     $ancestors = array();
 | |
| 
 | |
|     $seen = mpull($projects, null, 'getPHID');
 | |
| 
 | |
|     $stack = $projects;
 | |
|     while ($stack) {
 | |
|       $project = array_pop($stack);
 | |
| 
 | |
|       $phid = $project->getPHID();
 | |
|       $ancestors[$phid] = $project;
 | |
| 
 | |
|       $parent_phid = $project->getParentProjectPHID();
 | |
|       if (!$parent_phid) {
 | |
|         continue;
 | |
|       }
 | |
| 
 | |
|       if (isset($seen[$parent_phid])) {
 | |
|         continue;
 | |
|       }
 | |
| 
 | |
|       $seen[$parent_phid] = true;
 | |
|       $stack[] = $project->getParentProject();
 | |
|     }
 | |
| 
 | |
|     return $ancestors;
 | |
|   }
 | |
| 
 | |
|   private function loadSlugs(array $projects) {
 | |
|     // Build a map from primary slugs to projects.
 | |
|     $primary_map = array();
 | |
|     foreach ($projects as $project) {
 | |
|       $primary_slug = $project->getPrimarySlug();
 | |
|       if ($primary_slug === null) {
 | |
|         continue;
 | |
|       }
 | |
| 
 | |
|       $primary_map[$primary_slug] = $project;
 | |
|     }
 | |
| 
 | |
|     // Link up all of the queried slugs which correspond to primary
 | |
|     // slugs. If we can link up everything from this (no slugs were queried,
 | |
|     // or only primary slugs were queried) we don't need to load anything
 | |
|     // else.
 | |
|     $unknown = $this->slugNormals;
 | |
|     foreach ($unknown as $input => $normal) {
 | |
|       if (isset($primary_map[$input])) {
 | |
|         $match = $input;
 | |
|       } else if (isset($primary_map[$normal])) {
 | |
|         $match = $normal;
 | |
|       } else {
 | |
|         continue;
 | |
|       }
 | |
| 
 | |
|       $this->slugMap[$input] = array(
 | |
|         'slug' => $match,
 | |
|         'projectPHID' => $primary_map[$match]->getPHID(),
 | |
|       );
 | |
| 
 | |
|       unset($unknown[$input]);
 | |
|     }
 | |
| 
 | |
|     // If we need slugs, we have to load everything.
 | |
|     // If we still have some queried slugs which we haven't mapped, we only
 | |
|     // need to look for them.
 | |
|     // If we've mapped everything, we don't have to do any work.
 | |
|     $project_phids = mpull($projects, 'getPHID');
 | |
|     if ($this->needSlugs) {
 | |
|       $slugs = id(new PhabricatorProjectSlug())->loadAllWhere(
 | |
|         'projectPHID IN (%Ls)',
 | |
|         $project_phids);
 | |
|     } else if ($unknown) {
 | |
|       $slugs = id(new PhabricatorProjectSlug())->loadAllWhere(
 | |
|         'projectPHID IN (%Ls) AND slug IN (%Ls)',
 | |
|         $project_phids,
 | |
|         $unknown);
 | |
|     } else {
 | |
|       $slugs = array();
 | |
|     }
 | |
| 
 | |
|     // Link up any slugs we were not able to link up earlier.
 | |
|     $extra_map = mpull($slugs, 'getProjectPHID', 'getSlug');
 | |
|     foreach ($unknown as $input => $normal) {
 | |
|       if (isset($extra_map[$input])) {
 | |
|         $match = $input;
 | |
|       } else if (isset($extra_map[$normal])) {
 | |
|         $match = $normal;
 | |
|       } else {
 | |
|         continue;
 | |
|       }
 | |
| 
 | |
|       $this->slugMap[$input] = array(
 | |
|         'slug' => $match,
 | |
|         'projectPHID' => $extra_map[$match],
 | |
|       );
 | |
| 
 | |
|       unset($unknown[$input]);
 | |
|     }
 | |
| 
 | |
|     if ($this->needSlugs) {
 | |
|       $slug_groups = mgroup($slugs, 'getProjectPHID');
 | |
|       foreach ($projects as $project) {
 | |
|         $project_slugs = idx($slug_groups, $project->getPHID(), array());
 | |
|         $project->attachSlugs($project_slugs);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   private function getNameTokensForQuery(array $tokens) {
 | |
|     // When querying for projects by name, only actually search for the five
 | |
|     // longest tokens. MySQL can get grumpy with a large number of JOINs
 | |
|     // with LIKEs and queries for more than 5 tokens are essentially never
 | |
|     // legitimate searches for projects, but users copy/pasting nonsense.
 | |
|     // See also PHI47.
 | |
| 
 | |
|     $length_map = array();
 | |
|     foreach ($tokens as $token) {
 | |
|       $length_map[$token] = strlen($token);
 | |
|     }
 | |
|     arsort($length_map);
 | |
| 
 | |
|     $length_map = array_slice($length_map, 0, 5, true);
 | |
| 
 | |
|     return array_keys($length_map);
 | |
|   }
 | |
| 
 | |
| }
 |