Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/applications/policy/__tests__/PhabricatorPolicyTestCase.php b/src/applications/policy/__tests__/PhabricatorPolicyTestCase.php
index 93fc56ed2d..24fb6a08b5 100644
--- a/src/applications/policy/__tests__/PhabricatorPolicyTestCase.php
+++ b/src/applications/policy/__tests__/PhabricatorPolicyTestCase.php
@@ -1,183 +1,217 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
final class PhabricatorPolicyTestCase extends PhabricatorTestCase {
/**
* Verify that any user can view an object with POLICY_PUBLIC.
*/
public function testPublicPolicyEnabled() {
$env = PhabricatorEnv::beginScopedEnv();
$env->overrideEnvConfig('policy.allow-public', true);
$this->expectVisibility(
$this->buildObject(PhabricatorPolicies::POLICY_PUBLIC),
array(
'public' => true,
'user' => true,
'admin' => true,
),
'Public Policy (Enabled in Config)');
}
/**
* Verify that POLICY_PUBLIC is interpreted as POLICY_USER when public
* policies are disallowed.
*/
public function testPublicPolicyDisabled() {
$env = PhabricatorEnv::beginScopedEnv();
$env->overrideEnvConfig('policy.allow-public', false);
$this->expectVisibility(
$this->buildObject(PhabricatorPolicies::POLICY_PUBLIC),
array(
'public' => false,
'user' => true,
'admin' => true,
),
'Public Policy (Disabled in Config)');
}
/**
* Verify that any logged-in user can view an object with POLICY_USER, but
* logged-out users can not.
*/
public function testUsersPolicy() {
$this->expectVisibility(
$this->buildObject(PhabricatorPolicies::POLICY_USER),
array(
'public' => false,
'user' => true,
'admin' => true,
),
'User Policy');
}
/**
* Verify that only administrators can view an object with POLICY_ADMIN.
*/
public function testAdminPolicy() {
$this->expectVisibility(
$this->buildObject(PhabricatorPolicies::POLICY_ADMIN),
array(
'public' => false,
'user' => false,
'admin' => true,
),
'Admin Policy');
}
/**
* Verify that no one can view an object with POLICY_NOONE.
*/
public function testNoOnePolicy() {
$this->expectVisibility(
$this->buildObject(PhabricatorPolicies::POLICY_NOONE),
array(
'public' => false,
'user' => false,
'admin' => false,
),
'No One Policy');
}
+ /**
+ * Test offset-based filtering.
+ */
+ public function testOffsets() {
+ $results = array(
+ $this->buildObject(PhabricatorPolicies::POLICY_NOONE),
+ $this->buildObject(PhabricatorPolicies::POLICY_NOONE),
+ $this->buildObject(PhabricatorPolicies::POLICY_NOONE),
+ $this->buildObject(PhabricatorPolicies::POLICY_USER),
+ $this->buildObject(PhabricatorPolicies::POLICY_USER),
+ $this->buildObject(PhabricatorPolicies::POLICY_USER),
+ );
+
+ $query = new PhabricatorPolicyTestQuery();
+ $query->setResults($results);
+ $query->setViewer($this->buildUser('user'));
+
+ $this->assertEqual(
+ 3,
+ count($query->setLimit(3)->setOffset(0)->execute()),
+ 'Invisible objects are ignored.');
+
+ $this->assertEqual(
+ 0,
+ count($query->setLimit(3)->setOffset(3)->execute()),
+ 'Offset pages through visible objects only.');
+
+ $this->assertEqual(
+ 2,
+ count($query->setLimit(3)->setOffset(1)->execute()),
+ 'Offsets work correctly.');
+ }
+
+
/**
* Test an object for visibility across multiple user specifications.
*/
private function expectVisibility(
PhabricatorPolicyTestObject $object,
array $map,
$description) {
foreach ($map as $spec => $expect) {
$viewer = $this->buildUser($spec);
$query = new PhabricatorPolicyTestQuery();
$query->setResults(array($object));
$query->setViewer($viewer);
$caught = null;
try {
$result = $query->executeOne();
} catch (PhabricatorPolicyException $ex) {
$caught = $ex;
}
if ($expect) {
$this->assertEqual(
$object,
$result,
"{$description} with user {$spec} should succeed.");
} else {
$this->assertEqual(
true,
$caught instanceof PhabricatorPolicyException,
"{$description} with user {$spec} should fail.");
}
}
}
/**
* Build a test object to spec.
*/
private function buildObject($policy) {
$object = new PhabricatorPolicyTestObject();
$object->setCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
));
$object->setPolicies(
array(
PhabricatorPolicyCapability::CAN_VIEW => $policy,
));
return $object;
}
/**
* Build a test user to spec.
*/
private function buildUser($spec) {
$user = new PhabricatorUser();
switch ($spec) {
case 'public':
break;
case 'user':
$user->setPHID(1);
break;
case 'admin':
$user->setPHID(1);
$user->setIsAdmin(true);
break;
default:
throw new Exception("Unknown user spec '{$spec}'.");
}
return $user;
}
}
diff --git a/src/applications/policy/__tests__/PhabricatorPolicyTestQuery.php b/src/applications/policy/__tests__/PhabricatorPolicyTestQuery.php
index e1389782c2..5129e5bacc 100644
--- a/src/applications/policy/__tests__/PhabricatorPolicyTestQuery.php
+++ b/src/applications/policy/__tests__/PhabricatorPolicyTestQuery.php
@@ -1,40 +1,52 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Configurable test query for implementing Policy unit tests.
*/
final class PhabricatorPolicyTestQuery
extends PhabricatorPolicyQuery {
private $results;
+ private $offset = 0;
public function setResults(array $results) {
$this->results = $results;
return $this;
}
+ protected function willExecute() {
+ $this->offset = 0;
+ }
+
public function loadPage() {
- return $this->results;
+ if ($this->getRawResultLimit()) {
+ return array_slice(
+ $this->results,
+ $this->offset,
+ $this->getRawResultLimit());
+ } else {
+ return array_slice($this->results, $this->offset);
+ }
}
public function nextPage(array $page) {
- return null;
+ $this->offset += count($page);
}
}
diff --git a/src/infrastructure/query/PhabricatorOffsetPagedQuery.php b/src/infrastructure/query/PhabricatorOffsetPagedQuery.php
index 9684b3ec1b..371fcda559 100644
--- a/src/infrastructure/query/PhabricatorOffsetPagedQuery.php
+++ b/src/infrastructure/query/PhabricatorOffsetPagedQuery.php
@@ -1,59 +1,67 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* A query class which uses offset/limit paging. Provides logic and accessors
* for offsets and limits.
*/
abstract class PhabricatorOffsetPagedQuery extends PhabricatorQuery {
private $offset;
private $limit;
final public function setOffset($offset) {
$this->offset = $offset;
return $this;
}
final public function setLimit($limit) {
$this->limit = $limit;
return $this;
}
- final protected function buildLimitClause(AphrontDatabaseConnection $conn_r) {
+ final public function getOffset() {
+ return $this->offset;
+ }
+
+ final public function getLimit() {
+ return $this->limit;
+ }
+
+ protected function buildLimitClause(AphrontDatabaseConnection $conn_r) {
if ($this->limit && $this->offset) {
return qsprintf($conn_r, 'LIMIT %d, %d', $this->offset, $this->limit);
} else if ($this->limit) {
return qsprintf($conn_r, 'LIMIT %d', $this->limit);
} else if ($this->offset) {
return qsprintf($conn_r, 'LIMIT %d, %d', $this->offset, PHP_INT_MAX);
} else {
return '';
}
}
final public function executeWithOffsetPager(AphrontPagerView $pager) {
$this->setLimit($pager->getPageSize() + 1);
$this->setOffset($pager->getOffset());
$results = $this->execute();
return $pager->sliceResults($results);
}
}
diff --git a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyQuery.php b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyQuery.php
index d889de8901..8dda36f17d 100644
--- a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyQuery.php
+++ b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyQuery.php
@@ -1,130 +1,130 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* A query class which uses cursor-based paging. This paging is much more
* performant than offset-based paging in the presence of policy filtering.
*/
abstract class PhabricatorCursorPagedPolicyQuery
extends PhabricatorPolicyQuery {
private $afterID;
private $beforeID;
protected function getPagingColumn() {
return 'id';
}
protected function getPagingValue($result) {
return $result->getID();
}
protected function nextPage(array $page) {
if ($this->beforeID) {
$this->beforeID = $this->getPagingValue(head($page));
} else {
$this->afterID = $this->getPagingValue(last($page));
}
}
final public function setAfterID($object_id) {
$this->afterID = $object_id;
return $this;
}
final public function setBeforeID($object_id) {
$this->beforeID = $object_id;
return $this;
}
final protected function buildLimitClause(AphrontDatabaseConnection $conn_r) {
- if ($this->getLimit()) {
- return qsprintf($conn_r, 'LIMIT %d', $this->getLimit());
+ if ($this->getRawResultLimit()) {
+ return qsprintf($conn_r, 'LIMIT %d', $this->getRawResultLimit());
} else {
return '';
}
}
final protected function buildPagingClause(
AphrontDatabaseConnection $conn_r) {
if ($this->beforeID) {
return qsprintf(
$conn_r,
'%Q > %s',
$this->getPagingColumn(),
$this->beforeID);
} else if ($this->afterID) {
return qsprintf(
$conn_r,
'%Q < %s',
$this->getPagingColumn(),
$this->afterID);
}
return null;
}
final protected function buildOrderClause(AphrontDatabaseConnection $conn_r) {
if ($this->beforeID) {
return qsprintf(
$conn_r,
'ORDER BY %Q ASC',
$this->getPagingColumn());
} else {
return qsprintf(
$conn_r,
'ORDER BY %Q DESC',
$this->getPagingColumn());
}
}
final protected function processResults(array $results) {
if ($this->beforeID) {
$results = array_reverse($results, $preserve_keys = true);
}
return $results;
}
final public function executeWithCursorPager(AphrontCursorPagerView $pager) {
$this->setLimit($pager->getPageSize() + 1);
if ($pager->getAfterID()) {
$this->setAfterID($pager->getAfterID());
} else if ($pager->getBeforeID()) {
$this->setBeforeID($pager->getBeforeID());
}
$results = $this->execute();
$sliced_results = $pager->sliceResults($results);
if ($this->beforeID || (count($results) > $pager->getPageSize())) {
$pager->setNextPageID($this->getPagingValue(last($sliced_results)));
}
if ($this->afterID ||
($this->beforeID && (count($results) > $pager->getPageSize()))) {
$pager->setPrevPageID($this->getPagingValue(head($sliced_results)));
}
return $sliced_results;
}
}
diff --git a/src/infrastructure/query/policy/PhabricatorPolicyQuery.php b/src/infrastructure/query/policy/PhabricatorPolicyQuery.php
index 7d9a62dc88..ffdd362789 100644
--- a/src/infrastructure/query/policy/PhabricatorPolicyQuery.php
+++ b/src/infrastructure/query/policy/PhabricatorPolicyQuery.php
@@ -1,96 +1,274 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-abstract class PhabricatorPolicyQuery extends PhabricatorQuery {
+/**
+ * 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:PhabricatorCursorPagedPolicyQuery}, not
+ * this class. @{class:PhabricatorCursorPagedPolicyQuery} 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:PhabricatorCursorPagedPolicyQuery}, which can filter far more
+ * efficiently in MySQL.
+ *
+ * @task config Query Configuration
+ * @task exec Executing Queries
+ * @task policyimpl Policy Query Implementation
+ */
+abstract class PhabricatorPolicyQuery extends PhabricatorOffsetPagedQuery {
- private $limit;
private $viewer;
private $raisePolicyExceptions;
+ private $rawResultLimit;
+
- final public function setViewer($viewer) {
+/* -( 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;
}
- final public function setLimit($limit) {
- $this->limit = $limit;
- return $this;
- }
- final public function getLimit() {
- return $this->limit;
- }
+/* -( 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->raisePolicyExceptions = true;
try {
$results = $this->execute();
} catch (Exception $ex) {
$this->raisePolicyExceptions = false;
throw $ex;
}
if (count($results) > 1) {
throw new Exception("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 Exception("Call setViewer() before execute()!");
}
$results = array();
$filter = new PhabricatorPolicyFilter();
$filter->setViewer($this->viewer);
$filter->setCapability(PhabricatorPolicyCapability::CAN_VIEW);
$filter->raisePolicyExceptions($this->raisePolicyExceptions);
+ $offset = (int)$this->getOffset();
+ $limit = (int)$this->getLimit();
+ $count = 0;
+
+ $need = null;
+ if ($offset) {
+ $need = $offset + $limit;
+ }
+
+ $this->willExecute();
+
do {
+
+ // Figure out how many results to load. "0" means "all results".
+ $load = 0;
+ if ($need && ($count < $offset)) {
+ // This cap is just an arbitrary limit to keep memory usage from going
+ // crazy for large offsets; we can't execute them efficiently, but
+ // it should be possible to execute them without crashing.
+ $load = min($need, 1024);
+ } else if ($limit) {
+ // Otherwise, just load the number of rows we're after. Note that it
+ // might be more efficient to load more rows than this (if we expect
+ // about 5% of objects to be filtered, loading 105% of the limit might
+ // be better) or fewer rows than this (if we already have 95 rows and
+ // only need 100, loading only 5 rows might be better), but we currently
+ // just use the simplest heuristic since we don't have enough data
+ // about policy queries in the real world to tweak it.
+ $load = $limit;
+ }
+ $this->rawResultLimit = $load;
+
+
$page = $this->loadPage();
$visible = $filter->apply($page);
foreach ($visible as $key => $result) {
- $results[$key] = $result;
- if ($this->getLimit() && count($results) >= $this->getLimit()) {
+ ++$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->getLimit() || (count($page) < $this->getLimit())) {
+ if (!$load) {
+ // 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) < $load) {
+ // 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);
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);
}

File Metadata

Mime Type
text/x-diff
Expires
Sat, Nov 15, 9:55 AM (20 h, 55 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
337768
Default Alt Text
(22 KB)

Event Timeline