@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

+197 -136
+1 -1
src/applications/feed/PhabricatorFeedQuery.php
··· 45 45 } 46 46 47 47 protected function willFilterPage(array $data) { 48 - return PhabricatorFeedStory::loadAllFromRows($data); 48 + return PhabricatorFeedStory::loadAllFromRows($data, $this->getViewer()); 49 49 } 50 50 51 51 private function buildJoinClause(AphrontDatabaseConnection $conn_r) {
-9
src/applications/feed/builder/PhabricatorFeedBuilder.php
··· 44 44 $user = $this->user; 45 45 $stories = $this->stories; 46 46 47 - $handles = array(); 48 - if ($stories) { 49 - $handle_phids = array_mergev(mpull($stories, 'getRequiredHandlePHIDs')); 50 - $object_phids = array_mergev(mpull($stories, 'getRequiredObjectPHIDs')); 51 - $handles = id(new PhabricatorObjectHandleData($handle_phids)) 52 - ->loadHandles(); 53 - } 54 - 55 47 $null_view = new AphrontNullView(); 56 48 57 49 require_celerity_resource('phabricator-feed-css'); 58 50 59 51 $last_date = null; 60 52 foreach ($stories as $story) { 61 - $story->setHandles($handles); 62 53 $story->setFramed($this->framed); 63 54 64 55 $date = ucfirst(phabricator_relative_date($story->getEpoch(), $user));
-1
src/applications/feed/constants/PhabricatorFeedStoryTypeConstants.php
··· 19 19 final class PhabricatorFeedStoryTypeConstants 20 20 extends PhabricatorFeedConstants { 21 21 22 - const STORY_UNKNOWN = 'PhabricatorFeedStoryUnknown'; 23 22 const STORY_STATUS = 'PhabricatorFeedStoryStatus'; 24 23 const STORY_DIFFERENTIAL = 'PhabricatorFeedStoryDifferential'; 25 24 const STORY_PHRICTION = 'PhabricatorFeedStoryPhriction';
+142 -27
src/applications/feed/story/PhabricatorFeedStory.php
··· 21 21 * user adding a comment) which may be represented in different forms on 22 22 * different channels (like feed, notifications and realtime alerts). 23 23 * 24 - * @task load Loading Stories 24 + * @task load Loading Stories 25 + * @task policy Policy Implementation 25 26 */ 26 27 abstract class PhabricatorFeedStory implements PhabricatorPolicyInterface { 27 28 28 29 private $data; 29 30 private $hasViewed; 30 - private $handles; 31 31 private $framed; 32 - private $primaryObjectPHID; 32 + 33 + private $handles = array(); 34 + private $objects = array(); 33 35 34 36 35 37 /* -( Loading Stories )---------------------------------------------------- */ ··· 46 48 * objects. 47 49 * @task load 48 50 */ 49 - public static function loadAllFromRows(array $rows) { 51 + public static function loadAllFromRows(array $rows, PhabricatorUser $viewer) { 50 52 $stories = array(); 51 53 52 54 $data = id(new PhabricatorFeedStoryData())->loadAllFromArray($rows); ··· 62 64 } 63 65 64 66 // If the story type isn't a valid class or isn't a subclass of 65 - // PhabricatorFeedStory, load it as PhabricatorFeedStoryUnknown. 66 - 67 + // PhabricatorFeedStory, decline to load it. 67 68 if (!$ok) { 68 - $class = 'PhabricatorFeedStoryUnknown'; 69 + continue; 69 70 } 70 71 71 72 $key = $story_data->getChronologicalKey(); 72 73 $stories[$key] = newv($class, array($story_data)); 73 74 } 74 75 76 + $object_phids = array(); 77 + $key_phids = array(); 78 + foreach ($stories as $key => $story) { 79 + $phids = array(); 80 + foreach ($story->getRequiredObjectPHIDs() as $phid) { 81 + $phids[$phid] = true; 82 + } 83 + if ($story->getPrimaryObjectPHID()) { 84 + $phids[$story->getPrimaryObjectPHID()] = true; 85 + } 86 + $key_phids[$key] = $phids; 87 + $object_phids += $phids; 88 + } 89 + 90 + $objects = id(new PhabricatorObjectHandleData(array_keys($object_phids))) 91 + ->setViewer($viewer) 92 + ->loadObjects(); 93 + 94 + foreach ($key_phids as $key => $phids) { 95 + if (!$phids) { 96 + continue; 97 + } 98 + $story_objects = array_select_keys($objects, array_keys($phids)); 99 + if (count($story_objects) != count($phids)) { 100 + // An object this story requires either does not exist or is not visible 101 + // to the user. Decline to render the story. 102 + unset($stories[$key]); 103 + unset($key_phids[$key]); 104 + continue; 105 + } 106 + 107 + $stories[$key]->setObjects($story_objects); 108 + } 109 + 110 + $handle_phids = array(); 111 + foreach ($stories as $key => $story) { 112 + foreach ($story->getRequiredHandlePHIDs() as $phid) { 113 + $key_phids[$key][$phid] = true; 114 + } 115 + if ($story->getAuthorPHID()) { 116 + $key_phids[$key][$story->getAuthorPHID()] = true; 117 + } 118 + $handle_phids += $key_phids[$key]; 119 + } 120 + 121 + $handles = id(new PhabricatorObjectHandleData(array_keys($handle_phids))) 122 + ->setViewer($viewer) 123 + ->loadHandles(); 124 + 125 + foreach ($key_phids as $key => $phids) { 126 + if (!$phids) { 127 + continue; 128 + } 129 + $story_handles = array_select_keys($handles, array_keys($phids)); 130 + $stories[$key]->setHandles($story_handles); 131 + } 132 + 75 133 return $stories; 76 134 } 77 135 78 - public function getCapabilities() { 79 - return array( 80 - PhabricatorPolicyCapability::CAN_VIEW, 81 - ); 136 + public function setObjects(array $objects) { 137 + $this->objects = $objects; 138 + return $this; 82 139 } 83 140 84 - public function getPolicy($capability) { 85 - return PhabricatorEnv::getEnvConfig('feed.public') 86 - ? PhabricatorPolicies::POLICY_PUBLIC 87 - : PhabricatorPolicies::POLICY_USER; 88 - } 89 - 90 - public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { 91 - return false; 141 + public function getObject($phid) { 142 + $object = idx($this->objects, $phid); 143 + if (!$object) { 144 + throw new Exception( 145 + "Story is asking for an object it did not request ('{$phid}')!"); 146 + } 147 + return $object; 92 148 } 93 149 94 - public function setPrimaryObjectPHID($primary_object_phid) { 95 - $this->primaryObjectPHID = $primary_object_phid; 96 - return $this; 150 + public function getPrimaryObject() { 151 + $phid = $this->getPrimaryObjectPHID(); 152 + if (!$phid) { 153 + throw new Exception("Story has no primary object!"); 154 + } 155 + return $this->getObject($phid); 97 156 } 98 157 99 158 public function getPrimaryObjectPHID() { 100 - return $this->primaryObjectPHID; 159 + return null; 101 160 } 102 161 103 162 final public function __construct(PhabricatorFeedStoryData $data) { ··· 116 175 return array(); 117 176 } 118 177 178 + public function getRequiredObjectPHIDs() { 179 + return array(); 180 + } 181 + 119 182 public function setHasViewed($has_viewed) { 120 183 $this->hasViewed = $has_viewed; 121 184 return $this; ··· 125 188 return $this->hasViewed; 126 189 } 127 190 128 - public function getRequiredObjectPHIDs() { 129 - return array(); 130 - } 131 - 132 191 final public function setFramed($framed) { 133 192 $this->framed = $framed; 134 193 return $this; ··· 138 197 assert_instances_of($handles, 'PhabricatorObjectHandle'); 139 198 $this->handles = $handles; 140 199 return $this; 200 + } 201 + 202 + final protected function getObjects() { 203 + return $this->objects; 141 204 } 142 205 143 206 final protected function getHandles() { ··· 170 233 return $this->getStoryData()->getChronologicalKey(); 171 234 } 172 235 236 + final public function getValue($key, $default = null) { 237 + return $this->getStoryData()->getValue($key, $default); 238 + } 239 + 240 + final public function getAuthorPHID() { 241 + return $this->getStoryData()->getAuthorPHID(); 242 + } 243 + 173 244 final protected function renderHandleList(array $phids) { 174 245 $list = array(); 175 246 foreach ($phids as $phid) { ··· 208 279 209 280 public function getNotificationAggregations() { 210 281 return array(); 282 + } 283 + 284 + 285 + /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ 286 + 287 + 288 + /** 289 + * @task policy 290 + */ 291 + public function getCapabilities() { 292 + return array( 293 + PhabricatorPolicyCapability::CAN_VIEW, 294 + ); 295 + } 296 + 297 + 298 + /** 299 + * @task policy 300 + */ 301 + public function getPolicy($capability) { 302 + // If this story's primary object is a policy-aware object, use its policy 303 + // to control story visiblity. 304 + 305 + $primary_phid = $this->getPrimaryObjectPHID(); 306 + if (isset($this->objects[$primary_phid])) { 307 + $object = $this->objects[$primary_phid]; 308 + if ($object instanceof PhabricatorPolicyInterface) { 309 + return $object->getPolicy($capability); 310 + } 311 + } 312 + 313 + // TODO: Remove this once all objects are policy-aware. For now, keep 314 + // respecting the `feed.public` setting. 315 + return PhabricatorEnv::getEnvConfig('feed.public') 316 + ? PhabricatorPolicies::POLICY_PUBLIC 317 + : PhabricatorPolicies::POLICY_USER; 318 + } 319 + 320 + 321 + /** 322 + * @task policy 323 + */ 324 + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { 325 + return false; 211 326 } 212 327 213 328 }
+16
src/applications/feed/story/PhabricatorFeedStoryAggregate.php
··· 24 24 return head($this->getAggregateStories())->getHasViewed(); 25 25 } 26 26 27 + public function getPrimaryObjectPHID() { 28 + return head($this->getAggregateStories())->getPrimaryObjectPHID(); 29 + } 30 + 27 31 public function getRequiredHandlePHIDs() { 28 32 $phids = array(); 29 33 foreach ($this->getAggregateStories() as $story) { ··· 59 63 final public function setAggregateStories(array $aggregate_stories) { 60 64 assert_instances_of($aggregate_stories, 'PhabricatorFeedStory'); 61 65 $this->aggregateStories = $aggregate_stories; 66 + 67 + $objects = array(); 68 + $handles = array(); 69 + 70 + foreach ($this->aggregateStories as $story) { 71 + $objects += $story->getObjects(); 72 + $handles += $story->getHandles(); 73 + } 74 + 75 + $this->setObjects($objects); 76 + $this->setHandles($handles); 77 + 62 78 return $this; 63 79 } 64 80
+8 -17
src/applications/feed/story/PhabricatorFeedStoryAudit.php
··· 18 18 19 19 final class PhabricatorFeedStoryAudit extends PhabricatorFeedStory { 20 20 21 - public function getRequiredHandlePHIDs() { 22 - return array( 23 - $this->getStoryData()->getAuthorPHID(), 24 - $this->getStoryData()->getValue('commitPHID'), 25 - ); 26 - } 27 - 28 - public function getRequiredObjectPHIDs() { 29 - return array(); 21 + public function getPrimaryObjectPHID() { 22 + return $this->getStoryData()->getValue('commitPHID'); 30 23 } 31 24 32 25 public function renderView() { 33 - $data = $this->getStoryData(); 34 - 35 - $author_phid = $data->getAuthorPHID(); 36 - $commit_phid = $data->getValue('commitPHID'); 26 + $author_phid = $this->getAuthorPHID(); 27 + $commit_phid = $this->getPrimaryObjectPHID(); 37 28 38 29 $view = new PhabricatorFeedStoryView(); 39 30 40 - $action = $data->getValue('action'); 31 + $action = $this->getValue('action'); 41 32 $verb = PhabricatorAuditActionConstants::getActionPastTenseVerb($action); 42 33 43 34 $view->setTitle( ··· 46 37 $this->linkTo($commit_phid). 47 38 "."); 48 39 49 - $view->setEpoch($data->getEpoch()); 40 + $view->setEpoch($this->getEpoch()); 50 41 51 - $comments = $data->getValue('content'); 42 + $comments = $this->getValue('content'); 52 43 if ($comments) { 53 44 $full_size = true; 54 45 } else { ··· 57 48 58 49 if ($full_size) { 59 50 $view->setImage($this->getHandle($author_phid)->getImageURI()); 60 - $content = $this->renderSummary($data->getValue('content')); 51 + $content = $this->renderSummary($this->getValue('content')); 61 52 $view->appendChild($content); 62 53 } else { 63 54 $view->setOneLineStory(true);
+7 -8
src/applications/feed/story/PhabricatorFeedStoryCommit.php
··· 18 18 19 19 final class PhabricatorFeedStoryCommit extends PhabricatorFeedStory { 20 20 21 + public function getPrimaryObjectPHID() { 22 + return $this->getValue('commitPHID'); 23 + } 24 + 21 25 public function getRequiredHandlePHIDs() { 22 - $data = $this->getStoryData(); 23 - 24 - return array_filter( 25 - array( 26 - $data->getValue('commitPHID'), 27 - $data->getValue('authorPHID'), 28 - $data->getValue('committerPHID'), 29 - )); 26 + return array( 27 + $this->getValue('committerPHID'), 28 + ); 30 29 } 31 30 32 31 public function renderView() {
+2 -15
src/applications/feed/story/PhabricatorFeedStoryDifferential.php
··· 18 18 19 19 final class PhabricatorFeedStoryDifferential extends PhabricatorFeedStory { 20 20 21 - public function getRequiredHandlePHIDs() { 22 - $data = $this->getStoryData(); 23 - return array( 24 - $this->getStoryData()->getAuthorPHID(), 25 - $data->getValue('revision_phid'), 26 - $data->getValue('revision_author_phid'), 27 - ); 28 - } 29 - 30 - public function getRequiredObjectPHIDs() { 31 - return array( 32 - $this->getStoryData()->getAuthorPHID(), 33 - ); 21 + public function getPrimaryObjectPHID() { 22 + return $this->getValue('revision_phid'); 34 23 } 35 24 36 25 public function renderView() { ··· 78 67 79 68 private function getLineForData($data) { 80 69 $actor_phid = $data->getAuthorPHID(); 81 - $owner_phid = $data->getValue('revision_author_phid'); 82 70 $revision_phid = $data->getValue('revision_phid'); 83 71 $action = $data->getValue('action'); 84 72 85 73 $actor_link = $this->linkTo($actor_phid); 86 74 $revision_link = $this->linkTo($revision_phid); 87 - $owner_link = $this->linkTo($owner_phid); 88 75 89 76 $verb = DifferentialAction::getActionPastTenseVerb($action); 90 77
+4 -10
src/applications/feed/story/PhabricatorFeedStoryManiphest.php
··· 19 19 final class PhabricatorFeedStoryManiphest 20 20 extends PhabricatorFeedStory { 21 21 22 - public function getRequiredHandlePHIDs() { 23 - $data = $this->getStoryData(); 24 - return array_filter( 25 - array( 26 - $this->getStoryData()->getAuthorPHID(), 27 - $data->getValue('taskPHID'), 28 - $data->getValue('ownerPHID'), 29 - )); 22 + public function getPrimaryObjectPHID() { 23 + return $this->getValue('taskPHID'); 30 24 } 31 25 32 - public function getRequiredObjectPHIDs() { 26 + public function getRequiredHandlePHIDs() { 33 27 return array( 34 - $this->getStoryData()->getAuthorPHID(), 28 + $this->getValue('ownerPHID'), 35 29 ); 36 30 } 37 31
+2 -11
src/applications/feed/story/PhabricatorFeedStoryPhriction.php
··· 18 18 19 19 final class PhabricatorFeedStoryPhriction extends PhabricatorFeedStory { 20 20 21 - public function getRequiredHandlePHIDs() { 22 - return array( 23 - $this->getStoryData()->getAuthorPHID(), 24 - $this->getStoryData()->getValue('phid'), 25 - ); 26 - } 27 - 28 - public function getRequiredObjectPHIDs() { 29 - return array( 30 - $this->getStoryData()->getAuthorPHID(), 31 - ); 21 + public function getPrimaryObjectPHID() { 22 + return $this->getValue('phid'); 32 23 } 33 24 34 25 public function renderView() {
+2 -11
src/applications/feed/story/PhabricatorFeedStoryProject.php
··· 18 18 19 19 final class PhabricatorFeedStoryProject extends PhabricatorFeedStory { 20 20 21 - public function getRequiredHandlePHIDs() { 22 - return array( 23 - $this->getStoryData()->getAuthorPHID(), 24 - $this->getStoryData()->getValue('projectPHID'), 25 - ); 26 - } 27 - 28 - public function getRequiredObjectPHIDs() { 29 - return array( 30 - $this->getStoryData()->getAuthorPHID(), 31 - ); 21 + public function getPrimaryObjectPHID() { 22 + return $this->getValue('projectPHID'); 32 23 } 33 24 34 25 public function renderView() {
+2 -10
src/applications/feed/story/PhabricatorFeedStoryStatus.php
··· 18 18 19 19 final class PhabricatorFeedStoryStatus extends PhabricatorFeedStory { 20 20 21 - public function getRequiredHandlePHIDs() { 22 - return array( 23 - $this->getStoryData()->getAuthorPHID(), 24 - ); 25 - } 26 - 27 - public function getRequiredObjectPHIDs() { 28 - return array( 29 - $this->getStoryData()->getAuthorPHID(), 30 - ); 21 + public function getPrimaryObjectPHID() { 22 + return $this->getAuthorPHID(); 31 23 } 32 24 33 25 public function renderView() {
+8 -6
src/applications/notification/PhabricatorNotificationQuery.php
··· 20 20 * @task config Configuring the Query 21 21 * @task exec Query Execution 22 22 */ 23 - final class PhabricatorNotificationQuery extends PhabricatorOffsetPagedQuery { 23 + final class PhabricatorNotificationQuery 24 + extends PhabricatorCursorPagedPolicyAwareQuery { 24 25 25 26 private $userPHID; 26 27 private $keys; ··· 60 61 /* -( Query Execution )---------------------------------------------------- */ 61 62 62 63 63 - public function execute() { 64 + public function loadPage() { 64 65 if (!$this->userPHID) { 65 66 throw new Exception("Call setUser() before executing the query"); 66 67 } ··· 72 73 73 74 $data = queryfx_all( 74 75 $conn, 75 - "SELECT story.*, notif.primaryObjectPHID, notif.hasViewed FROM %T notif 76 + "SELECT story.*, notif.hasViewed FROM %T notif 76 77 JOIN %T story ON notif.chronologicalKey = story.chronologicalKey 77 78 %Q 78 79 ORDER BY notif.chronologicalKey DESC ··· 83 84 $this->buildLimitClause($conn)); 84 85 85 86 $viewed_map = ipull($data, 'hasViewed', 'chronologicalKey'); 86 - $primary_map = ipull($data, 'primaryObjectPHID', 'chronologicalKey'); 87 + 88 + $stories = PhabricatorFeedStory::loadAllFromRows( 89 + $data, 90 + $this->getViewer()); 87 91 88 - $stories = PhabricatorFeedStory::loadAllFromRows($data); 89 92 foreach ($stories as $key => $story) { 90 93 $story->setHasViewed($viewed_map[$key]); 91 - $story->setPrimaryObjectPHID($primary_map[$key]); 92 94 } 93 95 94 96 return $stories;
-10
src/applications/notification/builder/PhabricatorNotificationBuilder.php
··· 136 136 $stories = mpull($stories, null, 'getChronologicalKey'); 137 137 krsort($stories); 138 138 139 - $handles = array(); 140 - $objects = array(); 141 - 142 - if ($stories) { 143 - $handle_phids = array_mergev(mpull($stories, 'getRequiredHandlePHIDs')); 144 - $handles = id(new PhabricatorObjectHandleData($handle_phids)) 145 - ->loadHandles(); 146 - } 147 - 148 139 $null_view = new AphrontNullView(); 149 140 150 141 foreach ($stories as $story) { 151 - $story->setHandles($handles); 152 142 $view = $story->renderNotificationView(); 153 143 $null_view->appendChild($view); 154 144 }
+1
src/applications/notification/controller/PhabricatorNotificationIndividualController.php
··· 24 24 $user = $request->getUser(); 25 25 26 26 $stories = id(new PhabricatorNotificationQuery()) 27 + ->setViewer($user) 27 28 ->setUserPHID($user->getPHID()) 28 29 ->withKeys(array($request->getStr('key'))) 29 30 ->execute();
+1
src/applications/notification/controller/PhabricatorNotificationListController.php
··· 40 40 $pager->setOffset($request->getInt('offset')); 41 41 42 42 $query = new PhabricatorNotificationQuery(); 43 + $query->setViewer($user); 43 44 $query->setUserPHID($user->getPHID()); 44 45 45 46 switch ($filter) {
+1
src/applications/notification/controller/PhabricatorNotificationPanelController.php
··· 25 25 $user = $request->getUser(); 26 26 27 27 $query = new PhabricatorNotificationQuery(); 28 + $query->setViewer($user); 28 29 $query->setUserPHID($user->getPHID()); 29 30 $query->setLimit(15); 30 31