Correct a parameter order swap in "diffusion.historyquery" for Mercurial
[phabricator.git] / src / applications / feed / story / PhabricatorFeedStory.php
blobe0c65c7dc2d81262c443f05aa6e716607dfccf12
1 <?php
3 /**
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
12 extends Phobject
13 implements
14 PhabricatorPolicyInterface,
15 PhabricatorMarkupInterface {
17 private $data;
18 private $hasViewed;
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 )---------------------------------------------------- */
30 /**
31 * Given @{class:PhabricatorFeedStoryData} rows, load them into objects and
32 * construct appropriate @{class:PhabricatorFeedStory} wrappers for each
33 * data row.
35 * @param list<dict> List of @{class:PhabricatorFeedStoryData} rows from the
36 * database.
37 * @return list<PhabricatorFeedStory> List of @{class:PhabricatorFeedStory}
38 * objects.
39 * @task load
41 public static function loadAllFromRows(array $rows, PhabricatorUser $viewer) {
42 $stories = array();
44 $data = id(new PhabricatorFeedStoryData())->loadAllFromArray($rows);
45 foreach ($data as $story_data) {
46 $class = $story_data->getStoryType();
48 try {
49 $ok =
50 class_exists($class) &&
51 is_subclass_of($class, __CLASS__);
52 } catch (PhutilMissingSymbolException $ex) {
53 $ok = false;
56 // If the story type isn't a valid class or isn't a subclass of
57 // PhabricatorFeedStory, decline to load it.
58 if (!$ok) {
59 continue;
62 $key = $story_data->getChronologicalKey();
63 $stories[$key] = newv($class, array($story_data));
66 $object_phids = array();
67 $key_phids = array();
68 foreach ($stories as $key => $story) {
69 $phids = array();
70 foreach ($story->getRequiredObjectPHIDs() as $phid) {
71 $phids[$phid] = true;
73 if ($story->getPrimaryObjectPHID()) {
74 $phids[$story->getPrimaryObjectPHID()] = true;
76 $key_phids[$key] = $phids;
77 $object_phids += $phids;
80 $object_query = id(new PhabricatorObjectQuery())
81 ->setViewer($viewer)
82 ->withPHIDs(array_keys($object_phids));
84 $objects = $object_query->execute();
86 foreach ($key_phids as $key => $phids) {
87 if (!$phids) {
88 continue;
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]);
96 continue;
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
104 // on the stories.
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))
116 ->withEdgeTypes(
117 array(
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())
150 ->setViewer($viewer)
151 ->setParentQuery($object_query)
152 ->withPHIDs(array_keys($handle_phids))
153 ->execute();
155 foreach ($key_phids as $key => $phids) {
156 if (!$phids) {
157 continue;
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);
173 $engine->process();
175 foreach ($stories as $story) {
176 foreach ($story->getFieldStoryMarkupFields() as $field) {
177 $story->setMarkupFieldOutput(
178 $field,
179 $engine->getOutput($story, $field));
183 return $stories;
186 public function setMarkupFieldOutput($field, $output) {
187 $this->markupFieldOutput[$field] = $output;
188 return $this;
191 public function getMarkupFieldOutput($field) {
192 if (!array_key_exists($field, $this->markupFieldOutput)) {
193 throw new Exception(
194 pht(
195 'Trying to retrieve markup field key "%s", but this feed story '.
196 'did not request it be rendered.',
197 $field));
200 return $this->markupFieldOutput[$field];
203 public function setHovercard($hover) {
204 $this->hovercard = $hover;
205 return $this;
208 public function setRenderingTarget($target) {
209 $this->validateRenderingTarget($target);
210 $this->renderingTarget = $target;
211 return $this;
214 public function getRenderingTarget() {
215 return $this->renderingTarget;
218 private function validateRenderingTarget($target) {
219 switch ($target) {
220 case PhabricatorApplicationTransaction::TARGET_HTML:
221 case PhabricatorApplicationTransaction::TARGET_TEXT:
222 break;
223 default:
224 throw new Exception(pht('Unknown rendering target: %s', $target));
225 break;
229 public function setObjects(array $objects) {
230 $this->objects = $objects;
231 return $this;
234 public function getObject($phid) {
235 $object = idx($this->objects, $phid);
236 if (!$object) {
237 throw new Exception(
238 pht(
239 "Story is asking for an object it did not request ('%s')!",
240 $phid));
242 return $object;
245 public function getPrimaryObject() {
246 $phid = $this->getPrimaryObjectPHID();
247 if (!$phid) {
248 throw new Exception(pht('Story has no primary object!'));
250 return $this->getObject($phid);
253 public function getPrimaryObjectPHID() {
254 return null;
257 final public function __construct(PhabricatorFeedStoryData $data) {
258 $this->data = $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.
269 return pht(
270 '(Unable to render story of class %s for Doorkeeper.)',
271 get_class($this));
274 public function getRequiredHandlePHIDs() {
275 return array();
278 public function getRequiredObjectPHIDs() {
279 return array();
282 public function setHasViewed($has_viewed) {
283 $this->hasViewed = $has_viewed;
284 return $this;
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;
294 return $this;
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));
316 return $handle;
319 final public function getStoryData() {
320 return $this->data;
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) {
340 $items = array();
341 foreach ($phids as $phid) {
342 $items[] = $this->linkTo($phid);
344 $list = null;
345 switch ($this->getRenderingTarget()) {
346 case PhabricatorApplicationTransaction::TARGET_TEXT:
347 $list = implode(', ', $items);
348 break;
349 case PhabricatorApplicationTransaction::TARGET_HTML:
350 $list = phutil_implode_html(', ', $items);
351 break;
353 return $list;
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:
370 return $str;
371 case PhabricatorApplicationTransaction::TARGET_HTML:
372 return phutil_tag('strong', array(), $str);
376 final public function renderSummary($text, $len = 128) {
377 if ($len) {
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);
385 break;
387 return $text;
390 public function getNotificationAggregations() {
391 return array();
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));
405 return $view;
408 public function setProjectPHIDs(array $phids) {
409 $this->projectPHIDs = $phids;
410 return $this;
413 public function getProjectPHIDs() {
414 return $this->projectPHIDs;
417 public function getFieldStoryMarkupFields() {
418 return array();
421 public function isVisibleInFeed() {
422 return true;
425 public function isVisibleInNotifications() {
426 return true;
430 /* -( PhabricatorPolicyInterface Implementation )-------------------------- */
432 public function getPHID() {
433 return null;
437 * @task policy
439 public function getCapabilities() {
440 return array(
441 PhabricatorPolicyCapability::CAN_VIEW,
447 * @task policy
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();
458 * @task policy
460 public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
461 return false;
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(
481 $field,
482 $output,
483 PhutilMarkupEngine $engine) {
484 return $output;
487 public function shouldUseMarkupCache($field) {
488 return true;