[ Index ] |
PHP Cross Reference of Phabricator |
[Summary view] [Print] [Text view]
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 implements 13 PhabricatorPolicyInterface, 14 PhabricatorMarkupInterface { 15 16 private $data; 17 private $hasViewed; 18 private $framed; 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, 'PhabricatorFeedStory'); 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 $objects = id(new PhabricatorObjectQuery()) 81 ->setViewer($viewer) 82 ->withPHIDs(array_keys($object_phids)) 83 ->execute(); 84 85 foreach ($key_phids as $key => $phids) { 86 if (!$phids) { 87 continue; 88 } 89 $story_objects = array_select_keys($objects, array_keys($phids)); 90 if (count($story_objects) != count($phids)) { 91 // An object this story requires either does not exist or is not visible 92 // to the user. Decline to render the story. 93 unset($stories[$key]); 94 unset($key_phids[$key]); 95 continue; 96 } 97 98 $stories[$key]->setObjects($story_objects); 99 } 100 101 // If stories are about PhabricatorProjectInterface objects, load the 102 // projects the objects are a part of so we can render project tags 103 // on the stories. 104 105 $project_phids = array(); 106 foreach ($objects as $object) { 107 if ($object instanceof PhabricatorProjectInterface) { 108 $project_phids[$object->getPHID()] = array(); 109 } 110 } 111 112 if ($project_phids) { 113 $edge_query = id(new PhabricatorEdgeQuery()) 114 ->withSourcePHIDs(array_keys($project_phids)) 115 ->withEdgeTypes( 116 array( 117 PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, 118 )); 119 $edge_query->execute(); 120 foreach ($project_phids as $phid => $ignored) { 121 $project_phids[$phid] = $edge_query->getDestinationPHIDs(array($phid)); 122 } 123 } 124 125 $handle_phids = array(); 126 foreach ($stories as $key => $story) { 127 foreach ($story->getRequiredHandlePHIDs() as $phid) { 128 $key_phids[$key][$phid] = true; 129 } 130 if ($story->getAuthorPHID()) { 131 $key_phids[$key][$story->getAuthorPHID()] = true; 132 } 133 134 $object_phid = $story->getPrimaryObjectPHID(); 135 $object_project_phids = idx($project_phids, $object_phid, array()); 136 $story->setProjectPHIDs($object_project_phids); 137 foreach ($object_project_phids as $dst) { 138 $key_phids[$key][$dst] = true; 139 } 140 141 $handle_phids += $key_phids[$key]; 142 } 143 144 $handles = id(new PhabricatorHandleQuery()) 145 ->setViewer($viewer) 146 ->withPHIDs(array_keys($handle_phids)) 147 ->execute(); 148 149 foreach ($key_phids as $key => $phids) { 150 if (!$phids) { 151 continue; 152 } 153 $story_handles = array_select_keys($handles, array_keys($phids)); 154 $stories[$key]->setHandles($story_handles); 155 } 156 157 // Load and process story markup blocks. 158 159 $engine = new PhabricatorMarkupEngine(); 160 $engine->setViewer($viewer); 161 foreach ($stories as $story) { 162 foreach ($story->getFieldStoryMarkupFields() as $field) { 163 $engine->addObject($story, $field); 164 } 165 } 166 167 $engine->process(); 168 169 foreach ($stories as $story) { 170 foreach ($story->getFieldStoryMarkupFields() as $field) { 171 $story->setMarkupFieldOutput( 172 $field, 173 $engine->getOutput($story, $field)); 174 } 175 } 176 177 return $stories; 178 } 179 180 public function setMarkupFieldOutput($field, $output) { 181 $this->markupFieldOutput[$field] = $output; 182 return $this; 183 } 184 185 public function getMarkupFieldOutput($field) { 186 if (!array_key_exists($field, $this->markupFieldOutput)) { 187 throw new Exception( 188 pht( 189 'Trying to retrieve markup field key "%s", but this feed story '. 190 'did not request it be rendered.', 191 $field)); 192 } 193 194 return $this->markupFieldOutput[$field]; 195 } 196 197 public function setHovercard($hover) { 198 $this->hovercard = $hover; 199 return $this; 200 } 201 202 public function setRenderingTarget($target) { 203 $this->validateRenderingTarget($target); 204 $this->renderingTarget = $target; 205 return $this; 206 } 207 208 public function getRenderingTarget() { 209 return $this->renderingTarget; 210 } 211 212 private function validateRenderingTarget($target) { 213 switch ($target) { 214 case PhabricatorApplicationTransaction::TARGET_HTML: 215 case PhabricatorApplicationTransaction::TARGET_TEXT: 216 break; 217 default: 218 throw new Exception('Unknown rendering target: '.$target); 219 break; 220 } 221 } 222 223 public function setObjects(array $objects) { 224 $this->objects = $objects; 225 return $this; 226 } 227 228 public function getObject($phid) { 229 $object = idx($this->objects, $phid); 230 if (!$object) { 231 throw new Exception( 232 "Story is asking for an object it did not request ('{$phid}')!"); 233 } 234 return $object; 235 } 236 237 public function getPrimaryObject() { 238 $phid = $this->getPrimaryObjectPHID(); 239 if (!$phid) { 240 throw new Exception('Story has no primary object!'); 241 } 242 return $this->getObject($phid); 243 } 244 245 public function getPrimaryObjectPHID() { 246 return null; 247 } 248 249 final public function __construct(PhabricatorFeedStoryData $data) { 250 $this->data = $data; 251 } 252 253 abstract public function renderView(); 254 public function renderAsTextForDoorkeeper( 255 DoorkeeperFeedStoryPublisher $publisher) { 256 257 // TODO: This (and text rendering) should be properly abstract and 258 // universal. However, this is far less bad than it used to be, and we 259 // need to clean up more old feed code to really make this reasonable. 260 261 return pht( 262 '(Unable to render story of class %s for Doorkeeper.)', 263 get_class($this)); 264 } 265 266 public function getRequiredHandlePHIDs() { 267 return array(); 268 } 269 270 public function getRequiredObjectPHIDs() { 271 return array(); 272 } 273 274 public function setHasViewed($has_viewed) { 275 $this->hasViewed = $has_viewed; 276 return $this; 277 } 278 279 public function getHasViewed() { 280 return $this->hasViewed; 281 } 282 283 final public function setFramed($framed) { 284 $this->framed = $framed; 285 return $this; 286 } 287 288 final public function setHandles(array $handles) { 289 assert_instances_of($handles, 'PhabricatorObjectHandle'); 290 $this->handles = $handles; 291 return $this; 292 } 293 294 final protected function getObjects() { 295 return $this->objects; 296 } 297 298 final protected function getHandles() { 299 return $this->handles; 300 } 301 302 final protected function getHandle($phid) { 303 if (isset($this->handles[$phid])) { 304 if ($this->handles[$phid] instanceof PhabricatorObjectHandle) { 305 return $this->handles[$phid]; 306 } 307 } 308 309 $handle = new PhabricatorObjectHandle(); 310 $handle->setPHID($phid); 311 $handle->setName("Unloaded Object '{$phid}'"); 312 313 return $handle; 314 } 315 316 final public function getStoryData() { 317 return $this->data; 318 } 319 320 final public function getEpoch() { 321 return $this->getStoryData()->getEpoch(); 322 } 323 324 final public function getChronologicalKey() { 325 return $this->getStoryData()->getChronologicalKey(); 326 } 327 328 final public function getValue($key, $default = null) { 329 return $this->getStoryData()->getValue($key, $default); 330 } 331 332 final public function getAuthorPHID() { 333 return $this->getStoryData()->getAuthorPHID(); 334 } 335 336 final protected function renderHandleList(array $phids) { 337 $items = array(); 338 foreach ($phids as $phid) { 339 $items[] = $this->linkTo($phid); 340 } 341 $list = null; 342 switch ($this->getRenderingTarget()) { 343 case PhabricatorApplicationTransaction::TARGET_TEXT: 344 $list = implode(', ', $items); 345 break; 346 case PhabricatorApplicationTransaction::TARGET_HTML: 347 $list = phutil_implode_html(', ', $items); 348 break; 349 } 350 return $list; 351 } 352 353 final protected function linkTo($phid) { 354 $handle = $this->getHandle($phid); 355 356 switch ($this->getRenderingTarget()) { 357 case PhabricatorApplicationTransaction::TARGET_TEXT: 358 return $handle->getLinkName(); 359 } 360 361 // NOTE: We render our own link here to customize the styling and add 362 // the '_top' target for framed feeds. 363 364 $class = null; 365 if ($handle->getType() == PhabricatorPeopleUserPHIDType::TYPECONST) { 366 $class = 'phui-link-person'; 367 } 368 369 return javelin_tag( 370 'a', 371 array( 372 'href' => $handle->getURI(), 373 'target' => $this->framed ? '_top' : null, 374 'sigil' => $this->hovercard ? 'hovercard' : null, 375 'meta' => $this->hovercard ? array('hoverPHID' => $phid) : null, 376 'class' => $class, 377 ), 378 $handle->getLinkName()); 379 } 380 381 final protected function renderString($str) { 382 switch ($this->getRenderingTarget()) { 383 case PhabricatorApplicationTransaction::TARGET_TEXT: 384 return $str; 385 case PhabricatorApplicationTransaction::TARGET_HTML: 386 return phutil_tag('strong', array(), $str); 387 } 388 } 389 390 final public function renderSummary($text, $len = 128) { 391 if ($len) { 392 $text = id(new PhutilUTF8StringTruncator()) 393 ->setMaximumGlyphs($len) 394 ->truncateString($text); 395 } 396 switch ($this->getRenderingTarget()) { 397 case PhabricatorApplicationTransaction::TARGET_HTML: 398 $text = phutil_escape_html_newlines($text); 399 break; 400 } 401 return $text; 402 } 403 404 public function getNotificationAggregations() { 405 return array(); 406 } 407 408 protected function newStoryView() { 409 $view = id(new PHUIFeedStoryView()) 410 ->setChronologicalKey($this->getChronologicalKey()) 411 ->setEpoch($this->getEpoch()) 412 ->setViewed($this->getHasViewed()); 413 414 $project_phids = $this->getProjectPHIDs(); 415 if ($project_phids) { 416 $view->setTags($this->renderHandleList($project_phids)); 417 } 418 419 return $view; 420 } 421 422 public function setProjectPHIDs(array $phids) { 423 $this->projectPHIDs = $phids; 424 return $this; 425 } 426 427 public function getProjectPHIDs() { 428 return $this->projectPHIDs; 429 } 430 431 public function getFieldStoryMarkupFields() { 432 return array(); 433 } 434 435 436 /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ 437 438 public function getPHID() { 439 return null; 440 } 441 442 /** 443 * @task policy 444 */ 445 public function getCapabilities() { 446 return array( 447 PhabricatorPolicyCapability::CAN_VIEW, 448 ); 449 } 450 451 452 /** 453 * @task policy 454 */ 455 public function getPolicy($capability) { 456 // If this story's primary object is a policy-aware object, use its policy 457 // to control story visiblity. 458 459 $primary_phid = $this->getPrimaryObjectPHID(); 460 if (isset($this->objects[$primary_phid])) { 461 $object = $this->objects[$primary_phid]; 462 if ($object instanceof PhabricatorPolicyInterface) { 463 return $object->getPolicy($capability); 464 } 465 } 466 467 // TODO: Remove this once all objects are policy-aware. For now, keep 468 // respecting the `feed.public` setting. 469 return PhabricatorEnv::getEnvConfig('feed.public') 470 ? PhabricatorPolicies::POLICY_PUBLIC 471 : PhabricatorPolicies::POLICY_USER; 472 } 473 474 475 /** 476 * @task policy 477 */ 478 public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { 479 return false; 480 } 481 482 public function describeAutomaticCapability($capability) { 483 return null; 484 } 485 486 487 /* -( PhabricatorMarkupInterface Implementation )--------------------------- */ 488 489 490 public function getMarkupFieldKey($field) { 491 return 'feed:'.$this->getChronologicalKey().':'.$field; 492 } 493 494 public function newMarkupEngine($field) { 495 return PhabricatorMarkupEngine::newMarkupEngine(array()); 496 } 497 498 public function getMarkupText($field) { 499 throw new PhutilMethodNotImplementedException(); 500 } 501 502 public function didMarkupText( 503 $field, 504 $output, 505 PhutilMarkupEngine $engine) { 506 return $output; 507 } 508 509 public function shouldUseMarkupCache($field) { 510 return true; 511 } 512 513 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Sun Nov 30 09:20:46 2014 | Cross-referenced by PHPXref 0.7.1 |