4 * Manages rendering and aggregation of a story. A story is an event (like a
5 * user adding a comment) which may be represented in different forms on
6 * different channels (like feed, notifications and realtime alerts).
8 * @task load Loading Stories
9 * @task policy Policy Implementation
11 abstract class PhabricatorFeedStory
14 PhabricatorPolicyInterface
,
15 PhabricatorMarkupInterface
{
19 private $hovercard = false;
20 private $renderingTarget = PhabricatorApplicationTransaction
::TARGET_HTML
;
22 private $handles = array();
23 private $objects = array();
24 private $projectPHIDs = array();
25 private $markupFieldOutput = array();
27 /* -( Loading Stories )---------------------------------------------------- */
31 * Given @{class:PhabricatorFeedStoryData} rows, load them into objects and
32 * construct appropriate @{class:PhabricatorFeedStory} wrappers for each
35 * @param list<dict> List of @{class:PhabricatorFeedStoryData} rows from the
37 * @return list<PhabricatorFeedStory> List of @{class:PhabricatorFeedStory}
41 public static function loadAllFromRows(array $rows, PhabricatorUser
$viewer) {
44 $data = id(new PhabricatorFeedStoryData())->loadAllFromArray($rows);
45 foreach ($data as $story_data) {
46 $class = $story_data->getStoryType();
50 class_exists($class) &&
51 is_subclass_of($class, __CLASS__
);
52 } catch (PhutilMissingSymbolException
$ex) {
56 // If the story type isn't a valid class or isn't a subclass of
57 // PhabricatorFeedStory, decline to load it.
62 $key = $story_data->getChronologicalKey();
63 $stories[$key] = newv($class, array($story_data));
66 $object_phids = array();
68 foreach ($stories as $key => $story) {
70 foreach ($story->getRequiredObjectPHIDs() as $phid) {
73 if ($story->getPrimaryObjectPHID()) {
74 $phids[$story->getPrimaryObjectPHID()] = true;
76 $key_phids[$key] = $phids;
77 $object_phids +
= $phids;
80 $object_query = id(new PhabricatorObjectQuery())
82 ->withPHIDs(array_keys($object_phids));
84 $objects = $object_query->execute();
86 foreach ($key_phids as $key => $phids) {
90 $story_objects = array_select_keys($objects, array_keys($phids));
91 if (count($story_objects) != count($phids)) {
92 // An object this story requires either does not exist or is not visible
93 // to the user. Decline to render the story.
94 unset($stories[$key]);
95 unset($key_phids[$key]);
99 $stories[$key]->setObjects($story_objects);
102 // If stories are about PhabricatorProjectInterface objects, load the
103 // projects the objects are a part of so we can render project tags
106 $project_phids = array();
107 foreach ($objects as $object) {
108 if ($object instanceof PhabricatorProjectInterface
) {
109 $project_phids[$object->getPHID()] = array();
113 if ($project_phids) {
114 $edge_query = id(new PhabricatorEdgeQuery())
115 ->withSourcePHIDs(array_keys($project_phids))
118 PhabricatorProjectObjectHasProjectEdgeType
::EDGECONST
,
120 $edge_query->execute();
121 foreach ($project_phids as $phid => $ignored) {
122 $project_phids[$phid] = $edge_query->getDestinationPHIDs(array($phid));
126 $handle_phids = array();
127 foreach ($stories as $key => $story) {
128 foreach ($story->getRequiredHandlePHIDs() as $phid) {
129 $key_phids[$key][$phid] = true;
131 if ($story->getAuthorPHID()) {
132 $key_phids[$key][$story->getAuthorPHID()] = true;
135 $object_phid = $story->getPrimaryObjectPHID();
136 $object_project_phids = idx($project_phids, $object_phid, array());
137 $story->setProjectPHIDs($object_project_phids);
138 foreach ($object_project_phids as $dst) {
139 $key_phids[$key][$dst] = true;
142 $handle_phids +
= $key_phids[$key];
145 // NOTE: This setParentQuery() is a little sketchy. Ideally, this whole
146 // method should be inside FeedQuery and it should be the parent query of
147 // both subqueries. We're just trying to share the workspace cache.
149 $handles = id(new PhabricatorHandleQuery())
151 ->setParentQuery($object_query)
152 ->withPHIDs(array_keys($handle_phids))
155 foreach ($key_phids as $key => $phids) {
159 $story_handles = array_select_keys($handles, array_keys($phids));
160 $stories[$key]->setHandles($story_handles);
163 // Load and process story markup blocks.
165 $engine = new PhabricatorMarkupEngine();
166 $engine->setViewer($viewer);
167 foreach ($stories as $story) {
168 foreach ($story->getFieldStoryMarkupFields() as $field) {
169 $engine->addObject($story, $field);
175 foreach ($stories as $story) {
176 foreach ($story->getFieldStoryMarkupFields() as $field) {
177 $story->setMarkupFieldOutput(
179 $engine->getOutput($story, $field));
186 public function setMarkupFieldOutput($field, $output) {
187 $this->markupFieldOutput
[$field] = $output;
191 public function getMarkupFieldOutput($field) {
192 if (!array_key_exists($field, $this->markupFieldOutput
)) {
195 'Trying to retrieve markup field key "%s", but this feed story '.
196 'did not request it be rendered.',
200 return $this->markupFieldOutput
[$field];
203 public function setHovercard($hover) {
204 $this->hovercard
= $hover;
208 public function setRenderingTarget($target) {
209 $this->validateRenderingTarget($target);
210 $this->renderingTarget
= $target;
214 public function getRenderingTarget() {
215 return $this->renderingTarget
;
218 private function validateRenderingTarget($target) {
220 case PhabricatorApplicationTransaction
::TARGET_HTML
:
221 case PhabricatorApplicationTransaction
::TARGET_TEXT
:
224 throw new Exception(pht('Unknown rendering target: %s', $target));
229 public function setObjects(array $objects) {
230 $this->objects
= $objects;
234 public function getObject($phid) {
235 $object = idx($this->objects
, $phid);
239 "Story is asking for an object it did not request ('%s')!",
245 public function getPrimaryObject() {
246 $phid = $this->getPrimaryObjectPHID();
248 throw new Exception(pht('Story has no primary object!'));
250 return $this->getObject($phid);
253 public function getPrimaryObjectPHID() {
257 final public function __construct(PhabricatorFeedStoryData
$data) {
261 abstract public function renderView();
262 public function renderAsTextForDoorkeeper(
263 DoorkeeperFeedStoryPublisher
$publisher) {
265 // TODO: This (and text rendering) should be properly abstract and
266 // universal. However, this is far less bad than it used to be, and we
267 // need to clean up more old feed code to really make this reasonable.
270 '(Unable to render story of class %s for Doorkeeper.)',
274 public function getRequiredHandlePHIDs() {
278 public function getRequiredObjectPHIDs() {
282 public function setHasViewed($has_viewed) {
283 $this->hasViewed
= $has_viewed;
287 public function getHasViewed() {
288 return $this->hasViewed
;
291 final public function setHandles(array $handles) {
292 assert_instances_of($handles, 'PhabricatorObjectHandle');
293 $this->handles
= $handles;
297 final protected function getObjects() {
298 return $this->objects
;
301 final protected function getHandles() {
302 return $this->handles
;
305 final protected function getHandle($phid) {
306 if (isset($this->handles
[$phid])) {
307 if ($this->handles
[$phid] instanceof PhabricatorObjectHandle
) {
308 return $this->handles
[$phid];
312 $handle = new PhabricatorObjectHandle();
313 $handle->setPHID($phid);
314 $handle->setName(pht("Unloaded Object '%s'", $phid));
319 final public function getStoryData() {
323 final public function getEpoch() {
324 return $this->getStoryData()->getEpoch();
327 final public function getChronologicalKey() {
328 return $this->getStoryData()->getChronologicalKey();
331 final public function getValue($key, $default = null) {
332 return $this->getStoryData()->getValue($key, $default);
335 final public function getAuthorPHID() {
336 return $this->getStoryData()->getAuthorPHID();
339 final protected function renderHandleList(array $phids) {
341 foreach ($phids as $phid) {
342 $items[] = $this->linkTo($phid);
345 switch ($this->getRenderingTarget()) {
346 case PhabricatorApplicationTransaction
::TARGET_TEXT
:
347 $list = implode(', ', $items);
349 case PhabricatorApplicationTransaction
::TARGET_HTML
:
350 $list = phutil_implode_html(', ', $items);
356 final protected function linkTo($phid) {
357 $handle = $this->getHandle($phid);
359 switch ($this->getRenderingTarget()) {
360 case PhabricatorApplicationTransaction
::TARGET_TEXT
:
361 return $handle->getLinkName();
364 return $handle->renderLink();
367 final protected function renderString($str) {
368 switch ($this->getRenderingTarget()) {
369 case PhabricatorApplicationTransaction
::TARGET_TEXT
:
371 case PhabricatorApplicationTransaction
::TARGET_HTML
:
372 return phutil_tag('strong', array(), $str);
376 final public function renderSummary($text, $len = 128) {
378 $text = id(new PhutilUTF8StringTruncator())
379 ->setMaximumGlyphs($len)
380 ->truncateString($text);
382 switch ($this->getRenderingTarget()) {
383 case PhabricatorApplicationTransaction
::TARGET_HTML
:
384 $text = phutil_escape_html_newlines($text);
390 public function getNotificationAggregations() {
394 protected function newStoryView() {
395 $view = id(new PHUIFeedStoryView())
396 ->setChronologicalKey($this->getChronologicalKey())
397 ->setEpoch($this->getEpoch())
398 ->setViewed($this->getHasViewed());
400 $project_phids = $this->getProjectPHIDs();
401 if ($project_phids) {
402 $view->setTags($this->renderHandleList($project_phids));
408 public function setProjectPHIDs(array $phids) {
409 $this->projectPHIDs
= $phids;
413 public function getProjectPHIDs() {
414 return $this->projectPHIDs
;
417 public function getFieldStoryMarkupFields() {
421 public function isVisibleInFeed() {
425 public function isVisibleInNotifications() {
430 /* -( PhabricatorPolicyInterface Implementation )-------------------------- */
432 public function getPHID() {
439 public function getCapabilities() {
441 PhabricatorPolicyCapability
::CAN_VIEW
,
449 public function getPolicy($capability) {
450 // NOTE: We enforce that a user can see all the objects a story is about
451 // when loading it, so we don't need to perform a equivalent secondary
452 // policy check later.
453 return PhabricatorPolicies
::getMostOpenPolicy();
460 public function hasAutomaticCapability($capability, PhabricatorUser
$viewer) {
465 /* -( PhabricatorMarkupInterface Implementation )--------------------------- */
468 public function getMarkupFieldKey($field) {
469 return 'feed:'.$this->getChronologicalKey().':'.$field;
472 public function newMarkupEngine($field) {
473 return PhabricatorMarkupEngine
::getEngine('feed');
476 public function getMarkupText($field) {
477 throw new PhutilMethodNotImplementedException();
480 public function didMarkupText(
483 PhutilMarkupEngine
$engine) {
487 public function shouldUseMarkupCache($field) {