1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-22 21:40:55 +01:00

Make feed stories properly respect object policies

Summary:
  - When a feed story's primary object is a Policy object, use its visibility policy to control story visibility. Leave an exception for
  - Augment PhabricatorPolicyAwareQuery so queries may do pre-policy filtering without the need to handle their own buffering/cursor code. (We could slightly improve this: if a query returns less than a page of pre-filtered results we could keep getting pre-filtered results until we had at least a page's worth and then filter them all at once.)
  - Load and attach "required objects" to feed stories. We need this for policies anyway, and it will let us simplify story implementations by sourcing data directly from the object when we don't have some need to denormalize it (e.g., "title was changed from X to Y" needs to save the values of X and Y from when we published the story, but "user asked question X" can reflect the current version of the question).

Test Plan: Loaded main feed, project feed, notification menu / dropdown, notificaiton list, paginated things.

Reviewers: btrahan, vrana

Reviewed By: btrahan

CC: aran

Differential Revision: https://secure.phabricator.com/D3783
This commit is contained in:
epriestley 2012-10-23 12:01:59 -07:00
parent 951864d388
commit 696a1b22ba
17 changed files with 200 additions and 139 deletions

View file

@ -45,7 +45,7 @@ final class PhabricatorFeedQuery
}
protected function willFilterPage(array $data) {
return PhabricatorFeedStory::loadAllFromRows($data);
return PhabricatorFeedStory::loadAllFromRows($data, $this->getViewer());
}
private function buildJoinClause(AphrontDatabaseConnection $conn_r) {

View file

@ -44,21 +44,12 @@ final class PhabricatorFeedBuilder {
$user = $this->user;
$stories = $this->stories;
$handles = array();
if ($stories) {
$handle_phids = array_mergev(mpull($stories, 'getRequiredHandlePHIDs'));
$object_phids = array_mergev(mpull($stories, 'getRequiredObjectPHIDs'));
$handles = id(new PhabricatorObjectHandleData($handle_phids))
->loadHandles();
}
$null_view = new AphrontNullView();
require_celerity_resource('phabricator-feed-css');
$last_date = null;
foreach ($stories as $story) {
$story->setHandles($handles);
$story->setFramed($this->framed);
$date = ucfirst(phabricator_relative_date($story->getEpoch(), $user));

View file

@ -19,7 +19,6 @@
final class PhabricatorFeedStoryTypeConstants
extends PhabricatorFeedConstants {
const STORY_UNKNOWN = 'PhabricatorFeedStoryUnknown';
const STORY_STATUS = 'PhabricatorFeedStoryStatus';
const STORY_DIFFERENTIAL = 'PhabricatorFeedStoryDifferential';
const STORY_PHRICTION = 'PhabricatorFeedStoryPhriction';

View file

@ -22,14 +22,16 @@
* different channels (like feed, notifications and realtime alerts).
*
* @task load Loading Stories
* @task policy Policy Implementation
*/
abstract class PhabricatorFeedStory implements PhabricatorPolicyInterface {
private $data;
private $hasViewed;
private $handles;
private $framed;
private $primaryObjectPHID;
private $handles = array();
private $objects = array();
/* -( Loading Stories )---------------------------------------------------- */
@ -46,7 +48,7 @@ abstract class PhabricatorFeedStory implements PhabricatorPolicyInterface {
* objects.
* @task load
*/
public static function loadAllFromRows(array $rows) {
public static function loadAllFromRows(array $rows, PhabricatorUser $viewer) {
$stories = array();
$data = id(new PhabricatorFeedStoryData())->loadAllFromArray($rows);
@ -62,42 +64,99 @@ abstract class PhabricatorFeedStory implements PhabricatorPolicyInterface {
}
// If the story type isn't a valid class or isn't a subclass of
// PhabricatorFeedStory, load it as PhabricatorFeedStoryUnknown.
// PhabricatorFeedStory, decline to load it.
if (!$ok) {
$class = 'PhabricatorFeedStoryUnknown';
continue;
}
$key = $story_data->getChronologicalKey();
$stories[$key] = newv($class, array($story_data));
}
$object_phids = array();
$key_phids = array();
foreach ($stories as $key => $story) {
$phids = array();
foreach ($story->getRequiredObjectPHIDs() as $phid) {
$phids[$phid] = true;
}
if ($story->getPrimaryObjectPHID()) {
$phids[$story->getPrimaryObjectPHID()] = true;
}
$key_phids[$key] = $phids;
$object_phids += $phids;
}
$objects = id(new PhabricatorObjectHandleData(array_keys($object_phids)))
->setViewer($viewer)
->loadObjects();
foreach ($key_phids as $key => $phids) {
if (!$phids) {
continue;
}
$story_objects = array_select_keys($objects, array_keys($phids));
if (count($story_objects) != count($phids)) {
// An object this story requires either does not exist or is not visible
// to the user. Decline to render the story.
unset($stories[$key]);
unset($key_phids[$key]);
continue;
}
$stories[$key]->setObjects($story_objects);
}
$handle_phids = array();
foreach ($stories as $key => $story) {
foreach ($story->getRequiredHandlePHIDs() as $phid) {
$key_phids[$key][$phid] = true;
}
if ($story->getAuthorPHID()) {
$key_phids[$key][$story->getAuthorPHID()] = true;
}
$handle_phids += $key_phids[$key];
}
$handles = id(new PhabricatorObjectHandleData(array_keys($handle_phids)))
->setViewer($viewer)
->loadHandles();
foreach ($key_phids as $key => $phids) {
if (!$phids) {
continue;
}
$story_handles = array_select_keys($handles, array_keys($phids));
$stories[$key]->setHandles($story_handles);
}
return $stories;
}
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
return PhabricatorEnv::getEnvConfig('feed.public')
? PhabricatorPolicies::POLICY_PUBLIC
: PhabricatorPolicies::POLICY_USER;
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
public function setPrimaryObjectPHID($primary_object_phid) {
$this->primaryObjectPHID = $primary_object_phid;
public function setObjects(array $objects) {
$this->objects = $objects;
return $this;
}
public function getObject($phid) {
$object = idx($this->objects, $phid);
if (!$object) {
throw new Exception(
"Story is asking for an object it did not request ('{$phid}')!");
}
return $object;
}
public function getPrimaryObject() {
$phid = $this->getPrimaryObjectPHID();
if (!$phid) {
throw new Exception("Story has no primary object!");
}
return $this->getObject($phid);
}
public function getPrimaryObjectPHID() {
return $this->primaryObjectPHID;
return null;
}
final public function __construct(PhabricatorFeedStoryData $data) {
@ -116,6 +175,10 @@ abstract class PhabricatorFeedStory implements PhabricatorPolicyInterface {
return array();
}
public function getRequiredObjectPHIDs() {
return array();
}
public function setHasViewed($has_viewed) {
$this->hasViewed = $has_viewed;
return $this;
@ -125,10 +188,6 @@ abstract class PhabricatorFeedStory implements PhabricatorPolicyInterface {
return $this->hasViewed;
}
public function getRequiredObjectPHIDs() {
return array();
}
final public function setFramed($framed) {
$this->framed = $framed;
return $this;
@ -140,6 +199,10 @@ abstract class PhabricatorFeedStory implements PhabricatorPolicyInterface {
return $this;
}
final protected function getObjects() {
return $this->objects;
}
final protected function getHandles() {
return $this->handles;
}
@ -170,6 +233,14 @@ abstract class PhabricatorFeedStory implements PhabricatorPolicyInterface {
return $this->getStoryData()->getChronologicalKey();
}
final public function getValue($key, $default = null) {
return $this->getStoryData()->getValue($key, $default);
}
final public function getAuthorPHID() {
return $this->getStoryData()->getAuthorPHID();
}
final protected function renderHandleList(array $phids) {
$list = array();
foreach ($phids as $phid) {
@ -210,4 +281,48 @@ abstract class PhabricatorFeedStory implements PhabricatorPolicyInterface {
return array();
}
/* -( PhabricatorPolicyInterface Implementation )-------------------------- */
/**
* @task policy
*/
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
/**
* @task policy
*/
public function getPolicy($capability) {
// If this story's primary object is a policy-aware object, use its policy
// to control story visiblity.
$primary_phid = $this->getPrimaryObjectPHID();
if (isset($this->objects[$primary_phid])) {
$object = $this->objects[$primary_phid];
if ($object instanceof PhabricatorPolicyInterface) {
return $object->getPolicy($capability);
}
}
// TODO: Remove this once all objects are policy-aware. For now, keep
// respecting the `feed.public` setting.
return PhabricatorEnv::getEnvConfig('feed.public')
? PhabricatorPolicies::POLICY_PUBLIC
: PhabricatorPolicies::POLICY_USER;
}
/**
* @task policy
*/
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
}

View file

@ -24,6 +24,10 @@ abstract class PhabricatorFeedStoryAggregate extends PhabricatorFeedStory {
return head($this->getAggregateStories())->getHasViewed();
}
public function getPrimaryObjectPHID() {
return head($this->getAggregateStories())->getPrimaryObjectPHID();
}
public function getRequiredHandlePHIDs() {
$phids = array();
foreach ($this->getAggregateStories() as $story) {
@ -59,6 +63,18 @@ abstract class PhabricatorFeedStoryAggregate extends PhabricatorFeedStory {
final public function setAggregateStories(array $aggregate_stories) {
assert_instances_of($aggregate_stories, 'PhabricatorFeedStory');
$this->aggregateStories = $aggregate_stories;
$objects = array();
$handles = array();
foreach ($this->aggregateStories as $story) {
$objects += $story->getObjects();
$handles += $story->getHandles();
}
$this->setObjects($objects);
$this->setHandles($handles);
return $this;
}

View file

@ -18,26 +18,17 @@
final class PhabricatorFeedStoryAudit extends PhabricatorFeedStory {
public function getRequiredHandlePHIDs() {
return array(
$this->getStoryData()->getAuthorPHID(),
$this->getStoryData()->getValue('commitPHID'),
);
}
public function getRequiredObjectPHIDs() {
return array();
public function getPrimaryObjectPHID() {
return $this->getStoryData()->getValue('commitPHID');
}
public function renderView() {
$data = $this->getStoryData();
$author_phid = $data->getAuthorPHID();
$commit_phid = $data->getValue('commitPHID');
$author_phid = $this->getAuthorPHID();
$commit_phid = $this->getPrimaryObjectPHID();
$view = new PhabricatorFeedStoryView();
$action = $data->getValue('action');
$action = $this->getValue('action');
$verb = PhabricatorAuditActionConstants::getActionPastTenseVerb($action);
$view->setTitle(
@ -46,9 +37,9 @@ final class PhabricatorFeedStoryAudit extends PhabricatorFeedStory {
$this->linkTo($commit_phid).
".");
$view->setEpoch($data->getEpoch());
$view->setEpoch($this->getEpoch());
$comments = $data->getValue('content');
$comments = $this->getValue('content');
if ($comments) {
$full_size = true;
} else {
@ -57,7 +48,7 @@ final class PhabricatorFeedStoryAudit extends PhabricatorFeedStory {
if ($full_size) {
$view->setImage($this->getHandle($author_phid)->getImageURI());
$content = $this->renderSummary($data->getValue('content'));
$content = $this->renderSummary($this->getValue('content'));
$view->appendChild($content);
} else {
$view->setOneLineStory(true);

View file

@ -18,15 +18,14 @@
final class PhabricatorFeedStoryCommit extends PhabricatorFeedStory {
public function getRequiredHandlePHIDs() {
$data = $this->getStoryData();
public function getPrimaryObjectPHID() {
return $this->getValue('commitPHID');
}
return array_filter(
array(
$data->getValue('commitPHID'),
$data->getValue('authorPHID'),
$data->getValue('committerPHID'),
));
public function getRequiredHandlePHIDs() {
return array(
$this->getValue('committerPHID'),
);
}
public function renderView() {

View file

@ -18,19 +18,8 @@
final class PhabricatorFeedStoryDifferential extends PhabricatorFeedStory {
public function getRequiredHandlePHIDs() {
$data = $this->getStoryData();
return array(
$this->getStoryData()->getAuthorPHID(),
$data->getValue('revision_phid'),
$data->getValue('revision_author_phid'),
);
}
public function getRequiredObjectPHIDs() {
return array(
$this->getStoryData()->getAuthorPHID(),
);
public function getPrimaryObjectPHID() {
return $this->getValue('revision_phid');
}
public function renderView() {
@ -78,13 +67,11 @@ final class PhabricatorFeedStoryDifferential extends PhabricatorFeedStory {
private function getLineForData($data) {
$actor_phid = $data->getAuthorPHID();
$owner_phid = $data->getValue('revision_author_phid');
$revision_phid = $data->getValue('revision_phid');
$action = $data->getValue('action');
$actor_link = $this->linkTo($actor_phid);
$revision_link = $this->linkTo($revision_phid);
$owner_link = $this->linkTo($owner_phid);
$verb = DifferentialAction::getActionPastTenseVerb($action);

View file

@ -19,19 +19,13 @@
final class PhabricatorFeedStoryManiphest
extends PhabricatorFeedStory {
public function getRequiredHandlePHIDs() {
$data = $this->getStoryData();
return array_filter(
array(
$this->getStoryData()->getAuthorPHID(),
$data->getValue('taskPHID'),
$data->getValue('ownerPHID'),
));
public function getPrimaryObjectPHID() {
return $this->getValue('taskPHID');
}
public function getRequiredObjectPHIDs() {
public function getRequiredHandlePHIDs() {
return array(
$this->getStoryData()->getAuthorPHID(),
$this->getValue('ownerPHID'),
);
}

View file

@ -18,17 +18,8 @@
final class PhabricatorFeedStoryPhriction extends PhabricatorFeedStory {
public function getRequiredHandlePHIDs() {
return array(
$this->getStoryData()->getAuthorPHID(),
$this->getStoryData()->getValue('phid'),
);
}
public function getRequiredObjectPHIDs() {
return array(
$this->getStoryData()->getAuthorPHID(),
);
public function getPrimaryObjectPHID() {
return $this->getValue('phid');
}
public function renderView() {

View file

@ -18,17 +18,8 @@
final class PhabricatorFeedStoryProject extends PhabricatorFeedStory {
public function getRequiredHandlePHIDs() {
return array(
$this->getStoryData()->getAuthorPHID(),
$this->getStoryData()->getValue('projectPHID'),
);
}
public function getRequiredObjectPHIDs() {
return array(
$this->getStoryData()->getAuthorPHID(),
);
public function getPrimaryObjectPHID() {
return $this->getValue('projectPHID');
}
public function renderView() {

View file

@ -18,16 +18,8 @@
final class PhabricatorFeedStoryStatus extends PhabricatorFeedStory {
public function getRequiredHandlePHIDs() {
return array(
$this->getStoryData()->getAuthorPHID(),
);
}
public function getRequiredObjectPHIDs() {
return array(
$this->getStoryData()->getAuthorPHID(),
);
public function getPrimaryObjectPHID() {
return $this->getAuthorPHID();
}
public function renderView() {

View file

@ -20,7 +20,8 @@
* @task config Configuring the Query
* @task exec Query Execution
*/
final class PhabricatorNotificationQuery extends PhabricatorOffsetPagedQuery {
final class PhabricatorNotificationQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $userPHID;
private $keys;
@ -60,7 +61,7 @@ final class PhabricatorNotificationQuery extends PhabricatorOffsetPagedQuery {
/* -( Query Execution )---------------------------------------------------- */
public function execute() {
public function loadPage() {
if (!$this->userPHID) {
throw new Exception("Call setUser() before executing the query");
}
@ -72,7 +73,7 @@ final class PhabricatorNotificationQuery extends PhabricatorOffsetPagedQuery {
$data = queryfx_all(
$conn,
"SELECT story.*, notif.primaryObjectPHID, notif.hasViewed FROM %T notif
"SELECT story.*, notif.hasViewed FROM %T notif
JOIN %T story ON notif.chronologicalKey = story.chronologicalKey
%Q
ORDER BY notif.chronologicalKey DESC
@ -83,12 +84,13 @@ final class PhabricatorNotificationQuery extends PhabricatorOffsetPagedQuery {
$this->buildLimitClause($conn));
$viewed_map = ipull($data, 'hasViewed', 'chronologicalKey');
$primary_map = ipull($data, 'primaryObjectPHID', 'chronologicalKey');
$stories = PhabricatorFeedStory::loadAllFromRows($data);
$stories = PhabricatorFeedStory::loadAllFromRows(
$data,
$this->getViewer());
foreach ($stories as $key => $story) {
$story->setHasViewed($viewed_map[$key]);
$story->setPrimaryObjectPHID($primary_map[$key]);
}
return $stories;

View file

@ -136,19 +136,9 @@ final class PhabricatorNotificationBuilder {
$stories = mpull($stories, null, 'getChronologicalKey');
krsort($stories);
$handles = array();
$objects = array();
if ($stories) {
$handle_phids = array_mergev(mpull($stories, 'getRequiredHandlePHIDs'));
$handles = id(new PhabricatorObjectHandleData($handle_phids))
->loadHandles();
}
$null_view = new AphrontNullView();
foreach ($stories as $story) {
$story->setHandles($handles);
$view = $story->renderNotificationView();
$null_view->appendChild($view);
}

View file

@ -24,6 +24,7 @@ final class PhabricatorNotificationIndividualController
$user = $request->getUser();
$stories = id(new PhabricatorNotificationQuery())
->setViewer($user)
->setUserPHID($user->getPHID())
->withKeys(array($request->getStr('key')))
->execute();

View file

@ -40,6 +40,7 @@ final class PhabricatorNotificationListController
$pager->setOffset($request->getInt('offset'));
$query = new PhabricatorNotificationQuery();
$query->setViewer($user);
$query->setUserPHID($user->getPHID());
switch ($filter) {

View file

@ -25,6 +25,7 @@ final class PhabricatorNotificationPanelController
$user = $request->getUser();
$query = new PhabricatorNotificationQuery();
$query->setViewer($user);
$query->setUserPHID($user->getPHID());
$query->setLimit(15);