[ Index ] |
PHP Cross Reference of Phabricator |
[Summary view] [Print] [Text view]
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Sun Nov 30 09:20:46 2014 | Cross-referenced by PHPXref 0.7.1 |