diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
index 8934e7f9a1..dfbaedd6bb 100644
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -39,6 +39,7 @@ phutil_register_library_map(array(
'AphrontFormLayoutView' => 'view/form/layout',
'AphrontFormMarkupControl' => 'view/form/control/markup',
'AphrontFormPasswordControl' => 'view/form/control/password',
+ 'AphrontFormPolicyControl' => 'view/form/control/policy',
'AphrontFormRadioButtonControl' => 'view/form/control/radio',
'AphrontFormRecaptchaControl' => 'view/form/control/recaptcha',
'AphrontFormSelectControl' => 'view/form/control/select',
@@ -54,6 +55,7 @@ phutil_register_library_map(array(
'AphrontHeadsupActionListView' => 'view/layout/headsup/actionlist',
'AphrontHeadsupActionView' => 'view/layout/headsup/action',
'AphrontHeadsupView' => 'view/layout/headsup/panel',
+ 'AphrontIDPagerView' => 'view/control/idpager',
'AphrontIsolatedDatabaseConnection' => 'storage/connection/isolated',
'AphrontIsolatedDatabaseConnectionTestCase' => 'storage/connection/isolated/__tests__',
'AphrontIsolatedHTTPSink' => 'aphront/sink/test',
@@ -633,6 +635,7 @@ phutil_register_library_map(array(
'PhabricatorHash' => 'infrastructure/util/hash',
'PhabricatorHelpController' => 'applications/help/controller/base',
'PhabricatorHelpKeyboardShortcutController' => 'applications/help/controller/keyboardshortcut',
+ 'PhabricatorIDPagedPolicyQuery' => 'infrastructure/query/policy/idpaged',
'PhabricatorIRCBot' => 'infrastructure/daemon/irc/bot',
'PhabricatorIRCDifferentialNotificationHandler' => 'infrastructure/daemon/irc/handler/differentialnotification',
'PhabricatorIRCHandler' => 'infrastructure/daemon/irc/handler/base',
@@ -745,12 +748,23 @@ phutil_register_library_map(array(
'PhabricatorPasteController' => 'applications/paste/controller/base',
'PhabricatorPasteDAO' => 'applications/paste/storage/base',
'PhabricatorPasteListController' => 'applications/paste/controller/list',
+ 'PhabricatorPasteQuery' => 'applications/paste/query/paste',
'PhabricatorPasteViewController' => 'applications/paste/controller/view',
'PhabricatorPeopleController' => 'applications/people/controller/base',
'PhabricatorPeopleEditController' => 'applications/people/controller/edit',
'PhabricatorPeopleListController' => 'applications/people/controller/list',
'PhabricatorPeopleLogsController' => 'applications/people/controller/logs',
'PhabricatorPeopleProfileController' => 'applications/people/controller/profile',
+ 'PhabricatorPolicies' => 'applications/policy/constants/policy',
+ 'PhabricatorPolicyCapability' => 'applications/policy/constants/capability',
+ 'PhabricatorPolicyConstants' => 'applications/policy/constants/base',
+ 'PhabricatorPolicyException' => 'applications/policy/exception/base',
+ 'PhabricatorPolicyFilter' => 'applications/policy/filter/policy',
+ 'PhabricatorPolicyInterface' => 'applications/policy/interface/policy',
+ 'PhabricatorPolicyQuery' => 'infrastructure/query/policy/base',
+ 'PhabricatorPolicyTestCase' => 'applications/policy/__tests__',
+ 'PhabricatorPolicyTestObject' => 'applications/policy/__tests__',
+ 'PhabricatorPolicyTestQuery' => 'applications/policy/__tests__',
'PhabricatorProfileHeaderView' => 'view/layout/profileheader',
'PhabricatorProject' => 'applications/project/storage/project',
'PhabricatorProjectAffiliation' => 'applications/project/storage/affiliation',
@@ -1023,6 +1037,7 @@ phutil_register_library_map(array(
'AphrontFormLayoutView' => 'AphrontView',
'AphrontFormMarkupControl' => 'AphrontFormControl',
'AphrontFormPasswordControl' => 'AphrontFormControl',
+ 'AphrontFormPolicyControl' => 'AphrontFormControl',
'AphrontFormRadioButtonControl' => 'AphrontFormControl',
'AphrontFormRecaptchaControl' => 'AphrontFormControl',
'AphrontFormSelectControl' => 'AphrontFormControl',
@@ -1037,6 +1052,7 @@ phutil_register_library_map(array(
'AphrontHeadsupActionListView' => 'AphrontView',
'AphrontHeadsupActionView' => 'AphrontView',
'AphrontHeadsupView' => 'AphrontView',
+ 'AphrontIDPagerView' => 'AphrontView',
'AphrontIsolatedDatabaseConnection' => 'AphrontDatabaseConnection',
'AphrontIsolatedDatabaseConnectionTestCase' => 'PhabricatorTestCase',
'AphrontIsolatedHTTPSink' => 'AphrontHTTPSink',
@@ -1502,6 +1518,7 @@ phutil_register_library_map(array(
'PhabricatorGoodForNothingWorker' => 'PhabricatorWorker',
'PhabricatorHelpController' => 'PhabricatorController',
'PhabricatorHelpKeyboardShortcutController' => 'PhabricatorHelpController',
+ 'PhabricatorIDPagedPolicyQuery' => 'PhabricatorPolicyQuery',
'PhabricatorIRCBot' => 'PhabricatorDaemon',
'PhabricatorIRCDifferentialNotificationHandler' => 'PhabricatorIRCHandler',
'PhabricatorIRCLogHandler' => 'PhabricatorIRCHandler',
@@ -1594,12 +1611,18 @@ phutil_register_library_map(array(
'PhabricatorPasteController' => 'PhabricatorController',
'PhabricatorPasteDAO' => 'PhabricatorLiskDAO',
'PhabricatorPasteListController' => 'PhabricatorPasteController',
+ 'PhabricatorPasteQuery' => 'PhabricatorIDPagedPolicyQuery',
'PhabricatorPasteViewController' => 'PhabricatorPasteController',
'PhabricatorPeopleController' => 'PhabricatorController',
'PhabricatorPeopleEditController' => 'PhabricatorPeopleController',
'PhabricatorPeopleListController' => 'PhabricatorPeopleController',
'PhabricatorPeopleLogsController' => 'PhabricatorPeopleController',
'PhabricatorPeopleProfileController' => 'PhabricatorPeopleController',
+ 'PhabricatorPolicies' => 'PhabricatorPolicyConstants',
+ 'PhabricatorPolicyCapability' => 'PhabricatorPolicyConstants',
+ 'PhabricatorPolicyQuery' => 'PhabricatorQuery',
+ 'PhabricatorPolicyTestCase' => 'PhabricatorTestCase',
+ 'PhabricatorPolicyTestQuery' => 'PhabricatorPolicyQuery',
'PhabricatorProfileHeaderView' => 'AphrontView',
'PhabricatorProject' => 'PhabricatorProjectDAO',
'PhabricatorProjectAffiliation' => 'PhabricatorProjectDAO',
@@ -1804,5 +1827,13 @@ phutil_register_library_map(array(
array(
0 => 'PhabricatorInlineCommentInterface',
),
+ 'PhabricatorPaste' =>
+ array(
+ 0 => 'PhabricatorPolicyInterface',
+ ),
+ 'PhabricatorPolicyTestObject' =>
+ array(
+ 0 => 'PhabricatorPolicyInterface',
+ ),
),
));
diff --git a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php
index e74a089f8a..8940263ccd 100644
--- a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php
+++ b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php
@@ -468,11 +468,7 @@ class AphrontDefaultApplicationConfiguration
public function handleException(Exception $ex) {
- // Always log the unhandled exception.
- phlog($ex);
-
- $class = phutil_escape_html(get_class($ex));
- $message = phutil_escape_html($ex->getMessage());
+ $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
$user = $this->getRequest()->getUser();
if (!$user) {
@@ -480,6 +476,39 @@ class AphrontDefaultApplicationConfiguration
$user = new PhabricatorUser();
}
+ if ($ex instanceof PhabricatorPolicyException) {
+ $content =
+ '
'.
+ phutil_escape_html($ex->getMessage()).
+ '
';
+
+ $dialog = new AphrontDialogView();
+ $dialog
+ ->setTitle(
+ $is_serious
+ ? 'Access Denied'
+ : "You Shall Not Pass")
+ ->setClass('aphront-access-dialog')
+ ->setUser($user)
+ ->appendChild($content);
+
+ if ($this->getRequest()->isAjax()) {
+ $dialog->addCancelButton('/', 'Close');
+ } else {
+ $dialog->addCancelButton('/', $is_serious ? 'OK' : 'Away With Thee');
+ }
+
+ $response = new AphrontDialogResponse();
+ $response->setDialog($dialog);
+ return $response;
+ }
+
+ // Always log the unhandled exception.
+ phlog($ex);
+
+ $class = phutil_escape_html(get_class($ex));
+ $message = phutil_escape_html($ex->getMessage());
+
if (PhabricatorEnv::getEnvConfig('phabricator.show-stack-traces')) {
$trace = $this->renderStackTrace($ex->getTrace(), $user);
} else {
diff --git a/src/applications/paste/controller/list/PhabricatorPasteListController.php b/src/applications/paste/controller/list/PhabricatorPasteListController.php
index 5ef111e61b..f4a2ff1ea5 100644
--- a/src/applications/paste/controller/list/PhabricatorPasteListController.php
+++ b/src/applications/paste/controller/list/PhabricatorPasteListController.php
@@ -24,10 +24,6 @@ final class PhabricatorPasteListController extends PhabricatorPasteController {
private $paste;
private $pasteText;
- private $offset;
- private $pageSize;
- private $author;
-
private function setFilter($filter) {
$this->filter = $filter;
return $this;
@@ -40,6 +36,7 @@ final class PhabricatorPasteListController extends PhabricatorPasteController {
$this->errorView = $error_view;
return $this;
}
+
private function getErrorView() {
return $this->errorView;
}
@@ -68,40 +65,19 @@ final class PhabricatorPasteListController extends PhabricatorPasteController {
return $this->pasteText;
}
- private function setOffset($offset) {
- $this->offset = $offset;
- return $this;
- }
- private function getOffset() {
- return $this->offset;
- }
-
- private function setPageSize($page_size) {
- $this->pageSize = $page_size;
- return $this;
- }
- private function getPageSize() {
- return $this->pageSize;
- }
-
- private function setAuthor($author) {
- $this->author = $author;
- return $this;
- }
- private function getAuthor() {
- return $this->author;
- }
-
public function willProcessRequest(array $data) {
$this->setFilter(idx($data, 'filter', 'create'));
}
public function processRequest() {
-
$request = $this->getRequest();
$user = $request->getUser();
- $paste_list = array();
- $pager = null;
+
+ $pager = new AphrontIDPagerView();
+ $pager->readFromRequest($request);
+
+ $query = new PhabricatorPasteQuery();
+ $query->setViewer($user);
switch ($this->getFilter()) {
case 'create':
@@ -111,22 +87,18 @@ final class PhabricatorPasteListController extends PhabricatorPasteController {
if ($created_paste_redirect) {
return $created_paste_redirect;
}
- // if we didn't succeed or we weren't trying, load just a few
- // recent pastes with NO pagination
- $this->setOffset(0);
- $this->setPageSize(10);
- list($paste_list, $pager) = $this->loadPasteList();
- break;
+ $query->setLimit(10);
+ $paste_list = $query->execute();
+
+ $pager = null;
+ break;
case 'my':
- $this->setAuthor($user->getPHID());
- $this->setOffset($request->getInt('page', 0));
- list($paste_list, $pager) = $this->loadPasteList();
+ $query->withAuthorPHIDs(array($user->getPHID()));
+ $paste_list = $query->executeWithPager($pager);
break;
-
case 'all':
- $this->setOffset($request->getInt('page', 0));
- list($paste_list, $pager) = $this->loadPasteList();
+ $paste_list = $query->executeWithPager($pager);
break;
}
@@ -171,24 +143,19 @@ final class PhabricatorPasteListController extends PhabricatorPasteController {
),
'See all Pastes');
$header = "Recent Pastes · {$see_all}";
- $side_nav->appendChild($this->renderPasteList($paste_list,
- $header,
- $pager = null));
break;
case 'my':
$header = 'Your Pastes';
- $side_nav->appendChild($this->renderPasteList($paste_list,
- $header,
- $pager));
break;
case 'all':
$header = 'All Pastes';
- $side_nav->appendChild($this->renderPasteList($paste_list,
- $header,
- $pager));
break;
}
+ $side_nav->appendChild(
+ $this->renderPasteList($paste_list, $header, $pager));
+
+
return $this->buildStandardPageResponse(
$side_nav,
array(
@@ -282,30 +249,66 @@ final class PhabricatorPasteListController extends PhabricatorPasteController {
$this->setPaste($new_paste);
}
- private function loadPasteList() {
+ private function renderCreatePaste() {
$request = $this->getRequest();
+ $user = $request->getUser();
- $pager = new AphrontPagerView();
- $pager->setOffset($this->getOffset());
- if ($this->getPageSize()) {
- $pager->setPageSize($this->getPageSize());
- }
+ $new_paste = $this->getPaste();
- if ($this->getAuthor()) {
- $pastes = id(new PhabricatorPaste())->loadAllWhere(
- 'authorPHID = %s ORDER BY id DESC LIMIT %d, %d',
- $this->getAuthor(),
- $pager->getOffset(),
- $pager->getPageSize() + 1);
- } else {
- $pastes = id(new PhabricatorPaste())->loadAllWhere(
- '1 = 1 ORDER BY id DESC LIMIT %d, %d',
- $pager->getOffset(),
- $pager->getPageSize() + 1);
- }
+ $form = new AphrontFormView();
- $pastes = $pager->sliceResults($pastes);
- $pager->setURI($request->getRequestURI(), 'page');
+ $available_languages = PhabricatorEnv::getEnvConfig(
+ 'pygments.dropdown-choices');
+ asort($available_languages);
+ $language_select = id(new AphrontFormSelectControl())
+ ->setLabel('Language')
+ ->setName('language')
+ ->setValue($new_paste->getLanguage())
+ ->setOptions($available_languages);
+
+ $form
+ ->setUser($user)
+ ->setAction($request->getRequestURI()->getPath())
+ ->addHiddenInput('parent', $new_paste->getParentPHID())
+ ->appendChild(
+ id(new AphrontFormTextControl())
+ ->setLabel('Title')
+ ->setValue($new_paste->getTitle())
+ ->setName('title'))
+ ->appendChild($language_select)
+ ->appendChild(
+ id(new AphrontFormTextAreaControl())
+ ->setLabel('Text')
+ ->setError($this->getErrorText())
+ ->setValue($this->getPasteText())
+ ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL)
+ ->setName('text'))
+
+ /* TODO: Doesn't have any useful options yet.
+ ->appendChild(
+ id(new AphrontFormPolicyControl())
+ ->setLabel('Visible To')
+ ->setUser($user)
+ ->setValue(
+ $new_paste->getPolicy(PhabricatorPolicyCapability::CAN_VIEW))
+ ->setName('policy'))
+ */
+
+ ->appendChild(
+ id(new AphrontFormSubmitControl())
+ ->addCancelButton('/paste/')
+ ->setValue('Create Paste'));
+
+ $create_panel = new AphrontPanelView();
+ $create_panel->setWidth(AphrontPanelView::WIDTH_FULL);
+ $create_panel->setHeader('Create a Paste');
+ $create_panel->appendChild($form);
+
+ return $create_panel;
+ }
+
+ private function renderPasteList(array $pastes, $header, $pager) {
+ assert_instances_of($pastes, 'PhabricatorPaste');
$phids = mpull($pastes, 'getAuthorPHID');
$handles = array();
@@ -318,8 +321,7 @@ final class PhabricatorPasteListController extends PhabricatorPasteController {
if ($phids) {
$files = id(new PhabricatorFile())->loadAllWhere(
'phid in (%Ls)',
- $phids
- );
+ $phids);
if ($files) {
$file_uris = mpull($files, 'getBestURI', 'getPHID');
}
@@ -363,59 +365,7 @@ final class PhabricatorPasteListController extends PhabricatorPasteController {
);
}
- return array($paste_list_rows, $pager);
- }
- private function renderCreatePaste() {
- $request = $this->getRequest();
- $user = $request->getUser();
-
- $new_paste = $this->getPaste();
-
- $form = new AphrontFormView();
-
- $available_languages = PhabricatorEnv::getEnvConfig(
- 'pygments.dropdown-choices');
- asort($available_languages);
- $language_select = id(new AphrontFormSelectControl())
- ->setLabel('Language')
- ->setName('language')
- ->setValue($new_paste->getLanguage())
- ->setOptions($available_languages);
-
- $form
- ->setUser($user)
- ->setAction($request->getRequestURI()->getPath())
- ->addHiddenInput('parent', $new_paste->getParentPHID())
- ->appendChild(
- id(new AphrontFormTextControl())
- ->setLabel('Title')
- ->setValue($new_paste->getTitle())
- ->setName('title'))
- ->appendChild($language_select)
- ->appendChild(
- id(new AphrontFormTextAreaControl())
- ->setLabel('Text')
- ->setError($this->getErrorText())
- ->setValue($this->getPasteText())
- ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL)
- ->setName('text'))
- ->appendChild(
- id(new AphrontFormSubmitControl())
- ->addCancelButton('/paste/')
- ->setValue('Create Paste'));
-
- $create_panel = new AphrontPanelView();
- $create_panel->setWidth(AphrontPanelView::WIDTH_FULL);
- $create_panel->setHeader('Create a Paste');
- $create_panel->appendChild($form);
-
- return $create_panel;
- }
-
- private function renderPasteList($paste_list_rows,
- $header,
- $pager = null) {
$table = new AphrontTableView($paste_list_rows);
$table->setHeaders(
array(
diff --git a/src/applications/paste/controller/list/__init__.php b/src/applications/paste/controller/list/__init__.php
index 40f73b234f..7c46b6da12 100644
--- a/src/applications/paste/controller/list/__init__.php
+++ b/src/applications/paste/controller/list/__init__.php
@@ -9,10 +9,11 @@
phutil_require_module('phabricator', 'aphront/response/redirect');
phutil_require_module('phabricator', 'applications/files/storage/file');
phutil_require_module('phabricator', 'applications/paste/controller/base');
+phutil_require_module('phabricator', 'applications/paste/query/paste');
phutil_require_module('phabricator', 'applications/paste/storage/paste');
phutil_require_module('phabricator', 'applications/phid/handle/data');
phutil_require_module('phabricator', 'infrastructure/env');
-phutil_require_module('phabricator', 'view/control/pager');
+phutil_require_module('phabricator', 'view/control/idpager');
phutil_require_module('phabricator', 'view/control/table');
phutil_require_module('phabricator', 'view/form/base');
phutil_require_module('phabricator', 'view/form/control/select');
diff --git a/src/applications/paste/controller/view/PhabricatorPasteViewController.php b/src/applications/paste/controller/view/PhabricatorPasteViewController.php
index 26ccb1d7b9..9b3cdc44e2 100644
--- a/src/applications/paste/controller/view/PhabricatorPasteViewController.php
+++ b/src/applications/paste/controller/view/PhabricatorPasteViewController.php
@@ -29,7 +29,11 @@ final class PhabricatorPasteViewController extends PhabricatorPasteController {
$request = $this->getRequest();
$user = $request->getUser();
- $paste = id(new PhabricatorPaste())->load($this->id);
+ $paste = id(new PhabricatorPasteQuery())
+ ->setViewer($user)
+ ->withPasteIDs(array($this->id))
+ ->executeOne();
+
if (!$paste) {
return new Aphront404Response();
}
diff --git a/src/applications/paste/controller/view/__init__.php b/src/applications/paste/controller/view/__init__.php
index 57ad86603b..635b80b2ca 100644
--- a/src/applications/paste/controller/view/__init__.php
+++ b/src/applications/paste/controller/view/__init__.php
@@ -11,6 +11,7 @@ phutil_require_module('phabricator', 'aphront/response/404');
phutil_require_module('phabricator', 'applications/files/storage/file');
phutil_require_module('phabricator', 'applications/markup/syntax');
phutil_require_module('phabricator', 'applications/paste/controller/base');
+phutil_require_module('phabricator', 'applications/paste/query/paste');
phutil_require_module('phabricator', 'applications/paste/storage/paste');
phutil_require_module('phabricator', 'infrastructure/celerity/api');
phutil_require_module('phabricator', 'infrastructure/javelin/api');
diff --git a/src/applications/paste/query/paste/PhabricatorPasteQuery.php b/src/applications/paste/query/paste/PhabricatorPasteQuery.php
new file mode 100644
index 0000000000..4a8d12a87e
--- /dev/null
+++ b/src/applications/paste/query/paste/PhabricatorPasteQuery.php
@@ -0,0 +1,73 @@
+pasteIDs = $ids;
+ return $this;
+ }
+
+ public function withAuthorPHIDs(array $phids) {
+ $this->authorPHIDs = $phids;
+ return $this;
+ }
+
+ public function loadPage() {
+ $table = new PhabricatorPaste();
+ $conn_r = $table->establishConnection('r');
+
+ $data = queryfx_all(
+ $conn_r,
+ 'SELECT paste.* FROM %T paste %Q %Q %Q',
+ $table->getTableName(),
+ $this->buildWhereClause($conn_r),
+ $this->buildOrderClause($conn_r),
+ $this->buildLimitClause($conn_r));
+
+ $results = $table->loadAllFromArray($data);
+
+ return $this->processResults($results);
+ }
+
+ protected function buildWhereClause($conn_r) {
+ $where = array();
+
+ $where[] = $this->buildPagingClause($conn_r);
+
+ if ($this->pasteIDs) {
+ $where[] = qsprintf(
+ $conn_r,
+ 'id IN (%Ls)',
+ $this->pasteIDs);
+ }
+
+ if ($this->authorPHIDs) {
+ $where[] = qsprintf(
+ $conn_r,
+ 'authorPHID IN (%Ls)',
+ $this->authorPHIDs);
+ }
+
+ return $this->formatWhereClause($where);
+ }
+
+}
diff --git a/src/applications/paste/query/paste/__init__.php b/src/applications/paste/query/paste/__init__.php
new file mode 100644
index 0000000000..56f2397fde
--- /dev/null
+++ b/src/applications/paste/query/paste/__init__.php
@@ -0,0 +1,15 @@
+getPHID() == $this->getAuthorPHID());
+ }
+
}
diff --git a/src/applications/paste/storage/paste/__init__.php b/src/applications/paste/storage/paste/__init__.php
index ef2ac38a2f..815bfa13a2 100644
--- a/src/applications/paste/storage/paste/__init__.php
+++ b/src/applications/paste/storage/paste/__init__.php
@@ -9,6 +9,9 @@
phutil_require_module('phabricator', 'applications/paste/storage/base');
phutil_require_module('phabricator', 'applications/phid/constants');
phutil_require_module('phabricator', 'applications/phid/storage/phid');
+phutil_require_module('phabricator', 'applications/policy/constants/capability');
+phutil_require_module('phabricator', 'applications/policy/constants/policy');
+phutil_require_module('phabricator', 'applications/policy/interface/policy');
phutil_require_source('PhabricatorPaste.php');
diff --git a/src/applications/policy/__tests__/PhabricatorPolicyTestCase.php b/src/applications/policy/__tests__/PhabricatorPolicyTestCase.php
new file mode 100644
index 0000000000..7f8b3c69de
--- /dev/null
+++ b/src/applications/policy/__tests__/PhabricatorPolicyTestCase.php
@@ -0,0 +1,138 @@
+setCapabilities(
+ array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ ));
+ $object->setPolicies(
+ array(
+ PhabricatorPolicyCapability::CAN_VIEW =>
+ PhabricatorPolicies::POLICY_PUBLIC,
+ ));
+
+ $query = new PhabricatorPolicyTestQuery();
+ $query->setResults(array($object));
+ $query->setViewer($viewer);
+ $result = $query->executeOne();
+
+ $this->assertEqual($object, $result, 'Policy: Public');
+ }
+
+
+ /**
+ * Verify that any logged-in user can view an object with POLICY_USER, but
+ * logged-out users can not.
+ */
+ public function testUsersPolicy() {
+ $viewer = new PhabricatorUser();
+
+ $object = new PhabricatorPolicyTestObject();
+ $object->setCapabilities(
+ array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ ));
+ $object->setPolicies(
+ array(
+ PhabricatorPolicyCapability::CAN_VIEW =>
+ PhabricatorPolicies::POLICY_USER,
+ ));
+
+ $query = new PhabricatorPolicyTestQuery();
+ $query->setResults(array($object));
+ $query->setViewer($viewer);
+
+ $caught = null;
+ try {
+ $query->executeOne();
+ } catch (PhabricatorPolicyException $ex) {
+ $caught = $ex;
+ }
+
+ $this->assertEqual(
+ true,
+ ($caught instanceof PhabricatorPolicyException),
+ 'Policy: Users rejects logged out users.');
+
+ $viewer->setPHID(1);
+ $result = $query->executeOne();
+ $this->assertEqual(
+ $object,
+ $result,
+ 'Policy: Users');
+ }
+
+
+ /**
+ * Verify that no one can view an object with POLICY_NOONE.
+ */
+ public function testNoOnePolicy() {
+ $viewer = new PhabricatorUser();
+
+ $object = new PhabricatorPolicyTestObject();
+ $object->setCapabilities(
+ array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ ));
+ $object->setPolicies(
+ array(
+ PhabricatorPolicyCapability::CAN_VIEW =>
+ PhabricatorPolicies::POLICY_NOONE,
+ ));
+
+ $query = new PhabricatorPolicyTestQuery();
+ $query->setResults(array($object));
+ $query->setViewer($viewer);
+
+ $caught = null;
+ try {
+ $query->executeOne();
+ } catch (PhabricatorPolicyException $ex) {
+ $caught = $ex;
+ }
+
+ $this->assertEqual(
+ true,
+ ($caught instanceof PhabricatorPolicyException),
+ 'Policy: No One rejects logged out users.');
+
+ $viewer->setPHID(1);
+
+ $caught = null;
+ try {
+ $query->executeOne();
+ } catch (PhabricatorPolicyException $ex) {
+ $caught = $ex;
+ }
+
+ $this->assertEqual(
+ true,
+ ($caught instanceof PhabricatorPolicyException),
+ 'Policy: No One rejects logged-in users.');
+ }
+
+}
diff --git a/src/applications/policy/__tests__/PhabricatorPolicyTestObject.php b/src/applications/policy/__tests__/PhabricatorPolicyTestObject.php
new file mode 100644
index 0000000000..d9f310ff0d
--- /dev/null
+++ b/src/applications/policy/__tests__/PhabricatorPolicyTestObject.php
@@ -0,0 +1,57 @@
+capabilities;
+ }
+
+ public function getPolicy($capability) {
+ return idx($this->policies, $capability);
+ }
+
+ public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
+ $auto = idx($this->automaticCapabilities, $capability, array());
+ return idx($auto, $viewer->getPHID());
+ }
+
+ public function setCapabilities(array $capabilities) {
+ $this->capabilities = $capabilities;
+ return $this;
+ }
+
+ public function setPolicies(array $policy_map) {
+ $this->policies = $policy_map;
+ return $this;
+ }
+
+ public function setAutomaticCapabilities(array $auto_map) {
+ $this->automaticCapabilities = $auto_map;
+ return $this;
+ }
+
+}
diff --git a/src/applications/policy/__tests__/PhabricatorPolicyTestQuery.php b/src/applications/policy/__tests__/PhabricatorPolicyTestQuery.php
new file mode 100644
index 0000000000..e1389782c2
--- /dev/null
+++ b/src/applications/policy/__tests__/PhabricatorPolicyTestQuery.php
@@ -0,0 +1,40 @@
+results = $results;
+ return $this;
+ }
+
+ public function loadPage() {
+ return $this->results;
+ }
+
+ public function nextPage(array $page) {
+ return null;
+ }
+
+}
diff --git a/src/applications/policy/__tests__/__init__.php b/src/applications/policy/__tests__/__init__.php
new file mode 100644
index 0000000000..48d2be7ea0
--- /dev/null
+++ b/src/applications/policy/__tests__/__init__.php
@@ -0,0 +1,21 @@
+viewer = $user;
+ return $this;
+ }
+
+ public function setCapability($capability) {
+ $this->capability = $capability;
+ return $this;
+ }
+
+ public function raisePolicyExceptions($raise) {
+ $this->raisePolicyExceptions = $raise;
+ return $this;
+ }
+
+ public function apply(array $objects) {
+ assert_instances_of($objects, 'PhabricatorPolicyInterface');
+
+ $viewer = $this->viewer;
+ $capability = $this->capability;
+
+ if (!$viewer || !$capability) {
+ throw new Exception(
+ 'Call setViewer() and setCapability() before apply()!');
+ }
+
+ $filtered = array();
+
+ foreach ($objects as $key => $object) {
+ $object_capabilities = $object->getCapabilities();
+
+ if (!in_array($capability, $object_capabilities)) {
+ throw new Exception(
+ "Testing for capability '{$capability}' on an object which does not ".
+ "have that capability!");
+ }
+
+ if ($object->hasAutomaticCapability($capability, $this->viewer)) {
+ $filtered[$key] = $object;
+ continue;
+ }
+
+ $policy = $object->getPolicy($capability);
+
+ switch ($policy) {
+ case PhabricatorPolicies::POLICY_PUBLIC:
+ $filtered[$key] = $object;
+ break;
+ case PhabricatorPolicies::POLICY_USER:
+ if ($viewer->getPHID()) {
+ $filtered[$key] = $object;
+ } else {
+ $this->rejectObject($object, $policy);
+ }
+ break;
+ case PhabricatorPolicies::POLICY_NOONE:
+ $this->rejectObject($object, $policy);
+ break;
+ default:
+ throw new Exception("Object has unknown policy '{$policy}'!");
+ }
+ }
+
+ return $filtered;
+ }
+
+ private function rejectObject($object, $policy) {
+ if (!$this->raisePolicyExceptions) {
+ return;
+ }
+
+ $message = "You do not have permission to view this object.";
+
+ switch ($policy) {
+ case PhabricatorPolicies::POLICY_PUBLIC:
+ $who = "This is curious, since anyone can view the object.";
+ break;
+ case PhabricatorPolicies::POLICY_USER:
+ $who = "To view this object, you must be logged in.";
+ break;
+ case PhabricatorPolicies::POLICY_NOONE:
+ $who = "No one can view this object.";
+ break;
+ default:
+ $who = "It is unclear who can view this object.";
+ break;
+ }
+
+ throw new PhabricatorPolicyException("{$message} {$who}");
+ }
+}
diff --git a/src/applications/policy/filter/policy/__init__.php b/src/applications/policy/filter/policy/__init__.php
new file mode 100644
index 0000000000..53e078af9d
--- /dev/null
+++ b/src/applications/policy/filter/policy/__init__.php
@@ -0,0 +1,15 @@
+viewer = $viewer;
+ return $this;
+ }
+
+ final public function getViewer() {
+ return $this->viewer;
+ }
+
+ final public function setLimit($limit) {
+ $this->limit = $limit;
+ return $this;
+ }
+
+ final public function getLimit() {
+ return $this->limit;
+ }
+
+ 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!");
+ }
+ return head($results);
+ }
+
+ 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);
+
+ do {
+ $page = $this->loadPage();
+
+ $visible = $filter->apply($page);
+ foreach ($visible as $key => $result) {
+ $results[$key] = $result;
+ if ($this->getLimit() && count($results) >= $this->getLimit()) {
+ break 2;
+ }
+ }
+
+ if (!$this->getLimit() || (count($page) < $this->getLimit())) {
+ break;
+ }
+
+ $this->nextPage($page);
+ } while (true);
+
+ return $results;
+ }
+
+ abstract protected function loadPage();
+ abstract protected function nextPage(array $page);
+
+}
diff --git a/src/infrastructure/query/policy/base/__init__.php b/src/infrastructure/query/policy/base/__init__.php
new file mode 100644
index 0000000000..a9ec3c47f2
--- /dev/null
+++ b/src/infrastructure/query/policy/base/__init__.php
@@ -0,0 +1,16 @@
+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());
+ } else {
+ return '';
+ }
+ }
+
+ final protected function buildPagingClause(
+ AphrontDatabaseConnection $conn_r) {
+
+ if ($this->beforeID) {
+ return qsprintf(
+ $conn_r,
+ '%C > %s',
+ $this->getPagingColumn(),
+ $this->beforeID);
+ } else if ($this->afterID) {
+ return qsprintf(
+ $conn_r,
+ '%C < %s',
+ $this->getPagingColumn(),
+ $this->afterID);
+ }
+
+ return null;
+ }
+
+ final protected function buildOrderClause(AphrontDatabaseConnection $conn_r) {
+ if ($this->beforeID) {
+ return qsprintf(
+ $conn_r,
+ 'ORDER BY %C ASC',
+ $this->getPagingColumn());
+ } else {
+ return qsprintf(
+ $conn_r,
+ 'ORDER BY %C DESC',
+ $this->getPagingColumn());
+ }
+ }
+
+ final protected function processResults(array $results) {
+ if ($this->beforeID) {
+ $results = array_reverse($results, $preserve_keys = true);
+ }
+ return $results;
+ }
+
+
+ final public function executeWithPager(AphrontIDPagerView $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/idpaged/__init__.php b/src/infrastructure/query/policy/idpaged/__init__.php
new file mode 100644
index 0000000000..9852f089a6
--- /dev/null
+++ b/src/infrastructure/query/policy/idpaged/__init__.php
@@ -0,0 +1,15 @@
+pageSize = max(1, $page_size);
+ return $this;
+ }
+
+ final public function getPageSize() {
+ return $this->pageSize;
+ }
+
+ final public function setURI(PhutilURI $uri) {
+ $this->uri = $uri;
+ return $this;
+ }
+
+ final public function readFromRequest(AphrontRequest $request) {
+ $this->uri = $request->getRequestURI();
+ $this->afterID = $request->getStr('after');
+ $this->beforeID = $request->getStr('before');
+ return $this;
+ }
+
+ final public function setAfterID($after_id) {
+ $this->afterID = $after_id;
+ return $this;
+ }
+
+ final public function getAfterID() {
+ return $this->afterID;
+ }
+
+ final public function setBeforeID($before_id) {
+ $this->beforeID = $before_id;
+ return $this;
+ }
+
+ final public function getBeforeID() {
+ return $this->beforeID;
+ }
+
+ final public function setNextPageID($next_page_id) {
+ $this->nextPageID = $next_page_id;
+ return $this;
+ }
+
+ final public function getNextPageID() {
+ return $this->nextPageID;
+ }
+
+ final public function setPrevPageID($prev_page_id) {
+ $this->prevPageID = $prev_page_id;
+ return $this;
+ }
+
+ final public function getPrevPageID() {
+ return $this->prevPageID;
+ }
+
+ final public function sliceResults(array $results) {
+ if (count($results) > $this->getPageSize()) {
+ $results = array_slice($results, 0, $this->getPageSize(), true);
+ }
+ return $results;
+ }
+
+ public function render() {
+ if (!$this->uri) {
+ throw new Exception(
+ "You must call setURI() before you can call render().");
+ }
+
+ $links = array();
+
+ if ($this->beforeID || $this->afterID) {
+ $links[] = phutil_render_tag(
+ 'a',
+ array(
+ 'href' => $this->uri
+ ->alter('before', null)
+ ->alter('after', null),
+ ),
+ "\xC2\xAB First");
+ }
+
+ if ($this->prevPageID) {
+ $links[] = phutil_render_tag(
+ 'a',
+ array(
+ 'href' => $this->uri
+ ->alter('after', null)
+ ->alter('before', $this->prevPageID),
+ ),
+ "\xE2\x80\xB9 Prev");
+ }
+
+ if ($this->nextPageID) {
+ $links[] = phutil_render_tag(
+ 'a',
+ array(
+ 'href' => $this->uri
+ ->alter('after', $this->nextPageID)
+ ->alter('before', null),
+ ),
+ "Next \xE2\x80\xBA");
+ }
+
+ return
+ '';
+ }
+
+}
diff --git a/src/view/control/idpager/__init__.php b/src/view/control/idpager/__init__.php
new file mode 100644
index 0000000000..e332da4a43
--- /dev/null
+++ b/src/view/control/idpager/__init__.php
@@ -0,0 +1,14 @@
+user = $user;
+ return $this;
+ }
+
+ public function getUser() {
+ return $this->user;
+ }
+
+ protected function getCustomControlClass() {
+ return 'aphront-form-control-policy';
+ }
+
+ private function getOptions() {
+ return array(
+ PhabricatorPolicies::POLICY_USER => 'All Users',
+ );
+ }
+
+ protected function renderInput() {
+ return AphrontFormSelectControl::renderSelectTag(
+ $this->getValue(),
+ $this->getOptions(),
+ array(
+ 'name' => $this->getName(),
+ 'disabled' => $this->getDisabled() ? 'disabled' : null,
+ 'id' => $this->getID(),
+ ));
+ }
+
+
+}
diff --git a/src/view/form/control/policy/__init__.php b/src/view/form/control/policy/__init__.php
new file mode 100644
index 0000000000..bb17d845ba
--- /dev/null
+++ b/src/view/form/control/policy/__init__.php
@@ -0,0 +1,14 @@
+