@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3/**
4 * @extends PhabricatorCursorPagedPolicyAwareQuery<PhabricatorProject>
5 */
6final class PhabricatorProjectQuery
7 extends PhabricatorCursorPagedPolicyAwareQuery {
8
9 private $ids;
10 private $phids;
11 private $memberPHIDs;
12 private $watcherPHIDs;
13 private $slugs;
14 private $slugNormals;
15 private $slugMap;
16 private $allSlugs;
17 private $names;
18 private $namePrefixes;
19 private $nameTokens;
20 private $icons;
21 private $colors;
22 private $ancestorPHIDs;
23 private $parentPHIDs;
24 private $isMilestone;
25 private $hasSubprojects;
26 private $minDepth;
27 private $maxDepth;
28 private $minMilestoneNumber;
29 private $maxMilestoneNumber;
30 private $subtypes;
31
32 private $status = 'status-any';
33 const STATUS_ANY = 'status-any';
34 const STATUS_OPEN = 'status-open';
35 const STATUS_CLOSED = 'status-closed';
36 const STATUS_ACTIVE = 'status-active';
37 const STATUS_ARCHIVED = 'status-archived';
38 private $statuses;
39
40 private $needSlugs;
41 private $needMembers;
42 private $needAncestorMembers;
43 private $needWatchers;
44 private $needImages;
45
46 public function withIDs(array $ids) {
47 $this->ids = $ids;
48 return $this;
49 }
50
51 public function withPHIDs(array $phids) {
52 $this->phids = $phids;
53 return $this;
54 }
55
56 public function withStatus($status) {
57 $this->status = $status;
58 return $this;
59 }
60
61 public function withStatuses(array $statuses) {
62 $this->statuses = $statuses;
63 return $this;
64 }
65
66 public function withMemberPHIDs(array $member_phids) {
67 $this->memberPHIDs = $member_phids;
68 return $this;
69 }
70
71 public function withWatcherPHIDs(array $watcher_phids) {
72 $this->watcherPHIDs = $watcher_phids;
73 return $this;
74 }
75
76 public function withSlugs(array $slugs) {
77 $this->slugs = $slugs;
78 return $this;
79 }
80
81 public function withNames(array $names) {
82 $this->names = $names;
83 return $this;
84 }
85
86 /**
87 * Set a prefix to query in a LIKE clause of the query
88 * @param array<string> $prefixes String prefixes to search for
89 */
90 public function withNamePrefixes(array $prefixes) {
91 $this->namePrefixes = $prefixes;
92 return $this;
93 }
94
95 public function withNameTokens(array $tokens) {
96 $this->nameTokens = array_values($tokens);
97 return $this;
98 }
99
100 public function withIcons(array $icons) {
101 $this->icons = $icons;
102 return $this;
103 }
104
105 public function withColors(array $colors) {
106 $this->colors = $colors;
107 return $this;
108 }
109
110 public function withParentProjectPHIDs($parent_phids) {
111 $this->parentPHIDs = $parent_phids;
112 return $this;
113 }
114
115 public function withAncestorProjectPHIDs($ancestor_phids) {
116 $this->ancestorPHIDs = $ancestor_phids;
117 return $this;
118 }
119
120 public function withIsMilestone($is_milestone) {
121 $this->isMilestone = $is_milestone;
122 return $this;
123 }
124
125 public function withHasSubprojects($has_subprojects) {
126 $this->hasSubprojects = $has_subprojects;
127 return $this;
128 }
129
130 public function withDepthBetween($min, $max) {
131 $this->minDepth = $min;
132 $this->maxDepth = $max;
133 return $this;
134 }
135
136 public function withMilestoneNumberBetween($min, $max) {
137 $this->minMilestoneNumber = $min;
138 $this->maxMilestoneNumber = $max;
139 return $this;
140 }
141
142 public function withSubtypes(array $subtypes) {
143 $this->subtypes = $subtypes;
144 return $this;
145 }
146
147 public function needMembers($need_members) {
148 $this->needMembers = $need_members;
149 return $this;
150 }
151
152 public function needAncestorMembers($need_ancestor_members) {
153 $this->needAncestorMembers = $need_ancestor_members;
154 return $this;
155 }
156
157 public function needWatchers($need_watchers) {
158 $this->needWatchers = $need_watchers;
159 return $this;
160 }
161
162 public function needImages($need_images) {
163 $this->needImages = $need_images;
164 return $this;
165 }
166
167 public function needSlugs($need_slugs) {
168 $this->needSlugs = $need_slugs;
169 return $this;
170 }
171
172 public function newResultObject() {
173 return new PhabricatorProject();
174 }
175
176 protected function getDefaultOrderVector() {
177 return array('name');
178 }
179
180 public function getBuiltinOrders() {
181 return array(
182 'name' => array(
183 'vector' => array('name'),
184 'name' => pht('Name'),
185 ),
186 ) + parent::getBuiltinOrders();
187 }
188
189 public function getOrderableColumns() {
190 return parent::getOrderableColumns() + array(
191 'name' => array(
192 'table' => $this->getPrimaryTableAlias(),
193 'column' => 'name',
194 'reverse' => true,
195 'type' => 'string',
196 'unique' => true,
197 ),
198 'milestoneNumber' => array(
199 'table' => $this->getPrimaryTableAlias(),
200 'column' => 'milestoneNumber',
201 'type' => 'int',
202 ),
203 'status' => array(
204 'table' => $this->getPrimaryTableAlias(),
205 'column' => 'status',
206 'type' => 'int',
207 ),
208 );
209 }
210
211 protected function newPagingMapFromPartialObject($object) {
212 return array(
213 'id' => (int)$object->getID(),
214 'name' => $object->getName(),
215 'status' => $object->getStatus(),
216 );
217 }
218
219 public function getSlugMap() {
220 if ($this->slugMap === null) {
221 throw new PhutilInvalidStateException('execute');
222 }
223 return $this->slugMap;
224 }
225
226 protected function willExecute() {
227 $this->slugMap = array();
228 $this->slugNormals = array();
229 $this->allSlugs = array();
230 if ($this->slugs) {
231 foreach ($this->slugs as $slug) {
232 if (PhabricatorSlug::isValidProjectSlug($slug)) {
233 $normal = PhabricatorSlug::normalizeProjectSlug($slug);
234 $this->slugNormals[$slug] = $normal;
235 $this->allSlugs[$normal] = $normal;
236 }
237
238 // NOTE: At least for now, we query for the normalized slugs but also
239 // for the slugs exactly as entered. This allows older projects with
240 // slugs that are no longer valid to continue to work.
241 $this->allSlugs[$slug] = $slug;
242 }
243 }
244 }
245
246 protected function willFilterPage(array $projects) {
247 $ancestor_paths = array();
248 foreach ($projects as $project) {
249 foreach ($project->getAncestorProjectPaths() as $path) {
250 $ancestor_paths[$path] = $path;
251 }
252 }
253
254 if ($ancestor_paths) {
255 $ancestors = id(new PhabricatorProject())->loadAllWhere(
256 'projectPath IN (%Ls)',
257 $ancestor_paths);
258 } else {
259 $ancestors = array();
260 }
261
262 $projects = $this->linkProjectGraph($projects, $ancestors);
263
264 $viewer_phid = $this->getViewer()->getPHID();
265
266 $material_type = PhabricatorProjectMaterializedMemberEdgeType::EDGECONST;
267 $watcher_type = PhabricatorObjectHasWatcherEdgeType::EDGECONST;
268
269 $types = array();
270 $types[] = $material_type;
271 if ($this->needWatchers) {
272 $types[] = $watcher_type;
273 }
274
275 $all_graph = $this->getAllReachableAncestors($projects);
276
277 // See T13484. If the graph is damaged (and contains a cycle or an edge
278 // pointing at a project which has been destroyed), some of the nodes we
279 // started with may be filtered out by reachability tests. If any of the
280 // projects we are linking up don't have available ancestors, filter them
281 // out.
282
283 foreach ($projects as $key => $project) {
284 $project_phid = $project->getPHID();
285 if (!isset($all_graph[$project_phid])) {
286 $this->didRejectResult($project);
287 unset($projects[$key]);
288 continue;
289 }
290 }
291
292 if (!$projects) {
293 return array();
294 }
295
296 // NOTE: Although we may not need much information about ancestors, we
297 // always need to test if the viewer is a member, because we will return
298 // ancestor projects to the policy filter via ExtendedPolicy calls. If
299 // we skip populating membership data on a parent, the policy framework
300 // will think the user is not a member of the parent project.
301
302 $all_sources = array();
303 foreach ($all_graph as $project) {
304 // For milestones, we need parent members.
305 if ($project->isMilestone()) {
306 $parent_phid = $project->getParentProjectPHID();
307 $all_sources[$parent_phid] = $parent_phid;
308 }
309
310 $phid = $project->getPHID();
311 $all_sources[$phid] = $phid;
312 }
313
314 $edge_query = id(new PhabricatorEdgeQuery())
315 ->withSourcePHIDs($all_sources)
316 ->withEdgeTypes($types);
317
318 $need_all_edges =
319 $this->needMembers ||
320 $this->needWatchers ||
321 $this->needAncestorMembers;
322
323 // If we only need to know if the viewer is a member, we can restrict
324 // the query to just their PHID.
325 $any_edges = true;
326 if (!$need_all_edges) {
327 if ($viewer_phid) {
328 $edge_query->withDestinationPHIDs(array($viewer_phid));
329 } else {
330 // If we don't need members or watchers and don't have a viewer PHID
331 // (viewer is logged-out or omnipotent), they'll never be a member
332 // so we don't need to issue this query at all.
333 $any_edges = false;
334 }
335 }
336
337 if ($any_edges) {
338 $edge_query->execute();
339 }
340
341 $membership_projects = array();
342 foreach ($all_graph as $project) {
343 $project_phid = $project->getPHID();
344
345 if ($project->isMilestone()) {
346 $source_phids = array($project->getParentProjectPHID());
347 } else {
348 $source_phids = array($project_phid);
349 }
350
351 if ($any_edges) {
352 $member_phids = $edge_query->getDestinationPHIDs(
353 $source_phids,
354 array($material_type));
355 } else {
356 $member_phids = array();
357 }
358
359 if (in_array($viewer_phid, $member_phids)) {
360 $membership_projects[$project_phid] = $project;
361 }
362
363 if ($this->needMembers || $this->needAncestorMembers) {
364 $project->attachMemberPHIDs($member_phids);
365 }
366
367 if ($this->needWatchers) {
368 $watcher_phids = $edge_query->getDestinationPHIDs(
369 array($project_phid),
370 array($watcher_type));
371 $project->attachWatcherPHIDs($watcher_phids);
372 $project->setIsUserWatcher(
373 $viewer_phid,
374 in_array($viewer_phid, $watcher_phids));
375 }
376 }
377
378 // If we loaded ancestor members, we've already populated membership
379 // lists above, so we can skip this step.
380 if (!$this->needAncestorMembers) {
381 $member_graph = $this->getAllReachableAncestors($membership_projects);
382
383 foreach ($all_graph as $phid => $project) {
384 $is_member = isset($member_graph[$phid]);
385 $project->setIsUserMember($viewer_phid, $is_member);
386 }
387 }
388
389 return $projects;
390 }
391
392 protected function didFilterPage(array $projects) {
393 $viewer = $this->getViewer();
394
395 if ($this->needImages) {
396 $need_images = $projects;
397
398 // First, try to load custom profile images for any projects with custom
399 // images.
400 $file_phids = array();
401 foreach ($need_images as $key => $project) {
402 $image_phid = $project->getProfileImagePHID();
403 if ($image_phid) {
404 $file_phids[$key] = $image_phid;
405 }
406 }
407
408 if ($file_phids) {
409 $files = id(new PhabricatorFileQuery())
410 ->setParentQuery($this)
411 ->setViewer($viewer)
412 ->withPHIDs($file_phids)
413 ->execute();
414 $files = mpull($files, null, 'getPHID');
415
416 foreach ($file_phids as $key => $image_phid) {
417 $file = idx($files, $image_phid);
418 if (!$file) {
419 continue;
420 }
421
422 $need_images[$key]->attachProfileImageFile($file);
423 unset($need_images[$key]);
424 }
425 }
426
427 // For projects with default images, or projects where the custom image
428 // failed to load, load a builtin image.
429 if ($need_images) {
430 $builtin_map = array();
431 $builtins = array();
432 foreach ($need_images as $key => $project) {
433 $icon = $project->getIcon();
434
435 $builtin_name = PhabricatorProjectIconSet::getIconImage($icon);
436 $builtin_name = 'projects/'.$builtin_name;
437
438 $builtin = id(new PhabricatorFilesOnDiskBuiltinFile())
439 ->setName($builtin_name);
440
441 $builtin_key = $builtin->getBuiltinFileKey();
442
443 $builtins[] = $builtin;
444 $builtin_map[$key] = $builtin_key;
445 }
446
447 $builtin_files = PhabricatorFile::loadBuiltins(
448 $viewer,
449 $builtins);
450
451 foreach ($need_images as $key => $project) {
452 $builtin_key = $builtin_map[$key];
453 $builtin_file = $builtin_files[$builtin_key];
454 $project->attachProfileImageFile($builtin_file);
455 }
456 }
457 }
458
459 $this->loadSlugs($projects);
460
461 return $projects;
462 }
463
464 protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
465 $where = parent::buildWhereClauseParts($conn);
466
467 if ($this->status != self::STATUS_ANY) {
468 switch ($this->status) {
469 case self::STATUS_OPEN:
470 case self::STATUS_ACTIVE:
471 $filter = array(
472 PhabricatorProjectStatus::STATUS_ACTIVE,
473 );
474 break;
475 case self::STATUS_CLOSED:
476 case self::STATUS_ARCHIVED:
477 $filter = array(
478 PhabricatorProjectStatus::STATUS_ARCHIVED,
479 );
480 break;
481 default:
482 throw new Exception(
483 pht(
484 "Unknown project status '%s'!",
485 $this->status));
486 }
487 $where[] = qsprintf(
488 $conn,
489 'project.status IN (%Ld)',
490 $filter);
491 }
492
493 if ($this->statuses !== null) {
494 $where[] = qsprintf(
495 $conn,
496 'project.status IN (%Ls)',
497 $this->statuses);
498 }
499
500 if ($this->ids !== null) {
501 $where[] = qsprintf(
502 $conn,
503 'project.id IN (%Ld)',
504 $this->ids);
505 }
506
507 if ($this->phids !== null) {
508 $where[] = qsprintf(
509 $conn,
510 'project.phid IN (%Ls)',
511 $this->phids);
512 }
513
514 if ($this->memberPHIDs !== null) {
515 $where[] = qsprintf(
516 $conn,
517 'e.dst IN (%Ls)',
518 $this->memberPHIDs);
519 }
520
521 if ($this->watcherPHIDs !== null) {
522 $where[] = qsprintf(
523 $conn,
524 'w.dst IN (%Ls)',
525 $this->watcherPHIDs);
526 }
527
528 if ($this->slugs !== null) {
529 $where[] = qsprintf(
530 $conn,
531 'slug.slug IN (%Ls)',
532 $this->allSlugs);
533 }
534
535 if ($this->names !== null) {
536 $where[] = qsprintf(
537 $conn,
538 'project.name IN (%Ls)',
539 $this->names);
540 }
541
542 if ($this->namePrefixes) {
543 $parts = array();
544 foreach ($this->namePrefixes as $name_prefix) {
545 $parts[] = qsprintf(
546 $conn,
547 'project.name LIKE %>',
548 $name_prefix);
549 }
550 $where[] = qsprintf($conn, '%LO', $parts);
551 }
552
553 if ($this->icons !== null) {
554 $where[] = qsprintf(
555 $conn,
556 'project.icon IN (%Ls)',
557 $this->icons);
558 }
559
560 if ($this->colors !== null) {
561 $where[] = qsprintf(
562 $conn,
563 'project.color IN (%Ls)',
564 $this->colors);
565 }
566
567 if ($this->parentPHIDs !== null) {
568 $where[] = qsprintf(
569 $conn,
570 'project.parentProjectPHID IN (%Ls)',
571 $this->parentPHIDs);
572 }
573
574 if ($this->ancestorPHIDs !== null) {
575 $ancestor_paths = queryfx_all(
576 $conn,
577 'SELECT projectPath, projectDepth FROM %T WHERE phid IN (%Ls)',
578 id(new PhabricatorProject())->getTableName(),
579 $this->ancestorPHIDs);
580 if (!$ancestor_paths) {
581 throw new PhabricatorEmptyQueryException();
582 }
583
584 $sql = array();
585 foreach ($ancestor_paths as $ancestor_path) {
586 $sql[] = qsprintf(
587 $conn,
588 '(project.projectPath LIKE %> AND project.projectDepth > %d)',
589 $ancestor_path['projectPath'],
590 $ancestor_path['projectDepth']);
591 }
592
593 $where[] = qsprintf($conn, '%LO', $sql);
594
595 $where[] = qsprintf(
596 $conn,
597 'project.parentProjectPHID IS NOT NULL');
598 }
599
600 if ($this->isMilestone !== null) {
601 if ($this->isMilestone) {
602 $where[] = qsprintf(
603 $conn,
604 'project.milestoneNumber IS NOT NULL');
605 } else {
606 $where[] = qsprintf(
607 $conn,
608 'project.milestoneNumber IS NULL');
609 }
610 }
611
612
613 if ($this->hasSubprojects !== null) {
614 $where[] = qsprintf(
615 $conn,
616 'project.hasSubprojects = %d',
617 (int)$this->hasSubprojects);
618 }
619
620 if ($this->minDepth !== null) {
621 $where[] = qsprintf(
622 $conn,
623 'project.projectDepth >= %d',
624 $this->minDepth);
625 }
626
627 if ($this->maxDepth !== null) {
628 $where[] = qsprintf(
629 $conn,
630 'project.projectDepth <= %d',
631 $this->maxDepth);
632 }
633
634 if ($this->minMilestoneNumber !== null) {
635 $where[] = qsprintf(
636 $conn,
637 'project.milestoneNumber >= %d',
638 $this->minMilestoneNumber);
639 }
640
641 if ($this->maxMilestoneNumber !== null) {
642 $where[] = qsprintf(
643 $conn,
644 'project.milestoneNumber <= %d',
645 $this->maxMilestoneNumber);
646 }
647
648 if ($this->subtypes !== null) {
649 $where[] = qsprintf(
650 $conn,
651 'project.subtype IN (%Ls)',
652 $this->subtypes);
653 }
654
655 return $where;
656 }
657
658 protected function shouldGroupQueryResultRows() {
659 if ($this->memberPHIDs || $this->watcherPHIDs || $this->nameTokens) {
660 return true;
661 }
662
663 if ($this->slugs) {
664 return true;
665 }
666
667 return parent::shouldGroupQueryResultRows();
668 }
669
670 protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
671 $joins = parent::buildJoinClauseParts($conn);
672
673 if ($this->memberPHIDs !== null) {
674 $joins[] = qsprintf(
675 $conn,
676 'JOIN %T e ON e.src = project.phid AND e.type = %d',
677 PhabricatorEdgeConfig::TABLE_NAME_EDGE,
678 PhabricatorProjectMaterializedMemberEdgeType::EDGECONST);
679 }
680
681 if ($this->watcherPHIDs !== null) {
682 $joins[] = qsprintf(
683 $conn,
684 'JOIN %T w ON w.src = project.phid AND w.type = %d',
685 PhabricatorEdgeConfig::TABLE_NAME_EDGE,
686 PhabricatorObjectHasWatcherEdgeType::EDGECONST);
687 }
688
689 if ($this->slugs !== null) {
690 $joins[] = qsprintf(
691 $conn,
692 'JOIN %T slug on slug.projectPHID = project.phid',
693 id(new PhabricatorProjectSlug())->getTableName());
694 }
695
696 if ($this->nameTokens !== null) {
697 $name_tokens = $this->getNameTokensForQuery($this->nameTokens);
698 foreach ($name_tokens as $key => $token) {
699 $token_table = 'token_'.$key;
700 $joins[] = qsprintf(
701 $conn,
702 'JOIN %T %T ON %T.projectID = project.id AND %T.token LIKE %>',
703 PhabricatorProject::TABLE_DATASOURCE_TOKEN,
704 $token_table,
705 $token_table,
706 $token_table,
707 $token);
708 }
709 }
710
711 return $joins;
712 }
713
714 public function getQueryApplicationClass() {
715 return PhabricatorProjectApplication::class;
716 }
717
718 protected function getPrimaryTableAlias() {
719 return 'project';
720 }
721
722 private function linkProjectGraph(array $projects, array $ancestors) {
723 $ancestor_map = mpull($ancestors, null, 'getPHID');
724 $projects_map = mpull($projects, null, 'getPHID');
725
726 $all_map = $projects_map + $ancestor_map;
727
728 $done = array();
729 foreach ($projects as $key => $project) {
730 $seen = array($project->getPHID() => true);
731
732 if (!$this->linkProject($project, $all_map, $done, $seen)) {
733 $this->didRejectResult($project);
734 unset($projects[$key]);
735 continue;
736 }
737
738 foreach ($project->getAncestorProjects() as $ancestor) {
739 $seen[$ancestor->getPHID()] = true;
740 }
741 }
742
743 return $projects;
744 }
745
746 private function linkProject($project, array $all, array $done, array $seen) {
747 $parent_phid = $project->getParentProjectPHID();
748
749 // This project has no parent, so just attach `null` and return.
750 if (!$parent_phid) {
751 $project->attachParentProject(null);
752 return true;
753 }
754
755 // This project has a parent, but it failed to load.
756 if (empty($all[$parent_phid])) {
757 return false;
758 }
759
760 // Test for graph cycles. If we encounter one, we're going to hide the
761 // entire cycle since we can't meaningfully resolve it.
762 if (isset($seen[$parent_phid])) {
763 return false;
764 }
765
766 $seen[$parent_phid] = true;
767
768 $parent = $all[$parent_phid];
769 $project->attachParentProject($parent);
770
771 if (!empty($done[$parent_phid])) {
772 return true;
773 }
774
775 return $this->linkProject($parent, $all, $done, $seen);
776 }
777
778 private function getAllReachableAncestors(array $projects) {
779 $ancestors = array();
780
781 $seen = mpull($projects, null, 'getPHID');
782
783 $stack = $projects;
784 while ($stack) {
785 $project = array_pop($stack);
786
787 $phid = $project->getPHID();
788 $ancestors[$phid] = $project;
789
790 $parent_phid = $project->getParentProjectPHID();
791 if (!$parent_phid) {
792 continue;
793 }
794
795 if (isset($seen[$parent_phid])) {
796 continue;
797 }
798
799 $seen[$parent_phid] = true;
800 $stack[] = $project->getParentProject();
801 }
802
803 return $ancestors;
804 }
805
806 private function loadSlugs(array $projects) {
807 // Build a map from primary slugs to projects.
808 $primary_map = array();
809 foreach ($projects as $project) {
810 $primary_slug = $project->getPrimarySlug();
811 if ($primary_slug === null) {
812 continue;
813 }
814
815 $primary_map[$primary_slug] = $project;
816 }
817
818 // Link up all of the queried slugs which correspond to primary
819 // slugs. If we can link up everything from this (no slugs were queried,
820 // or only primary slugs were queried) we don't need to load anything
821 // else.
822 $unknown = $this->slugNormals;
823 foreach ($unknown as $input => $normal) {
824 if (isset($primary_map[$input])) {
825 $match = $input;
826 } else if (isset($primary_map[$normal])) {
827 $match = $normal;
828 } else {
829 continue;
830 }
831
832 $this->slugMap[$input] = array(
833 'slug' => $match,
834 'projectPHID' => $primary_map[$match]->getPHID(),
835 );
836
837 unset($unknown[$input]);
838 }
839
840 // If we need slugs, we have to load everything.
841 // If we still have some queried slugs which we haven't mapped, we only
842 // need to look for them.
843 // If we've mapped everything, we don't have to do any work.
844 $project_phids = mpull($projects, 'getPHID');
845 if ($this->needSlugs) {
846 $slugs = id(new PhabricatorProjectSlug())->loadAllWhere(
847 'projectPHID IN (%Ls)',
848 $project_phids);
849 } else if ($unknown) {
850 $slugs = id(new PhabricatorProjectSlug())->loadAllWhere(
851 'projectPHID IN (%Ls) AND slug IN (%Ls)',
852 $project_phids,
853 $unknown);
854 } else {
855 $slugs = array();
856 }
857
858 // Link up any slugs we were not able to link up earlier.
859 $extra_map = mpull($slugs, 'getProjectPHID', 'getSlug');
860 foreach ($unknown as $input => $normal) {
861 if (isset($extra_map[$input])) {
862 $match = $input;
863 } else if (isset($extra_map[$normal])) {
864 $match = $normal;
865 } else {
866 continue;
867 }
868
869 $this->slugMap[$input] = array(
870 'slug' => $match,
871 'projectPHID' => $extra_map[$match],
872 );
873
874 unset($unknown[$input]);
875 }
876
877 if ($this->needSlugs) {
878 $slug_groups = mgroup($slugs, 'getProjectPHID');
879 foreach ($projects as $project) {
880 $project_slugs = idx($slug_groups, $project->getPHID(), array());
881 $project->attachSlugs($project_slugs);
882 }
883 }
884 }
885
886 private function getNameTokensForQuery(array $tokens) {
887 // When querying for projects by name, only actually search for the five
888 // longest tokens. MySQL can get grumpy with a large number of JOINs
889 // with LIKEs and queries for more than 5 tokens are essentially never
890 // legitimate searches for projects, but users copy/pasting nonsense.
891 // See also PHI47.
892
893 $length_map = array();
894 foreach ($tokens as $token) {
895 $length_map[$token] = strlen($token);
896 }
897 arsort($length_map);
898
899 $length_map = array_slice($length_map, 0, 5, true);
900
901 return array_keys($length_map);
902 }
903
904}