Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/feed/story/PhabricatorFeedStory.php
12241 views
1
<?php
2
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).
7
*
8
* @task load Loading Stories
9
* @task policy Policy Implementation
10
*/
11
abstract class PhabricatorFeedStory
12
extends Phobject
13
implements
14
PhabricatorPolicyInterface,
15
PhabricatorMarkupInterface {
16
17
private $data;
18
private $hasViewed;
19
private $hovercard = false;
20
private $renderingTarget = PhabricatorApplicationTransaction::TARGET_HTML;
21
22
private $handles = array();
23
private $objects = array();
24
private $projectPHIDs = array();
25
private $markupFieldOutput = array();
26
27
/* -( Loading Stories )---------------------------------------------------- */
28
29
30
/**
31
* Given @{class:PhabricatorFeedStoryData} rows, load them into objects and
32
* construct appropriate @{class:PhabricatorFeedStory} wrappers for each
33
* data row.
34
*
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
40
*/
41
public static function loadAllFromRows(array $rows, PhabricatorUser $viewer) {
42
$stories = array();
43
44
$data = id(new PhabricatorFeedStoryData())->loadAllFromArray($rows);
45
foreach ($data as $story_data) {
46
$class = $story_data->getStoryType();
47
48
try {
49
$ok =
50
class_exists($class) &&
51
is_subclass_of($class, __CLASS__);
52
} catch (PhutilMissingSymbolException $ex) {
53
$ok = false;
54
}
55
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;
60
}
61
62
$key = $story_data->getChronologicalKey();
63
$stories[$key] = newv($class, array($story_data));
64
}
65
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;
72
}
73
if ($story->getPrimaryObjectPHID()) {
74
$phids[$story->getPrimaryObjectPHID()] = true;
75
}
76
$key_phids[$key] = $phids;
77
$object_phids += $phids;
78
}
79
80
$object_query = id(new PhabricatorObjectQuery())
81
->setViewer($viewer)
82
->withPHIDs(array_keys($object_phids));
83
84
$objects = $object_query->execute();
85
86
foreach ($key_phids as $key => $phids) {
87
if (!$phids) {
88
continue;
89
}
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;
97
}
98
99
$stories[$key]->setObjects($story_objects);
100
}
101
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.
105
106
$project_phids = array();
107
foreach ($objects as $object) {
108
if ($object instanceof PhabricatorProjectInterface) {
109
$project_phids[$object->getPHID()] = array();
110
}
111
}
112
113
if ($project_phids) {
114
$edge_query = id(new PhabricatorEdgeQuery())
115
->withSourcePHIDs(array_keys($project_phids))
116
->withEdgeTypes(
117
array(
118
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
119
));
120
$edge_query->execute();
121
foreach ($project_phids as $phid => $ignored) {
122
$project_phids[$phid] = $edge_query->getDestinationPHIDs(array($phid));
123
}
124
}
125
126
$handle_phids = array();
127
foreach ($stories as $key => $story) {
128
foreach ($story->getRequiredHandlePHIDs() as $phid) {
129
$key_phids[$key][$phid] = true;
130
}
131
if ($story->getAuthorPHID()) {
132
$key_phids[$key][$story->getAuthorPHID()] = true;
133
}
134
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;
140
}
141
142
$handle_phids += $key_phids[$key];
143
}
144
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.
148
149
$handles = id(new PhabricatorHandleQuery())
150
->setViewer($viewer)
151
->setParentQuery($object_query)
152
->withPHIDs(array_keys($handle_phids))
153
->execute();
154
155
foreach ($key_phids as $key => $phids) {
156
if (!$phids) {
157
continue;
158
}
159
$story_handles = array_select_keys($handles, array_keys($phids));
160
$stories[$key]->setHandles($story_handles);
161
}
162
163
// Load and process story markup blocks.
164
165
$engine = new PhabricatorMarkupEngine();
166
$engine->setViewer($viewer);
167
foreach ($stories as $story) {
168
foreach ($story->getFieldStoryMarkupFields() as $field) {
169
$engine->addObject($story, $field);
170
}
171
}
172
173
$engine->process();
174
175
foreach ($stories as $story) {
176
foreach ($story->getFieldStoryMarkupFields() as $field) {
177
$story->setMarkupFieldOutput(
178
$field,
179
$engine->getOutput($story, $field));
180
}
181
}
182
183
return $stories;
184
}
185
186
public function setMarkupFieldOutput($field, $output) {
187
$this->markupFieldOutput[$field] = $output;
188
return $this;
189
}
190
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));
198
}
199
200
return $this->markupFieldOutput[$field];
201
}
202
203
public function setHovercard($hover) {
204
$this->hovercard = $hover;
205
return $this;
206
}
207
208
public function setRenderingTarget($target) {
209
$this->validateRenderingTarget($target);
210
$this->renderingTarget = $target;
211
return $this;
212
}
213
214
public function getRenderingTarget() {
215
return $this->renderingTarget;
216
}
217
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;
226
}
227
}
228
229
public function setObjects(array $objects) {
230
$this->objects = $objects;
231
return $this;
232
}
233
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));
241
}
242
return $object;
243
}
244
245
public function getPrimaryObject() {
246
$phid = $this->getPrimaryObjectPHID();
247
if (!$phid) {
248
throw new Exception(pht('Story has no primary object!'));
249
}
250
return $this->getObject($phid);
251
}
252
253
public function getPrimaryObjectPHID() {
254
return null;
255
}
256
257
final public function __construct(PhabricatorFeedStoryData $data) {
258
$this->data = $data;
259
}
260
261
abstract public function renderView();
262
public function renderAsTextForDoorkeeper(
263
DoorkeeperFeedStoryPublisher $publisher) {
264
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.
268
269
return pht(
270
'(Unable to render story of class %s for Doorkeeper.)',
271
get_class($this));
272
}
273
274
public function getRequiredHandlePHIDs() {
275
return array();
276
}
277
278
public function getRequiredObjectPHIDs() {
279
return array();
280
}
281
282
public function setHasViewed($has_viewed) {
283
$this->hasViewed = $has_viewed;
284
return $this;
285
}
286
287
public function getHasViewed() {
288
return $this->hasViewed;
289
}
290
291
final public function setHandles(array $handles) {
292
assert_instances_of($handles, 'PhabricatorObjectHandle');
293
$this->handles = $handles;
294
return $this;
295
}
296
297
final protected function getObjects() {
298
return $this->objects;
299
}
300
301
final protected function getHandles() {
302
return $this->handles;
303
}
304
305
final protected function getHandle($phid) {
306
if (isset($this->handles[$phid])) {
307
if ($this->handles[$phid] instanceof PhabricatorObjectHandle) {
308
return $this->handles[$phid];
309
}
310
}
311
312
$handle = new PhabricatorObjectHandle();
313
$handle->setPHID($phid);
314
$handle->setName(pht("Unloaded Object '%s'", $phid));
315
316
return $handle;
317
}
318
319
final public function getStoryData() {
320
return $this->data;
321
}
322
323
final public function getEpoch() {
324
return $this->getStoryData()->getEpoch();
325
}
326
327
final public function getChronologicalKey() {
328
return $this->getStoryData()->getChronologicalKey();
329
}
330
331
final public function getValue($key, $default = null) {
332
return $this->getStoryData()->getValue($key, $default);
333
}
334
335
final public function getAuthorPHID() {
336
return $this->getStoryData()->getAuthorPHID();
337
}
338
339
final protected function renderHandleList(array $phids) {
340
$items = array();
341
foreach ($phids as $phid) {
342
$items[] = $this->linkTo($phid);
343
}
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;
352
}
353
return $list;
354
}
355
356
final protected function linkTo($phid) {
357
$handle = $this->getHandle($phid);
358
359
switch ($this->getRenderingTarget()) {
360
case PhabricatorApplicationTransaction::TARGET_TEXT:
361
return $handle->getLinkName();
362
}
363
364
return $handle->renderLink();
365
}
366
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);
373
}
374
}
375
376
final public function renderSummary($text, $len = 128) {
377
if ($len) {
378
$text = id(new PhutilUTF8StringTruncator())
379
->setMaximumGlyphs($len)
380
->truncateString($text);
381
}
382
switch ($this->getRenderingTarget()) {
383
case PhabricatorApplicationTransaction::TARGET_HTML:
384
$text = phutil_escape_html_newlines($text);
385
break;
386
}
387
return $text;
388
}
389
390
public function getNotificationAggregations() {
391
return array();
392
}
393
394
protected function newStoryView() {
395
$view = id(new PHUIFeedStoryView())
396
->setChronologicalKey($this->getChronologicalKey())
397
->setEpoch($this->getEpoch())
398
->setViewed($this->getHasViewed());
399
400
$project_phids = $this->getProjectPHIDs();
401
if ($project_phids) {
402
$view->setTags($this->renderHandleList($project_phids));
403
}
404
405
return $view;
406
}
407
408
public function setProjectPHIDs(array $phids) {
409
$this->projectPHIDs = $phids;
410
return $this;
411
}
412
413
public function getProjectPHIDs() {
414
return $this->projectPHIDs;
415
}
416
417
public function getFieldStoryMarkupFields() {
418
return array();
419
}
420
421
public function isVisibleInFeed() {
422
return true;
423
}
424
425
public function isVisibleInNotifications() {
426
return true;
427
}
428
429
430
/* -( PhabricatorPolicyInterface Implementation )-------------------------- */
431
432
public function getPHID() {
433
return null;
434
}
435
436
/**
437
* @task policy
438
*/
439
public function getCapabilities() {
440
return array(
441
PhabricatorPolicyCapability::CAN_VIEW,
442
);
443
}
444
445
446
/**
447
* @task policy
448
*/
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();
454
}
455
456
457
/**
458
* @task policy
459
*/
460
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
461
return false;
462
}
463
464
465
/* -( PhabricatorMarkupInterface Implementation )--------------------------- */
466
467
468
public function getMarkupFieldKey($field) {
469
return 'feed:'.$this->getChronologicalKey().':'.$field;
470
}
471
472
public function newMarkupEngine($field) {
473
return PhabricatorMarkupEngine::getEngine('feed');
474
}
475
476
public function getMarkupText($field) {
477
throw new PhutilMethodNotImplementedException();
478
}
479
480
public function didMarkupText(
481
$field,
482
$output,
483
PhutilMarkupEngine $engine) {
484
return $output;
485
}
486
487
public function shouldUseMarkupCache($field) {
488
return true;
489
}
490
491
}
492
493