diff --git a/src/applications/phid/query/PhabricatorObjectQuery.php b/src/applications/phid/query/PhabricatorObjectQuery.php index 65e9938311..8364f0210d 100644 --- a/src/applications/phid/query/PhabricatorObjectQuery.php +++ b/src/applications/phid/query/PhabricatorObjectQuery.php @@ -1,217 +1,227 @@ phids = $phids; return $this; } public function withNames(array $names) { $this->names = $names; return $this; } public function withTypes(array $types) { $this->types = $types; return $this; } protected function loadPage() { if ($this->namedResults === null) { $this->namedResults = array(); } - $types = PhabricatorPHIDType::getAllTypes(); - if ($this->types) { - $types = array_select_keys($types, $this->types); - } - $names = array_unique($this->names); $phids = $this->phids; // We allow objects to be named by their PHID in addition to their normal // name so that, e.g., CLI tools which accept object names can also accept // PHIDs and work as users expect. $actually_phids = array(); if ($names) { foreach ($names as $key => $name) { if (!strncmp($name, 'PHID-', 5)) { $actually_phids[] = $name; $phids[] = $name; unset($names[$key]); } } } - $phids = array_unique($phids); - if ($names) { + $types = PhabricatorPHIDType::getAllTypes(); + if ($this->types) { + $types = array_select_keys($types, $this->types); + } $name_results = $this->loadObjectsByName($types, $names); } else { $name_results = array(); } if ($phids) { + $phids = array_unique($phids); + + $phid_types = array(); + foreach ($phids as $phid) { + $phid_type = phid_get_type($phid); + $phid_types[$phid_type] = $phid_type; + } + + $types = PhabricatorPHIDType::getTypes($phid_types); + if ($this->types) { + $types = array_select_keys($types, $this->types); + } + $phid_results = $this->loadObjectsByPHID($types, $phids); } else { $phid_results = array(); } foreach ($actually_phids as $phid) { if (isset($phid_results[$phid])) { $name_results[$phid] = $phid_results[$phid]; } } $this->namedResults += $name_results; return $phid_results + mpull($name_results, null, 'getPHID'); } public function getNamedResults() { if ($this->namedResults === null) { throw new PhutilInvalidStateException('execute'); } return $this->namedResults; } private function loadObjectsByName(array $types, array $names) { $groups = array(); foreach ($names as $name) { foreach ($types as $type => $type_impl) { if (!$type_impl->canLoadNamedObject($name)) { continue; } $groups[$type][] = $name; break; } } $results = array(); foreach ($groups as $type => $group) { $results += $types[$type]->loadNamedObjects($this, $group); } return $results; } private function loadObjectsByPHID(array $types, array $phids) { $results = array(); $groups = array(); foreach ($phids as $phid) { $type = phid_get_type($phid); $groups[$type][] = $phid; } $in_flight = $this->getPHIDsInFlight(); foreach ($groups as $type => $group) { // We check the workspace for each group, because some groups may trigger // other groups to load (for example, transactions load their objects). $workspace = $this->getObjectsFromWorkspace($group); foreach ($group as $key => $phid) { if (isset($workspace[$phid])) { $results[$phid] = $workspace[$phid]; unset($group[$key]); } } if (!$group) { continue; } // Don't try to load PHIDs which are already "in flight"; this prevents // us from recursing indefinitely if policy checks or edges form a loop. // We will decline to load the corresponding objects. foreach ($group as $key => $phid) { if (isset($in_flight[$phid])) { unset($group[$key]); } } if ($group && isset($types[$type])) { $this->putPHIDsInFlight($group); $objects = $types[$type]->loadObjects($this, $group); $map = mpull($objects, null, 'getPHID'); $this->putObjectsInWorkspace($map); $results += $map; } } return $results; } protected function didFilterResults(array $filtered) { foreach ($this->namedResults as $name => $result) { if (isset($filtered[$result->getPHID()])) { unset($this->namedResults[$name]); } } } /** * This query disables policy filtering if the only required capability is * the view capability. * * The view capability is always checked in the subqueries, so we do not need * to re-filter results. For any other set of required capabilities, we do. */ protected function shouldDisablePolicyFiltering() { $view_capability = PhabricatorPolicyCapability::CAN_VIEW; if ($this->getRequiredCapabilities() === array($view_capability)) { return true; } return false; } public function getQueryApplicationClass() { return null; } /** * Select invalid or restricted PHIDs from a list. * * PHIDs are invalid if their objects do not exist or can not be seen by the * viewer. This method is generally used to validate that PHIDs affected by * a transaction are valid. * * @param PhabricatorUser Viewer. * @param list List of ostensibly valid PHIDs. * @return list List of invalid or restricted PHIDs. */ public static function loadInvalidPHIDsForViewer( PhabricatorUser $viewer, array $phids) { if (!$phids) { return array(); } $objects = id(new PhabricatorObjectQuery()) ->setViewer($viewer) ->withPHIDs($phids) ->execute(); $objects = mpull($objects, null, 'getPHID'); $invalid = array(); foreach ($phids as $phid) { if (empty($objects[$phid])) { $invalid[] = $phid; } } return $invalid; } } diff --git a/src/applications/phid/type/PhabricatorPHIDType.php b/src/applications/phid/type/PhabricatorPHIDType.php index 99deb84763..2f82dcf169 100644 --- a/src/applications/phid/type/PhabricatorPHIDType.php +++ b/src/applications/phid/type/PhabricatorPHIDType.php @@ -1,197 +1,208 @@ getPhobjectClassConstant('TYPECONST'); if (!is_string($const) || !preg_match('/^[A-Z]{4}$/', $const)) { throw new Exception( pht( '%s class "%s" has an invalid %s property. PHID '. 'constants must be a four character uppercase string.', __CLASS__, get_class($this), 'TYPECONST')); } return $const; } abstract public function getTypeName(); public function getTypeIcon() { // Default to the application icon if the type doesn't specify one. $application_class = $this->getPHIDTypeApplicationClass(); if ($application_class) { $application = newv($application_class, array()); return $application->getIcon(); } return null; } public function newObject() { return null; } /** * Get the class name for the application this type belongs to. * * @return string|null Class name of the corresponding application, or null * if the type is not bound to an application. */ abstract public function getPHIDTypeApplicationClass(); /** * Build a @{class:PhabricatorPolicyAwareQuery} to load objects of this type * by PHID. * * If you can not build a single query which satisfies this requirement, you * can provide a dummy implementation for this method and overload * @{method:loadObjects} instead. * * @param PhabricatorObjectQuery Query being executed. * @param list PHIDs to load. * @return PhabricatorPolicyAwareQuery Query object which loads the * specified PHIDs when executed. */ abstract protected function buildQueryForObjects( PhabricatorObjectQuery $query, array $phids); /** * Load objects of this type, by PHID. For most PHID types, it is only * necessary to implement @{method:buildQueryForObjects} to get object * loading to work. * * @param PhabricatorObjectQuery Query being executed. * @param list PHIDs to load. * @return list Corresponding objects. */ public function loadObjects( PhabricatorObjectQuery $query, array $phids) { $object_query = $this->buildQueryForObjects($query, $phids) ->setViewer($query->getViewer()) ->setParentQuery($query); // If the user doesn't have permission to use the application at all, // just mark all the PHIDs as filtered. This primarily makes these // objects show up as "Restricted" instead of "Unknown" when loaded as // handles, which is technically true. if (!$object_query->canViewerUseQueryApplication()) { $object_query->addPolicyFilteredPHIDs(array_fuse($phids)); return array(); } return $object_query->execute(); } /** * Populate provided handles with application-specific data, like titles and * URIs. * * NOTE: The `$handles` and `$objects` lists are guaranteed to be nonempty * and have the same keys: subclasses are expected to load information only * for handles with visible objects. * * Because of this guarantee, a safe implementation will typically look like* * * foreach ($handles as $phid => $handle) { * $object = $objects[$phid]; * * $handle->setStuff($object->getStuff()); * // ... * } * * In general, an implementation should call `setName()` and `setURI()` on * each handle at a minimum. See @{class:PhabricatorObjectHandle} for other * handle properties. * * @param PhabricatorHandleQuery Issuing query object. * @param list Handles to populate with data. * @param list Objects for these PHIDs loaded by * @{method:buildQueryForObjects()}. * @return void */ abstract public function loadHandles( PhabricatorHandleQuery $query, array $handles, array $objects); public function canLoadNamedObject($name) { return false; } public function loadNamedObjects( PhabricatorObjectQuery $query, array $names) { throw new PhutilMethodNotImplementedException(); } /** * Get all known PHID types. * * To get PHID types a given user has access to, see * @{method:getAllInstalledTypes}. * * @return dict Map of type constants to types. */ final public static function getAllTypes() { + return self::newClassMapQuery() + ->execute(); + } + + final public static function getTypes(array $types) { + return id(new PhabricatorCachedClassMapQuery()) + ->setClassMapQuery(self::newClassMapQuery()) + ->setMapKeyMethod('getTypeConstant') + ->loadClasses($types); + } + + private static function newClassMapQuery() { return id(new PhutilClassMapQuery()) ->setAncestorClass(__CLASS__) - ->setUniqueMethod('getTypeConstant') - ->execute(); + ->setUniqueMethod('getTypeConstant'); } /** * Get all PHID types of applications installed for a given viewer. * * @param PhabricatorUser Viewing user. * @return dict Map of constants to installed * types. */ public static function getAllInstalledTypes(PhabricatorUser $viewer) { $all_types = self::getAllTypes(); $installed_types = array(); $app_classes = array(); foreach ($all_types as $key => $type) { $app_class = $type->getPHIDTypeApplicationClass(); if ($app_class === null) { // If the PHID type isn't bound to an application, include it as // installed. $installed_types[$key] = $type; continue; } // Otherwise, we need to check if this application is installed before // including the PHID type. $app_classes[$app_class][$key] = $type; } if ($app_classes) { $apps = id(new PhabricatorApplicationQuery()) ->setViewer($viewer) ->withInstalled(true) ->withClasses(array_keys($app_classes)) ->execute(); foreach ($apps as $app_class => $app) { $installed_types += $app_classes[$app_class]; } } return $installed_types; } }