[ Index ]

PHP Cross Reference of Phabricator

title

Body

[close]

/src/infrastructure/query/policy/ -> PhabricatorPolicyAwareQuery.php (source)

   1  <?php
   2  
   3  /**
   4   * A @{class:PhabricatorQuery} which filters results according to visibility
   5   * policies for the querying user. Broadly, this class allows you to implement
   6   * a query that returns only objects the user is allowed to see.
   7   *
   8   *   $results = id(new ExampleQuery())
   9   *     ->setViewer($user)
  10   *     ->withConstraint($example)
  11   *     ->execute();
  12   *
  13   * Normally, you should extend @{class:PhabricatorCursorPagedPolicyAwareQuery},
  14   * not this class. @{class:PhabricatorCursorPagedPolicyAwareQuery} provides a
  15   * more practical interface for building usable queries against most object
  16   * types.
  17   *
  18   * NOTE: Although this class extends @{class:PhabricatorOffsetPagedQuery},
  19   * offset paging with policy filtering is not efficient. All results must be
  20   * loaded into the application and filtered here: skipping `N` rows via offset
  21   * is an `O(N)` operation with a large constant. Prefer cursor-based paging
  22   * with @{class:PhabricatorCursorPagedPolicyAwareQuery}, which can filter far
  23   * more efficiently in MySQL.
  24   *
  25   * @task config     Query Configuration
  26   * @task exec       Executing Queries
  27   * @task policyimpl Policy Query Implementation
  28   */
  29  abstract class PhabricatorPolicyAwareQuery extends PhabricatorOffsetPagedQuery {
  30  
  31    private $viewer;
  32    private $parentQuery;
  33    private $rawResultLimit;
  34    private $capabilities;
  35    private $workspace = array();
  36    private $policyFilteredPHIDs = array();
  37    private $canUseApplication;
  38  
  39    /**
  40     * Should we continue or throw an exception when a query result is filtered
  41     * by policy rules?
  42     *
  43     * Values are `true` (raise exceptions), `false` (do not raise exceptions)
  44     * and `null` (inherit from parent query, with no exceptions by default).
  45     */
  46    private $raisePolicyExceptions;
  47  
  48  
  49  /* -(  Query Configuration  )------------------------------------------------ */
  50  
  51  
  52    /**
  53     * Set the viewer who is executing the query. Results will be filtered
  54     * according to the viewer's capabilities. You must set a viewer to execute
  55     * a policy query.
  56     *
  57     * @param PhabricatorUser The viewing user.
  58     * @return this
  59     * @task config
  60     */
  61    final public function setViewer(PhabricatorUser $viewer) {
  62      $this->viewer = $viewer;
  63      return $this;
  64    }
  65  
  66  
  67    /**
  68     * Get the query's viewer.
  69     *
  70     * @return PhabricatorUser The viewing user.
  71     * @task config
  72     */
  73    final public function getViewer() {
  74      return $this->viewer;
  75    }
  76  
  77  
  78    /**
  79     * Set the parent query of this query. This is useful for nested queries so
  80     * that configuration like whether or not to raise policy exceptions is
  81     * seamlessly passed along to child queries.
  82     *
  83     * @return this
  84     * @task config
  85     */
  86    final public function setParentQuery(PhabricatorPolicyAwareQuery $query) {
  87      $this->parentQuery = $query;
  88      return $this;
  89    }
  90  
  91  
  92    /**
  93     * Get the parent query. See @{method:setParentQuery} for discussion.
  94     *
  95     * @return PhabricatorPolicyAwareQuery The parent query.
  96     * @task config
  97     */
  98    final public function getParentQuery() {
  99      return $this->parentQuery;
 100    }
 101  
 102  
 103    /**
 104     * Hook to configure whether this query should raise policy exceptions.
 105     *
 106     * @return this
 107     * @task config
 108     */
 109    final public function setRaisePolicyExceptions($bool) {
 110      $this->raisePolicyExceptions = $bool;
 111      return $this;
 112    }
 113  
 114  
 115    /**
 116     * @return bool
 117     * @task config
 118     */
 119    final public function shouldRaisePolicyExceptions() {
 120      return (bool)$this->raisePolicyExceptions;
 121    }
 122  
 123  
 124    /**
 125     * @task config
 126     */
 127    final public function requireCapabilities(array $capabilities) {
 128      $this->capabilities = $capabilities;
 129      return $this;
 130    }
 131  
 132  
 133  /* -(  Query Execution  )---------------------------------------------------- */
 134  
 135  
 136    /**
 137     * Execute the query, expecting a single result. This method simplifies
 138     * loading objects for detail pages or edit views.
 139     *
 140     *   // Load one result by ID.
 141     *   $obj = id(new ExampleQuery())
 142     *     ->setViewer($user)
 143     *     ->withIDs(array($id))
 144     *     ->executeOne();
 145     *   if (!$obj) {
 146     *     return new Aphront404Response();
 147     *   }
 148     *
 149     * If zero results match the query, this method returns `null`.
 150     * If one result matches the query, this method returns that result.
 151     *
 152     * If two or more results match the query, this method throws an exception.
 153     * You should use this method only when the query constraints guarantee at
 154     * most one match (e.g., selecting a specific ID or PHID).
 155     *
 156     * If one result matches the query but it is caught by the policy filter (for
 157     * example, the user is trying to view or edit an object which exists but
 158     * which they do not have permission to see) a policy exception is thrown.
 159     *
 160     * @return mixed Single result, or null.
 161     * @task exec
 162     */
 163    final public function executeOne() {
 164  
 165      $this->setRaisePolicyExceptions(true);
 166      try {
 167        $results = $this->execute();
 168      } catch (Exception $ex) {
 169        $this->setRaisePolicyExceptions(false);
 170        throw $ex;
 171      }
 172  
 173      if (count($results) > 1) {
 174        throw new Exception('Expected a single result!');
 175      }
 176  
 177      if (!$results) {
 178        return null;
 179      }
 180  
 181      return head($results);
 182    }
 183  
 184  
 185    /**
 186     * Execute the query, loading all visible results.
 187     *
 188     * @return list<PhabricatorPolicyInterface> Result objects.
 189     * @task exec
 190     */
 191    final public function execute() {
 192      if (!$this->viewer) {
 193        throw new Exception('Call setViewer() before execute()!');
 194      }
 195  
 196      $parent_query = $this->getParentQuery();
 197      if ($parent_query && ($this->raisePolicyExceptions === null)) {
 198        $this->setRaisePolicyExceptions(
 199          $parent_query->shouldRaisePolicyExceptions());
 200      }
 201  
 202      $results = array();
 203  
 204      $filter = $this->getPolicyFilter();
 205  
 206      $offset = (int)$this->getOffset();
 207      $limit  = (int)$this->getLimit();
 208      $count  = 0;
 209  
 210      if ($limit) {
 211        $need = $offset + $limit;
 212      } else {
 213        $need = 0;
 214      }
 215  
 216      $this->willExecute();
 217  
 218      do {
 219        if ($need) {
 220          $this->rawResultLimit = min($need - $count, 1024);
 221        } else {
 222          $this->rawResultLimit = 0;
 223        }
 224  
 225        if ($this->canViewerUseQueryApplication()) {
 226          try {
 227            $page = $this->loadPage();
 228          } catch (PhabricatorEmptyQueryException $ex) {
 229            $page = array();
 230          }
 231        } else {
 232          $page = array();
 233        }
 234  
 235        if ($page) {
 236          $maybe_visible = $this->willFilterPage($page);
 237        } else {
 238          $maybe_visible = array();
 239        }
 240  
 241        if ($this->shouldDisablePolicyFiltering()) {
 242          $visible = $maybe_visible;
 243        } else {
 244          $visible = $filter->apply($maybe_visible);
 245  
 246          $policy_filtered = array();
 247          foreach ($maybe_visible as $key => $object) {
 248            if (empty($visible[$key])) {
 249              $phid = $object->getPHID();
 250              if ($phid) {
 251                $policy_filtered[$phid] = $phid;
 252              }
 253            }
 254          }
 255          $this->addPolicyFilteredPHIDs($policy_filtered);
 256        }
 257  
 258        if ($visible) {
 259          $this->putObjectsInWorkspace($this->getWorkspaceMapForPage($visible));
 260          $visible = $this->didFilterPage($visible);
 261        }
 262  
 263        $removed = array();
 264        foreach ($maybe_visible as $key => $object) {
 265          if (empty($visible[$key])) {
 266            $removed[$key] = $object;
 267          }
 268        }
 269  
 270        $this->didFilterResults($removed);
 271  
 272        foreach ($visible as $key => $result) {
 273          ++$count;
 274  
 275          // If we have an offset, we just ignore that many results and start
 276          // storing them only once we've hit the offset. This reduces memory
 277          // requirements for large offsets, compared to storing them all and
 278          // slicing them away later.
 279          if ($count > $offset) {
 280            $results[$key] = $result;
 281          }
 282  
 283          if ($need && ($count >= $need)) {
 284            // If we have all the rows we need, break out of the paging query.
 285            break 2;
 286          }
 287        }
 288  
 289        if (!$this->rawResultLimit) {
 290          // If we don't have a load count, we loaded all the results. We do
 291          // not need to load another page.
 292          break;
 293        }
 294  
 295        if (count($page) < $this->rawResultLimit) {
 296          // If we have a load count but the unfiltered results contained fewer
 297          // objects, we know this was the last page of objects; we do not need
 298          // to load another page because we can deduce it would be empty.
 299          break;
 300        }
 301  
 302        $this->nextPage($page);
 303      } while (true);
 304  
 305      $results = $this->didLoadResults($results);
 306  
 307      return $results;
 308    }
 309  
 310    private function getPolicyFilter() {
 311      $filter = new PhabricatorPolicyFilter();
 312      $filter->setViewer($this->viewer);
 313      $capabilities = $this->getRequiredCapabilities();
 314      $filter->requireCapabilities($capabilities);
 315      $filter->raisePolicyExceptions($this->shouldRaisePolicyExceptions());
 316  
 317      return $filter;
 318    }
 319  
 320    protected function getRequiredCapabilities() {
 321      if ($this->capabilities) {
 322        return $this->capabilities;
 323      }
 324  
 325      return array(
 326        PhabricatorPolicyCapability::CAN_VIEW,
 327      );
 328    }
 329  
 330    protected function applyPolicyFilter(array $objects, array $capabilities) {
 331      if ($this->shouldDisablePolicyFiltering()) {
 332        return $objects;
 333      }
 334      $filter = $this->getPolicyFilter();
 335      $filter->requireCapabilities($capabilities);
 336      return $filter->apply($objects);
 337    }
 338  
 339    protected function didRejectResult(PhabricatorPolicyInterface $object) {
 340      $this->getPolicyFilter()->rejectObject(
 341        $object,
 342        $object->getPolicy(PhabricatorPolicyCapability::CAN_VIEW),
 343        PhabricatorPolicyCapability::CAN_VIEW);
 344    }
 345  
 346    public function addPolicyFilteredPHIDs(array $phids) {
 347      $this->policyFilteredPHIDs += $phids;
 348      if ($this->getParentQuery()) {
 349        $this->getParentQuery()->addPolicyFilteredPHIDs($phids);
 350      }
 351      return $this;
 352    }
 353  
 354    /**
 355     * Return a map of all object PHIDs which were loaded in the query but
 356     * filtered out by policy constraints. This allows a caller to distinguish
 357     * between objects which do not exist (or, at least, were filtered at the
 358     * content level) and objects which exist but aren't visible.
 359     *
 360     * @return map<phid, phid> Map of object PHIDs which were filtered
 361     *   by policies.
 362     * @task exec
 363     */
 364    public function getPolicyFilteredPHIDs() {
 365      return $this->policyFilteredPHIDs;
 366    }
 367  
 368  
 369  /* -(  Query Workspace  )---------------------------------------------------- */
 370  
 371  
 372    /**
 373     * Put a map of objects into the query workspace. Many queries perform
 374     * subqueries, which can eventually end up loading the same objects more than
 375     * once (often to perform policy checks).
 376     *
 377     * For example, loading a user may load the user's profile image, which might
 378     * load the user object again in order to verify that the viewer has
 379     * permission to see the file.
 380     *
 381     * The "query workspace" allows queries to load objects from elsewhere in a
 382     * query block instead of refetching them.
 383     *
 384     * When using the query workspace, it's important to obey two rules:
 385     *
 386     * **Never put objects into the workspace which the viewer may not be able
 387     * to see**. You need to apply all policy filtering //before// putting
 388     * objects in the workspace. Otherwise, subqueries may read the objects and
 389     * use them to permit access to content the user shouldn't be able to view.
 390     *
 391     * **Fully enrich objects pulled from the workspace.** After pulling objects
 392     * from the workspace, you still need to load and attach any additional
 393     * content the query requests. Otherwise, a query might return objects without
 394     * requested content.
 395     *
 396     * Generally, you do not need to update the workspace yourself: it is
 397     * automatically populated as a side effect of objects surviving policy
 398     * filtering.
 399     *
 400     * @param map<phid, PhabricatorPolicyInterface> Objects to add to the query
 401     *   workspace.
 402     * @return this
 403     * @task workspace
 404     */
 405    public function putObjectsInWorkspace(array $objects) {
 406      assert_instances_of($objects, 'PhabricatorPolicyInterface');
 407  
 408      $viewer_phid = $this->getViewer()->getPHID();
 409  
 410      // The workspace is scoped per viewer to prevent accidental contamination.
 411      if (empty($this->workspace[$viewer_phid])) {
 412        $this->workspace[$viewer_phid] = array();
 413      }
 414  
 415      $this->workspace[$viewer_phid] += $objects;
 416  
 417      return $this;
 418    }
 419  
 420  
 421    /**
 422     * Retrieve objects from the query workspace. For more discussion about the
 423     * workspace mechanism, see @{method:putObjectsInWorkspace}. This method
 424     * searches both the current query's workspace and the workspaces of parent
 425     * queries.
 426     *
 427     * @param list<phid> List of PHIDs to retrieve.
 428     * @return this
 429     * @task workspace
 430     */
 431    public function getObjectsFromWorkspace(array $phids) {
 432      $viewer_phid = $this->getViewer()->getPHID();
 433  
 434      $results = array();
 435      foreach ($phids as $key => $phid) {
 436        if (isset($this->workspace[$viewer_phid][$phid])) {
 437          $results[$phid] = $this->workspace[$viewer_phid][$phid];
 438          unset($phids[$key]);
 439        }
 440      }
 441  
 442      if ($phids && $this->getParentQuery()) {
 443        $results += $this->getParentQuery()->getObjectsFromWorkspace($phids);
 444      }
 445  
 446      return $results;
 447    }
 448  
 449  
 450    /**
 451     * Convert a result page to a `<phid, PhabricatorPolicyInterface>` map.
 452     *
 453     * @param list<PhabricatorPolicyInterface> Objects.
 454     * @return map<phid, PhabricatorPolicyInterface> Map of objects which can
 455     *   be put into the workspace.
 456     * @task workspace
 457     */
 458    protected function getWorkspaceMapForPage(array $results) {
 459      $map = array();
 460      foreach ($results as $result) {
 461        $phid = $result->getPHID();
 462        if ($phid !== null) {
 463          $map[$phid] = $result;
 464        }
 465      }
 466  
 467      return $map;
 468    }
 469  
 470  
 471  /* -(  Policy Query Implementation  )---------------------------------------- */
 472  
 473  
 474    /**
 475     * Get the number of results @{method:loadPage} should load. If the value is
 476     * 0, @{method:loadPage} should load all available results.
 477     *
 478     * @return int The number of results to load, or 0 for all results.
 479     * @task policyimpl
 480     */
 481    final protected function getRawResultLimit() {
 482      return $this->rawResultLimit;
 483    }
 484  
 485  
 486    /**
 487     * Hook invoked before query execution. Generally, implementations should
 488     * reset any internal cursors.
 489     *
 490     * @return void
 491     * @task policyimpl
 492     */
 493    protected function willExecute() {
 494      return;
 495    }
 496  
 497  
 498    /**
 499     * Load a raw page of results. Generally, implementations should load objects
 500     * from the database. They should attempt to return the number of results
 501     * hinted by @{method:getRawResultLimit}.
 502     *
 503     * @return list<PhabricatorPolicyInterface> List of filterable policy objects.
 504     * @task policyimpl
 505     */
 506    abstract protected function loadPage();
 507  
 508  
 509    /**
 510     * Update internal state so that the next call to @{method:loadPage} will
 511     * return new results. Generally, you should adjust a cursor position based
 512     * on the provided result page.
 513     *
 514     * @param list<PhabricatorPolicyInterface> The current page of results.
 515     * @return void
 516     * @task policyimpl
 517     */
 518    abstract protected function nextPage(array $page);
 519  
 520  
 521    /**
 522     * Hook for applying a page filter prior to the privacy filter. This allows
 523     * you to drop some items from the result set without creating problems with
 524     * pagination or cursor updates. You can also load and attach data which is
 525     * required to perform policy filtering.
 526     *
 527     * Generally, you should load non-policy data and perform non-policy filtering
 528     * later, in @{method:didFilterPage}. Strictly fewer objects will make it that
 529     * far (so the program will load less data) and subqueries from that context
 530     * can use the query workspace to further reduce query load.
 531     *
 532     * This method will only be called if data is available. Implementations
 533     * do not need to handle the case of no results specially.
 534     *
 535     * @param   list<wild>  Results from `loadPage()`.
 536     * @return  list<PhabricatorPolicyInterface> Objects for policy filtering.
 537     * @task policyimpl
 538     */
 539    protected function willFilterPage(array $page) {
 540      return $page;
 541    }
 542  
 543    /**
 544     * Hook for performing additional non-policy loading or filtering after an
 545     * object has satisfied all policy checks. Generally, this means loading and
 546     * attaching related data.
 547     *
 548     * Subqueries executed during this phase can use the query workspace, which
 549     * may improve performance or make circular policies resolvable. Data which
 550     * is not necessary for policy filtering should generally be loaded here.
 551     *
 552     * This callback can still filter objects (for example, if attachable data
 553     * is discovered to not exist), but should not do so for policy reasons.
 554     *
 555     * This method will only be called if data is available. Implementations do
 556     * not need to handle the case of no results specially.
 557     *
 558     * @param list<wild> Results from @{method:willFilterPage()}.
 559     * @return list<PhabricatorPolicyInterface> Objects after additional
 560     *   non-policy processing.
 561     */
 562    protected function didFilterPage(array $page) {
 563      return $page;
 564    }
 565  
 566  
 567    /**
 568     * Hook for removing filtered results from alternate result sets. This
 569     * hook will be called with any objects which were returned by the query but
 570     * filtered for policy reasons. The query should remove them from any cached
 571     * or partial result sets.
 572     *
 573     * @param list<wild>  List of objects that should not be returned by alternate
 574     *                    result mechanisms.
 575     * @return void
 576     * @task policyimpl
 577     */
 578    protected function didFilterResults(array $results) {
 579      return;
 580    }
 581  
 582  
 583    /**
 584     * Hook for applying final adjustments before results are returned. This is
 585     * used by @{class:PhabricatorCursorPagedPolicyAwareQuery} to reverse results
 586     * that are queried during reverse paging.
 587     *
 588     * @param   list<PhabricatorPolicyInterface> Query results.
 589     * @return  list<PhabricatorPolicyInterface> Final results.
 590     * @task policyimpl
 591     */
 592    protected function didLoadResults(array $results) {
 593      return $results;
 594    }
 595  
 596  
 597    /**
 598     * Allows a subclass to disable policy filtering. This method is dangerous.
 599     * It should be used only if the query loads data which has already been
 600     * filtered (for example, because it wraps some other query which uses
 601     * normal policy filtering).
 602     *
 603     * @return bool True to disable all policy filtering.
 604     * @task policyimpl
 605     */
 606    protected function shouldDisablePolicyFiltering() {
 607      return false;
 608    }
 609  
 610  
 611    /**
 612     * If this query belongs to an application, return the application class name
 613     * here. This will prevent the query from returning results if the viewer can
 614     * not access the application.
 615     *
 616     * If this query does not belong to an application, return `null`.
 617     *
 618     * @return string|null Application class name.
 619     */
 620    abstract public function getQueryApplicationClass();
 621  
 622  
 623    /**
 624     * Determine if the viewer has permission to use this query's application.
 625     * For queries which aren't part of an application, this method always returns
 626     * true.
 627     *
 628     * @return bool True if the viewer has application-level permission to
 629     *   execute the query.
 630     */
 631    public function canViewerUseQueryApplication() {
 632      if ($this->canUseApplication === null) {
 633        $class = $this->getQueryApplicationClass();
 634        if (!$class) {
 635          $this->canUseApplication = true;
 636        } else {
 637          $result = id(new PhabricatorApplicationQuery())
 638            ->setViewer($this->getViewer())
 639            ->withClasses(array($class))
 640            ->execute();
 641  
 642          $this->canUseApplication = (bool)$result;
 643        }
 644      }
 645  
 646      return $this->canUseApplication;
 647    }
 648  
 649  }


Generated: Sun Nov 30 09:20:46 2014 Cross-referenced by PHPXref 0.7.1