@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
fork

Configure Feed

Select the types of activity you want to include in your feed.

at recaptime-dev/main 904 lines 24 kB view raw
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}