Page MenuHomePhorge

No OneTemporary

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 @@
<?php
/**
* @task info Application Information
* @task ui UI Integration
* @task uri URI Routing
* @task mail Email integration
* @task fact Fact Integration
* @task meta Application Management
*/
abstract class PhabricatorApplication
extends Phobject
implements PhabricatorPolicyInterface {
const MAX_STATUS_ITEMS = 100;
const GROUP_CORE = 'core';
const GROUP_UTILITIES = 'util';
const GROUP_ADMIN = 'admin';
const GROUP_DEVELOPER = 'developer';
final public static function getApplicationGroups() {
return array(
self::GROUP_CORE => 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<PhabricatorApplicationStatusView> 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<PHUIListItemView> 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<PHUIListItemView> 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 @@
<?php
/**
* A @{class:PhabricatorQuery} which filters results according to visibility
* policies for the querying user. Broadly, this class allows you to implement
* a query that returns only objects the user is allowed to see.
*
* $results = id(new ExampleQuery())
* ->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<PhabricatorPolicyInterface> 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<phid, phid> 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<phid, PhabricatorPolicyInterface> 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<phid> 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 `<phid, PhabricatorPolicyInterface>` map.
*
* @param list<PhabricatorPolicyInterface> Objects.
* @return map<phid, PhabricatorPolicyInterface> 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<phid> 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<phid, phid> 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<PhabricatorPolicyInterface> 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<PhabricatorPolicyInterface> 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<wild> Results from `loadPage()`.
* @return list<PhabricatorPolicyInterface> 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<wild> Results from @{method:willFilterPage()}.
* @return list<PhabricatorPolicyInterface> 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<wild> 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<PhabricatorPolicyInterface> Query results.
* @return list<PhabricatorPolicyInterface> 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);
}
}

File Metadata

Mime Type
text/x-diff
Expires
Sun, Jan 19, 15:51 (2 w, 6 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1126183
Default Alt Text
(38 KB)

Event Timeline