diff --git a/src/applications/base/PhabricatorApplication.php b/src/applications/base/PhabricatorApplication.php index 03f14a241b..1f605476bc 100644 --- a/src/applications/base/PhabricatorApplication.php +++ b/src/applications/base/PhabricatorApplication.php @@ -1,602 +1,613 @@ pht('Core Applications'), self::GROUP_UTILITIES => pht('Utilities'), self::GROUP_ADMIN => pht('Administration'), self::GROUP_DEVELOPER => pht('Developer Tools'), ); } /* -( Application Information )-------------------------------------------- */ abstract public function getName(); public function getShortDescription() { return pht('%s Application', $this->getName()); } final public function isInstalled() { if (!$this->canUninstall()) { return true; } $prototypes = PhabricatorEnv::getEnvConfig('phabricator.show-prototypes'); if (!$prototypes && $this->isPrototype()) { return false; } $uninstalled = PhabricatorEnv::getEnvConfig( 'phabricator.uninstalled-applications'); return empty($uninstalled[get_class($this)]); } public function isPrototype() { return false; } /** * Return `true` if this application should never appear in application lists * in the UI. Primarily intended for unit test applications or other * pseudo-applications. * * Few applications should be unlisted. For most applications, use * @{method:isLaunchable} to hide them from main launch views instead. * * @return bool True to remove application from UI lists. */ public function isUnlisted() { return false; } /** * Return `true` if this application is a normal application with a base * URI and a web interface. * * Launchable applications can be pinned to the home page, and show up in the * "Launcher" view of the Applications application. Making an application * unlauncahble prevents pinning and hides it from this view. * * Usually, an application should be marked unlaunchable if: * * - it is available on every page anyway (like search); or * - it does not have a web interface (like subscriptions); or * - it is still pre-release and being intentionally buried. * * To hide applications more completely, use @{method:isUnlisted}. * * @return bool True if the application is launchable. */ public function isLaunchable() { return true; } /** * Return `true` if this application should be pinned by default. * * Users who have not yet set preferences see a default list of applications. * * @param PhabricatorUser User viewing the pinned application list. * @return bool True if this application should be pinned by default. */ public function isPinnedByDefault(PhabricatorUser $viewer) { return false; } /** * Returns true if an application is first-party (developed by Phacility) * and false otherwise. * * @return bool True if this application is developed by Phacility. */ final public function isFirstParty() { $where = id(new ReflectionClass($this))->getFileName(); $root = phutil_get_library_root('phabricator'); if (!Filesystem::isDescendant($where, $root)) { return false; } if (Filesystem::isDescendant($where, $root.'/extensions')) { return false; } return true; } public function canUninstall() { return true; } final public function getPHID() { return 'PHID-APPS-'.get_class($this); } public function getTypeaheadURI() { return $this->isLaunchable() ? $this->getBaseURI() : null; } public function getBaseURI() { return null; } final public function getApplicationURI($path = '') { return $this->getBaseURI().ltrim($path, '/'); } public function getIconURI() { return null; } public function getFontIcon() { return 'fa-puzzle-piece'; } public function getApplicationOrder() { return PHP_INT_MAX; } public function getApplicationGroup() { return self::GROUP_CORE; } public function getTitleGlyph() { return null; } final public function getHelpMenuItems(PhabricatorUser $viewer) { $items = array(); $articles = $this->getHelpDocumentationArticles($viewer); if ($articles) { $items[] = id(new PHUIListItemView()) ->setType(PHUIListItemView::TYPE_LABEL) ->setName(pht('%s Documentation', $this->getName())); foreach ($articles as $article) { $item = id(new PHUIListItemView()) ->setName($article['name']) ->setIcon('fa-book') ->setHref($article['href']); $items[] = $item; } } $command_specs = $this->getMailCommandObjects(); if ($command_specs) { $items[] = id(new PHUIListItemView()) ->setType(PHUIListItemView::TYPE_LABEL) ->setName(pht('Email Help')); foreach ($command_specs as $key => $spec) { $object = $spec['object']; $class = get_class($this); $href = '/applications/mailcommands/'.$class.'/'.$key.'/'; $item = id(new PHUIListItemView()) ->setName($spec['name']) ->setIcon('fa-envelope-o') ->setHref($href); $items[] = $item; } } return $items; } public function getHelpDocumentationArticles(PhabricatorUser $viewer) { return array(); } public function getOverview() { return null; } public function getEventListeners() { return array(); } public function getRemarkupRules() { return array(); } public function getQuicksandURIPatternBlacklist() { return array(); } public function getMailCommandObjects() { return array(); } /* -( URI Routing )-------------------------------------------------------- */ public function getRoutes() { return array(); } /* -( Email Integration )-------------------------------------------------- */ public function supportsEmailIntegration() { return false; } final protected function getInboundEmailSupportLink() { return PhabricatorEnv::getDocLink('Configuring Inbound Email'); } public function getAppEmailBlurb() { throw new PhutilMethodNotImplementedException(); } /* -( Fact Integration )--------------------------------------------------- */ public function getFactObjectsForAnalysis() { return array(); } /* -( UI Integration )----------------------------------------------------- */ /** * Render status elements (like "3 Waiting Reviews") for application list * views. These provide a way to alert users to new or pending action items * in applications. * * @param PhabricatorUser Viewing user. * @return list Application status elements. * @task ui */ public function loadStatus(PhabricatorUser $user) { return array(); } /** * @return string * @task ui */ final public static function formatStatusCount( $count, $limit_string = '%s', $base_string = '%d') { if ($count == self::MAX_STATUS_ITEMS) { $count_str = pht($limit_string, ($count - 1).'+'); } else { $count_str = pht($base_string, $count); } return $count_str; } /** * You can provide an optional piece of flavor text for the application. This * is currently rendered in application launch views if the application has no * status elements. * * @return string|null Flavor text. * @task ui */ public function getFlavorText() { return null; } /** * Build items for the main menu. * * @param PhabricatorUser The viewing user. * @param AphrontController The current controller. May be null for special * pages like 404, exception handlers, etc. * @return list List of menu items. * @task ui */ public function buildMainMenuItems( PhabricatorUser $user, PhabricatorController $controller = null) { return array(); } /** * Build extra items for the main menu. Generally, this is used to render * static dropdowns. * * @param PhabricatorUser The viewing user. * @param AphrontController The current controller. May be null for special * pages like 404, exception handlers, etc. * @return view List of menu items. * @task ui */ public function buildMainMenuExtraNodes( PhabricatorUser $viewer, PhabricatorController $controller = null) { return array(); } /** * Build items for the "quick create" menu. * * @param PhabricatorUser The viewing user. * @return list List of menu items. */ public function getQuickCreateItems(PhabricatorUser $viewer) { return array(); } /* -( Application Management )--------------------------------------------- */ final public static function getByClass($class_name) { $selected = null; $applications = self::getAllApplications(); foreach ($applications as $application) { if (get_class($application) == $class_name) { $selected = $application; break; } } if (!$selected) { throw new Exception(pht("No application '%s'!", $class_name)); } return $selected; } final public static function getAllApplications() { static $applications; if ($applications === null) { $apps = id(new PhutilSymbolLoader()) ->setAncestorClass(__CLASS__) ->loadObjects(); // Reorder the applications into "application order". Notably, this // ensures their event handlers register in application order. $apps = msort($apps, 'getApplicationOrder'); $apps = mgroup($apps, 'getApplicationGroup'); $group_order = array_keys(self::getApplicationGroups()); $apps = array_select_keys($apps, $group_order) + $apps; $apps = array_mergev($apps); $applications = $apps; } return $applications; } final public static function getAllInstalledApplications() { $all_applications = self::getAllApplications(); $apps = array(); foreach ($all_applications as $app) { if (!$app->isInstalled()) { continue; } $apps[] = $app; } return $apps; } /** * Determine if an application is installed, by application class name. * * To check if an application is installed //and// available to a particular * viewer, user @{method:isClassInstalledForViewer}. * * @param string Application class name. * @return bool True if the class is installed. * @task meta */ final public static function isClassInstalled($class) { return self::getByClass($class)->isInstalled(); } /** * Determine if an application is installed and available to a viewer, by * application class name. * * To check if an application is installed at all, use * @{method:isClassInstalled}. * * @param string Application class name. * @param PhabricatorUser Viewing user. * @return bool True if the class is installed for the viewer. * @task meta */ final public static function isClassInstalledForViewer( $class, PhabricatorUser $viewer) { - if (!self::isClassInstalled($class)) { - return false; + $cache = PhabricatorCaches::getRequestCache(); + $viewer_phid = $viewer->getPHID(); + $key = 'app.'.$class.'.installed.'.$viewer_phid; + + $result = $cache->getKey($key); + if ($result === null) { + if (!self::isClassInstalled($class)) { + $result = false; + } else { + $result = PhabricatorPolicyFilter::hasCapability( + $viewer, + self::getByClass($class), + PhabricatorPolicyCapability::CAN_VIEW); + } + + $cache->setKey($key, $result); } - return PhabricatorPolicyFilter::hasCapability( - $viewer, - self::getByClass($class), - PhabricatorPolicyCapability::CAN_VIEW); + return $result; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array_merge( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ), array_keys($this->getCustomCapabilities())); } public function getPolicy($capability) { $default = $this->getCustomPolicySetting($capability); if ($default) { return $default; } switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return PhabricatorPolicies::getMostOpenPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return PhabricatorPolicies::POLICY_ADMIN; default: $spec = $this->getCustomCapabilitySpecification($capability); return idx($spec, 'default', PhabricatorPolicies::POLICY_USER); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { return null; } /* -( Policies )----------------------------------------------------------- */ protected function getCustomCapabilities() { return array(); } final private function getCustomPolicySetting($capability) { if (!$this->isCapabilityEditable($capability)) { return null; } $policy_locked = PhabricatorEnv::getEnvConfig('policy.locked'); if (isset($policy_locked[$capability])) { return $policy_locked[$capability]; } $config = PhabricatorEnv::getEnvConfig('phabricator.application-settings'); $app = idx($config, $this->getPHID()); if (!$app) { return null; } $policy = idx($app, 'policy'); if (!$policy) { return null; } return idx($policy, $capability); } final private function getCustomCapabilitySpecification($capability) { $custom = $this->getCustomCapabilities(); if (!isset($custom[$capability])) { throw new Exception(pht("Unknown capability '%s'!", $capability)); } return $custom[$capability]; } final public function getCapabilityLabel($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return pht('Can Use Application'); case PhabricatorPolicyCapability::CAN_EDIT: return pht('Can Configure Application'); } $capobj = PhabricatorPolicyCapability::getCapabilityByKey($capability); if ($capobj) { return $capobj->getCapabilityName(); } return null; } final public function isCapabilityEditable($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->canUninstall(); case PhabricatorPolicyCapability::CAN_EDIT: return false; default: $spec = $this->getCustomCapabilitySpecification($capability); return idx($spec, 'edit', true); } } final public function getCapabilityCaption($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: if (!$this->canUninstall()) { return pht( 'This application is required for Phabricator to operate, so all '. 'users must have access to it.'); } else { return null; } case PhabricatorPolicyCapability::CAN_EDIT: return null; default: $spec = $this->getCustomCapabilitySpecification($capability); return idx($spec, 'caption'); } } final public function getCapabilityTemplatePHIDType($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: case PhabricatorPolicyCapability::CAN_EDIT: return null; } $spec = $this->getCustomCapabilitySpecification($capability); return idx($spec, 'template'); } public function getApplicationSearchDocumentTypes() { return array(); } } diff --git a/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php b/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php index e7b9b35628..30b7b8dd0e 100644 --- a/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php +++ b/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php @@ -1,699 +1,690 @@ setViewer($user) * ->withConstraint($example) * ->execute(); * * Normally, you should extend @{class:PhabricatorCursorPagedPolicyAwareQuery}, * not this class. @{class:PhabricatorCursorPagedPolicyAwareQuery} provides a * more practical interface for building usable queries against most object * types. * * NOTE: Although this class extends @{class:PhabricatorOffsetPagedQuery}, * offset paging with policy filtering is not efficient. All results must be * loaded into the application and filtered here: skipping `N` rows via offset * is an `O(N)` operation with a large constant. Prefer cursor-based paging * with @{class:PhabricatorCursorPagedPolicyAwareQuery}, which can filter far * more efficiently in MySQL. * * @task config Query Configuration * @task exec Executing Queries * @task policyimpl Policy Query Implementation */ abstract class PhabricatorPolicyAwareQuery extends PhabricatorOffsetPagedQuery { private $viewer; private $parentQuery; private $rawResultLimit; private $capabilities; private $workspace = array(); private $inFlightPHIDs = array(); private $policyFilteredPHIDs = array(); - private $canUseApplication; /** * Should we continue or throw an exception when a query result is filtered * by policy rules? * * Values are `true` (raise exceptions), `false` (do not raise exceptions) * and `null` (inherit from parent query, with no exceptions by default). */ private $raisePolicyExceptions; /* -( Query Configuration )------------------------------------------------ */ /** * Set the viewer who is executing the query. Results will be filtered * according to the viewer's capabilities. You must set a viewer to execute * a policy query. * * @param PhabricatorUser The viewing user. * @return this * @task config */ final public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; return $this; } /** * Get the query's viewer. * * @return PhabricatorUser The viewing user. * @task config */ final public function getViewer() { return $this->viewer; } /** * Set the parent query of this query. This is useful for nested queries so * that configuration like whether or not to raise policy exceptions is * seamlessly passed along to child queries. * * @return this * @task config */ final public function setParentQuery(PhabricatorPolicyAwareQuery $query) { $this->parentQuery = $query; return $this; } /** * Get the parent query. See @{method:setParentQuery} for discussion. * * @return PhabricatorPolicyAwareQuery The parent query. * @task config */ final public function getParentQuery() { return $this->parentQuery; } /** * Hook to configure whether this query should raise policy exceptions. * * @return this * @task config */ final public function setRaisePolicyExceptions($bool) { $this->raisePolicyExceptions = $bool; return $this; } /** * @return bool * @task config */ final public function shouldRaisePolicyExceptions() { return (bool)$this->raisePolicyExceptions; } /** * @task config */ final public function requireCapabilities(array $capabilities) { $this->capabilities = $capabilities; return $this; } /* -( Query Execution )---------------------------------------------------- */ /** * Execute the query, expecting a single result. This method simplifies * loading objects for detail pages or edit views. * * // Load one result by ID. * $obj = id(new ExampleQuery()) * ->setViewer($user) * ->withIDs(array($id)) * ->executeOne(); * if (!$obj) { * return new Aphront404Response(); * } * * If zero results match the query, this method returns `null`. * If one result matches the query, this method returns that result. * * If two or more results match the query, this method throws an exception. * You should use this method only when the query constraints guarantee at * most one match (e.g., selecting a specific ID or PHID). * * If one result matches the query but it is caught by the policy filter (for * example, the user is trying to view or edit an object which exists but * which they do not have permission to see) a policy exception is thrown. * * @return mixed Single result, or null. * @task exec */ final public function executeOne() { $this->setRaisePolicyExceptions(true); try { $results = $this->execute(); } catch (Exception $ex) { $this->setRaisePolicyExceptions(false); throw $ex; } if (count($results) > 1) { throw new Exception(pht('Expected a single result!')); } if (!$results) { return null; } return head($results); } /** * Execute the query, loading all visible results. * * @return list Result objects. * @task exec */ final public function execute() { if (!$this->viewer) { throw new PhutilInvalidStateException('setViewer'); } $parent_query = $this->getParentQuery(); if ($parent_query && ($this->raisePolicyExceptions === null)) { $this->setRaisePolicyExceptions( $parent_query->shouldRaisePolicyExceptions()); } $results = array(); $filter = $this->getPolicyFilter(); $offset = (int)$this->getOffset(); $limit = (int)$this->getLimit(); $count = 0; if ($limit) { $need = $offset + $limit; } else { $need = 0; } $this->willExecute(); do { if ($need) { $this->rawResultLimit = min($need - $count, 1024); } else { $this->rawResultLimit = 0; } if ($this->canViewerUseQueryApplication()) { try { $page = $this->loadPage(); } catch (PhabricatorEmptyQueryException $ex) { $page = array(); } } else { $page = array(); } if ($page) { $maybe_visible = $this->willFilterPage($page); } else { $maybe_visible = array(); } if ($this->shouldDisablePolicyFiltering()) { $visible = $maybe_visible; } else { $visible = $filter->apply($maybe_visible); $policy_filtered = array(); foreach ($maybe_visible as $key => $object) { if (empty($visible[$key])) { $phid = $object->getPHID(); if ($phid) { $policy_filtered[$phid] = $phid; } } } $this->addPolicyFilteredPHIDs($policy_filtered); } if ($visible) { $this->putObjectsInWorkspace($this->getWorkspaceMapForPage($visible)); $visible = $this->didFilterPage($visible); } $removed = array(); foreach ($maybe_visible as $key => $object) { if (empty($visible[$key])) { $removed[$key] = $object; } } $this->didFilterResults($removed); foreach ($visible as $key => $result) { ++$count; // If we have an offset, we just ignore that many results and start // storing them only once we've hit the offset. This reduces memory // requirements for large offsets, compared to storing them all and // slicing them away later. if ($count > $offset) { $results[$key] = $result; } if ($need && ($count >= $need)) { // If we have all the rows we need, break out of the paging query. break 2; } } if (!$this->rawResultLimit) { // If we don't have a load count, we loaded all the results. We do // not need to load another page. break; } if (count($page) < $this->rawResultLimit) { // If we have a load count but the unfiltered results contained fewer // objects, we know this was the last page of objects; we do not need // to load another page because we can deduce it would be empty. break; } $this->nextPage($page); } while (true); $results = $this->didLoadResults($results); return $results; } private function getPolicyFilter() { $filter = new PhabricatorPolicyFilter(); $filter->setViewer($this->viewer); $capabilities = $this->getRequiredCapabilities(); $filter->requireCapabilities($capabilities); $filter->raisePolicyExceptions($this->shouldRaisePolicyExceptions()); return $filter; } protected function getRequiredCapabilities() { if ($this->capabilities) { return $this->capabilities; } return array( PhabricatorPolicyCapability::CAN_VIEW, ); } protected function applyPolicyFilter(array $objects, array $capabilities) { if ($this->shouldDisablePolicyFiltering()) { return $objects; } $filter = $this->getPolicyFilter(); $filter->requireCapabilities($capabilities); return $filter->apply($objects); } protected function didRejectResult(PhabricatorPolicyInterface $object) { // Some objects (like commits) may be rejected because related objects // (like repositories) can not be loaded. In some cases, we may need these // related objects to determine the object policy, so it's expected that // we may occasionally be unable to determine the policy. try { $policy = $object->getPolicy(PhabricatorPolicyCapability::CAN_VIEW); } catch (Exception $ex) { $policy = null; } // Mark this object as filtered so handles can render "Restricted" instead // of "Unknown". $phid = $object->getPHID(); $this->addPolicyFilteredPHIDs(array($phid => $phid)); $this->getPolicyFilter()->rejectObject( $object, $policy, PhabricatorPolicyCapability::CAN_VIEW); } public function addPolicyFilteredPHIDs(array $phids) { $this->policyFilteredPHIDs += $phids; if ($this->getParentQuery()) { $this->getParentQuery()->addPolicyFilteredPHIDs($phids); } return $this; } /** * Return a map of all object PHIDs which were loaded in the query but * filtered out by policy constraints. This allows a caller to distinguish * between objects which do not exist (or, at least, were filtered at the * content level) and objects which exist but aren't visible. * * @return map Map of object PHIDs which were filtered * by policies. * @task exec */ public function getPolicyFilteredPHIDs() { return $this->policyFilteredPHIDs; } /* -( Query Workspace )---------------------------------------------------- */ /** * Put a map of objects into the query workspace. Many queries perform * subqueries, which can eventually end up loading the same objects more than * once (often to perform policy checks). * * For example, loading a user may load the user's profile image, which might * load the user object again in order to verify that the viewer has * permission to see the file. * * The "query workspace" allows queries to load objects from elsewhere in a * query block instead of refetching them. * * When using the query workspace, it's important to obey two rules: * * **Never put objects into the workspace which the viewer may not be able * to see**. You need to apply all policy filtering //before// putting * objects in the workspace. Otherwise, subqueries may read the objects and * use them to permit access to content the user shouldn't be able to view. * * **Fully enrich objects pulled from the workspace.** After pulling objects * from the workspace, you still need to load and attach any additional * content the query requests. Otherwise, a query might return objects without * requested content. * * Generally, you do not need to update the workspace yourself: it is * automatically populated as a side effect of objects surviving policy * filtering. * * @param map Objects to add to the query * workspace. * @return this * @task workspace */ public function putObjectsInWorkspace(array $objects) { assert_instances_of($objects, 'PhabricatorPolicyInterface'); $viewer_phid = $this->getViewer()->getPHID(); // The workspace is scoped per viewer to prevent accidental contamination. if (empty($this->workspace[$viewer_phid])) { $this->workspace[$viewer_phid] = array(); } $this->workspace[$viewer_phid] += $objects; return $this; } /** * Retrieve objects from the query workspace. For more discussion about the * workspace mechanism, see @{method:putObjectsInWorkspace}. This method * searches both the current query's workspace and the workspaces of parent * queries. * * @param list List of PHIDs to retrieve. * @return this * @task workspace */ public function getObjectsFromWorkspace(array $phids) { $viewer_phid = $this->getViewer()->getPHID(); $results = array(); foreach ($phids as $key => $phid) { if (isset($this->workspace[$viewer_phid][$phid])) { $results[$phid] = $this->workspace[$viewer_phid][$phid]; unset($phids[$key]); } } if ($phids && $this->getParentQuery()) { $results += $this->getParentQuery()->getObjectsFromWorkspace($phids); } return $results; } /** * Convert a result page to a `` map. * * @param list Objects. * @return map Map of objects which can * be put into the workspace. * @task workspace */ protected function getWorkspaceMapForPage(array $results) { $map = array(); foreach ($results as $result) { $phid = $result->getPHID(); if ($phid !== null) { $map[$phid] = $result; } } return $map; } /** * Mark PHIDs as in flight. * * PHIDs which are "in flight" are actively being queried for. Using this * list can prevent infinite query loops by aborting queries which cycle. * * @param list List of PHIDs which are now in flight. * @return this */ public function putPHIDsInFlight(array $phids) { foreach ($phids as $phid) { $this->inFlightPHIDs[$phid] = $phid; } return $this; } /** * Get PHIDs which are currently in flight. * * PHIDs which are "in flight" are actively being queried for. * * @return map PHIDs currently in flight. */ public function getPHIDsInFlight() { $results = $this->inFlightPHIDs; if ($this->getParentQuery()) { $results += $this->getParentQuery()->getPHIDsInFlight(); } return $results; } /* -( Policy Query Implementation )---------------------------------------- */ /** * Get the number of results @{method:loadPage} should load. If the value is * 0, @{method:loadPage} should load all available results. * * @return int The number of results to load, or 0 for all results. * @task policyimpl */ final protected function getRawResultLimit() { return $this->rawResultLimit; } /** * Hook invoked before query execution. Generally, implementations should * reset any internal cursors. * * @return void * @task policyimpl */ protected function willExecute() { return; } /** * Load a raw page of results. Generally, implementations should load objects * from the database. They should attempt to return the number of results * hinted by @{method:getRawResultLimit}. * * @return list List of filterable policy objects. * @task policyimpl */ abstract protected function loadPage(); /** * Update internal state so that the next call to @{method:loadPage} will * return new results. Generally, you should adjust a cursor position based * on the provided result page. * * @param list The current page of results. * @return void * @task policyimpl */ abstract protected function nextPage(array $page); /** * Hook for applying a page filter prior to the privacy filter. This allows * you to drop some items from the result set without creating problems with * pagination or cursor updates. You can also load and attach data which is * required to perform policy filtering. * * Generally, you should load non-policy data and perform non-policy filtering * later, in @{method:didFilterPage}. Strictly fewer objects will make it that * far (so the program will load less data) and subqueries from that context * can use the query workspace to further reduce query load. * * This method will only be called if data is available. Implementations * do not need to handle the case of no results specially. * * @param list Results from `loadPage()`. * @return list Objects for policy filtering. * @task policyimpl */ protected function willFilterPage(array $page) { return $page; } /** * Hook for performing additional non-policy loading or filtering after an * object has satisfied all policy checks. Generally, this means loading and * attaching related data. * * Subqueries executed during this phase can use the query workspace, which * may improve performance or make circular policies resolvable. Data which * is not necessary for policy filtering should generally be loaded here. * * This callback can still filter objects (for example, if attachable data * is discovered to not exist), but should not do so for policy reasons. * * This method will only be called if data is available. Implementations do * not need to handle the case of no results specially. * * @param list Results from @{method:willFilterPage()}. * @return list Objects after additional * non-policy processing. */ protected function didFilterPage(array $page) { return $page; } /** * Hook for removing filtered results from alternate result sets. This * hook will be called with any objects which were returned by the query but * filtered for policy reasons. The query should remove them from any cached * or partial result sets. * * @param list List of objects that should not be returned by alternate * result mechanisms. * @return void * @task policyimpl */ protected function didFilterResults(array $results) { return; } /** * Hook for applying final adjustments before results are returned. This is * used by @{class:PhabricatorCursorPagedPolicyAwareQuery} to reverse results * that are queried during reverse paging. * * @param list Query results. * @return list Final results. * @task policyimpl */ protected function didLoadResults(array $results) { return $results; } /** * Allows a subclass to disable policy filtering. This method is dangerous. * It should be used only if the query loads data which has already been * filtered (for example, because it wraps some other query which uses * normal policy filtering). * * @return bool True to disable all policy filtering. * @task policyimpl */ protected function shouldDisablePolicyFiltering() { return false; } /** * If this query belongs to an application, return the application class name * here. This will prevent the query from returning results if the viewer can * not access the application. * * If this query does not belong to an application, return `null`. * * @return string|null Application class name. */ abstract public function getQueryApplicationClass(); /** * Determine if the viewer has permission to use this query's application. * For queries which aren't part of an application, this method always returns * true. * * @return bool True if the viewer has application-level permission to * execute the query. */ public function canViewerUseQueryApplication() { - if ($this->canUseApplication === null) { - $class = $this->getQueryApplicationClass(); - if (!$class) { - $this->canUseApplication = true; - } else { - $result = id(new PhabricatorApplicationQuery()) - ->setViewer($this->getViewer()) - ->withClasses(array($class)) - ->execute(); - - $this->canUseApplication = (bool)$result; - } + $class = $this->getQueryApplicationClass(); + if (!$class) { + return true; } - return $this->canUseApplication; + $viewer = $this->getViewer(); + return PhabricatorApplication::isClassInstalledForViewer($class, $viewer); } }