Path: blob/master/src/applications/feed/story/PhabricatorFeedStory.php
12241 views
<?php12/**3* Manages rendering and aggregation of a story. A story is an event (like a4* user adding a comment) which may be represented in different forms on5* different channels (like feed, notifications and realtime alerts).6*7* @task load Loading Stories8* @task policy Policy Implementation9*/10abstract class PhabricatorFeedStory11extends Phobject12implements13PhabricatorPolicyInterface,14PhabricatorMarkupInterface {1516private $data;17private $hasViewed;18private $hovercard = false;19private $renderingTarget = PhabricatorApplicationTransaction::TARGET_HTML;2021private $handles = array();22private $objects = array();23private $projectPHIDs = array();24private $markupFieldOutput = array();2526/* -( Loading Stories )---------------------------------------------------- */272829/**30* Given @{class:PhabricatorFeedStoryData} rows, load them into objects and31* construct appropriate @{class:PhabricatorFeedStory} wrappers for each32* data row.33*34* @param list<dict> List of @{class:PhabricatorFeedStoryData} rows from the35* database.36* @return list<PhabricatorFeedStory> List of @{class:PhabricatorFeedStory}37* objects.38* @task load39*/40public static function loadAllFromRows(array $rows, PhabricatorUser $viewer) {41$stories = array();4243$data = id(new PhabricatorFeedStoryData())->loadAllFromArray($rows);44foreach ($data as $story_data) {45$class = $story_data->getStoryType();4647try {48$ok =49class_exists($class) &&50is_subclass_of($class, __CLASS__);51} catch (PhutilMissingSymbolException $ex) {52$ok = false;53}5455// If the story type isn't a valid class or isn't a subclass of56// PhabricatorFeedStory, decline to load it.57if (!$ok) {58continue;59}6061$key = $story_data->getChronologicalKey();62$stories[$key] = newv($class, array($story_data));63}6465$object_phids = array();66$key_phids = array();67foreach ($stories as $key => $story) {68$phids = array();69foreach ($story->getRequiredObjectPHIDs() as $phid) {70$phids[$phid] = true;71}72if ($story->getPrimaryObjectPHID()) {73$phids[$story->getPrimaryObjectPHID()] = true;74}75$key_phids[$key] = $phids;76$object_phids += $phids;77}7879$object_query = id(new PhabricatorObjectQuery())80->setViewer($viewer)81->withPHIDs(array_keys($object_phids));8283$objects = $object_query->execute();8485foreach ($key_phids as $key => $phids) {86if (!$phids) {87continue;88}89$story_objects = array_select_keys($objects, array_keys($phids));90if (count($story_objects) != count($phids)) {91// An object this story requires either does not exist or is not visible92// to the user. Decline to render the story.93unset($stories[$key]);94unset($key_phids[$key]);95continue;96}9798$stories[$key]->setObjects($story_objects);99}100101// If stories are about PhabricatorProjectInterface objects, load the102// projects the objects are a part of so we can render project tags103// on the stories.104105$project_phids = array();106foreach ($objects as $object) {107if ($object instanceof PhabricatorProjectInterface) {108$project_phids[$object->getPHID()] = array();109}110}111112if ($project_phids) {113$edge_query = id(new PhabricatorEdgeQuery())114->withSourcePHIDs(array_keys($project_phids))115->withEdgeTypes(116array(117PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,118));119$edge_query->execute();120foreach ($project_phids as $phid => $ignored) {121$project_phids[$phid] = $edge_query->getDestinationPHIDs(array($phid));122}123}124125$handle_phids = array();126foreach ($stories as $key => $story) {127foreach ($story->getRequiredHandlePHIDs() as $phid) {128$key_phids[$key][$phid] = true;129}130if ($story->getAuthorPHID()) {131$key_phids[$key][$story->getAuthorPHID()] = true;132}133134$object_phid = $story->getPrimaryObjectPHID();135$object_project_phids = idx($project_phids, $object_phid, array());136$story->setProjectPHIDs($object_project_phids);137foreach ($object_project_phids as $dst) {138$key_phids[$key][$dst] = true;139}140141$handle_phids += $key_phids[$key];142}143144// NOTE: This setParentQuery() is a little sketchy. Ideally, this whole145// method should be inside FeedQuery and it should be the parent query of146// both subqueries. We're just trying to share the workspace cache.147148$handles = id(new PhabricatorHandleQuery())149->setViewer($viewer)150->setParentQuery($object_query)151->withPHIDs(array_keys($handle_phids))152->execute();153154foreach ($key_phids as $key => $phids) {155if (!$phids) {156continue;157}158$story_handles = array_select_keys($handles, array_keys($phids));159$stories[$key]->setHandles($story_handles);160}161162// Load and process story markup blocks.163164$engine = new PhabricatorMarkupEngine();165$engine->setViewer($viewer);166foreach ($stories as $story) {167foreach ($story->getFieldStoryMarkupFields() as $field) {168$engine->addObject($story, $field);169}170}171172$engine->process();173174foreach ($stories as $story) {175foreach ($story->getFieldStoryMarkupFields() as $field) {176$story->setMarkupFieldOutput(177$field,178$engine->getOutput($story, $field));179}180}181182return $stories;183}184185public function setMarkupFieldOutput($field, $output) {186$this->markupFieldOutput[$field] = $output;187return $this;188}189190public function getMarkupFieldOutput($field) {191if (!array_key_exists($field, $this->markupFieldOutput)) {192throw new Exception(193pht(194'Trying to retrieve markup field key "%s", but this feed story '.195'did not request it be rendered.',196$field));197}198199return $this->markupFieldOutput[$field];200}201202public function setHovercard($hover) {203$this->hovercard = $hover;204return $this;205}206207public function setRenderingTarget($target) {208$this->validateRenderingTarget($target);209$this->renderingTarget = $target;210return $this;211}212213public function getRenderingTarget() {214return $this->renderingTarget;215}216217private function validateRenderingTarget($target) {218switch ($target) {219case PhabricatorApplicationTransaction::TARGET_HTML:220case PhabricatorApplicationTransaction::TARGET_TEXT:221break;222default:223throw new Exception(pht('Unknown rendering target: %s', $target));224break;225}226}227228public function setObjects(array $objects) {229$this->objects = $objects;230return $this;231}232233public function getObject($phid) {234$object = idx($this->objects, $phid);235if (!$object) {236throw new Exception(237pht(238"Story is asking for an object it did not request ('%s')!",239$phid));240}241return $object;242}243244public function getPrimaryObject() {245$phid = $this->getPrimaryObjectPHID();246if (!$phid) {247throw new Exception(pht('Story has no primary object!'));248}249return $this->getObject($phid);250}251252public function getPrimaryObjectPHID() {253return null;254}255256final public function __construct(PhabricatorFeedStoryData $data) {257$this->data = $data;258}259260abstract public function renderView();261public function renderAsTextForDoorkeeper(262DoorkeeperFeedStoryPublisher $publisher) {263264// TODO: This (and text rendering) should be properly abstract and265// universal. However, this is far less bad than it used to be, and we266// need to clean up more old feed code to really make this reasonable.267268return pht(269'(Unable to render story of class %s for Doorkeeper.)',270get_class($this));271}272273public function getRequiredHandlePHIDs() {274return array();275}276277public function getRequiredObjectPHIDs() {278return array();279}280281public function setHasViewed($has_viewed) {282$this->hasViewed = $has_viewed;283return $this;284}285286public function getHasViewed() {287return $this->hasViewed;288}289290final public function setHandles(array $handles) {291assert_instances_of($handles, 'PhabricatorObjectHandle');292$this->handles = $handles;293return $this;294}295296final protected function getObjects() {297return $this->objects;298}299300final protected function getHandles() {301return $this->handles;302}303304final protected function getHandle($phid) {305if (isset($this->handles[$phid])) {306if ($this->handles[$phid] instanceof PhabricatorObjectHandle) {307return $this->handles[$phid];308}309}310311$handle = new PhabricatorObjectHandle();312$handle->setPHID($phid);313$handle->setName(pht("Unloaded Object '%s'", $phid));314315return $handle;316}317318final public function getStoryData() {319return $this->data;320}321322final public function getEpoch() {323return $this->getStoryData()->getEpoch();324}325326final public function getChronologicalKey() {327return $this->getStoryData()->getChronologicalKey();328}329330final public function getValue($key, $default = null) {331return $this->getStoryData()->getValue($key, $default);332}333334final public function getAuthorPHID() {335return $this->getStoryData()->getAuthorPHID();336}337338final protected function renderHandleList(array $phids) {339$items = array();340foreach ($phids as $phid) {341$items[] = $this->linkTo($phid);342}343$list = null;344switch ($this->getRenderingTarget()) {345case PhabricatorApplicationTransaction::TARGET_TEXT:346$list = implode(', ', $items);347break;348case PhabricatorApplicationTransaction::TARGET_HTML:349$list = phutil_implode_html(', ', $items);350break;351}352return $list;353}354355final protected function linkTo($phid) {356$handle = $this->getHandle($phid);357358switch ($this->getRenderingTarget()) {359case PhabricatorApplicationTransaction::TARGET_TEXT:360return $handle->getLinkName();361}362363return $handle->renderLink();364}365366final protected function renderString($str) {367switch ($this->getRenderingTarget()) {368case PhabricatorApplicationTransaction::TARGET_TEXT:369return $str;370case PhabricatorApplicationTransaction::TARGET_HTML:371return phutil_tag('strong', array(), $str);372}373}374375final public function renderSummary($text, $len = 128) {376if ($len) {377$text = id(new PhutilUTF8StringTruncator())378->setMaximumGlyphs($len)379->truncateString($text);380}381switch ($this->getRenderingTarget()) {382case PhabricatorApplicationTransaction::TARGET_HTML:383$text = phutil_escape_html_newlines($text);384break;385}386return $text;387}388389public function getNotificationAggregations() {390return array();391}392393protected function newStoryView() {394$view = id(new PHUIFeedStoryView())395->setChronologicalKey($this->getChronologicalKey())396->setEpoch($this->getEpoch())397->setViewed($this->getHasViewed());398399$project_phids = $this->getProjectPHIDs();400if ($project_phids) {401$view->setTags($this->renderHandleList($project_phids));402}403404return $view;405}406407public function setProjectPHIDs(array $phids) {408$this->projectPHIDs = $phids;409return $this;410}411412public function getProjectPHIDs() {413return $this->projectPHIDs;414}415416public function getFieldStoryMarkupFields() {417return array();418}419420public function isVisibleInFeed() {421return true;422}423424public function isVisibleInNotifications() {425return true;426}427428429/* -( PhabricatorPolicyInterface Implementation )-------------------------- */430431public function getPHID() {432return null;433}434435/**436* @task policy437*/438public function getCapabilities() {439return array(440PhabricatorPolicyCapability::CAN_VIEW,441);442}443444445/**446* @task policy447*/448public function getPolicy($capability) {449// NOTE: We enforce that a user can see all the objects a story is about450// when loading it, so we don't need to perform a equivalent secondary451// policy check later.452return PhabricatorPolicies::getMostOpenPolicy();453}454455456/**457* @task policy458*/459public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {460return false;461}462463464/* -( PhabricatorMarkupInterface Implementation )--------------------------- */465466467public function getMarkupFieldKey($field) {468return 'feed:'.$this->getChronologicalKey().':'.$field;469}470471public function newMarkupEngine($field) {472return PhabricatorMarkupEngine::getEngine('feed');473}474475public function getMarkupText($field) {476throw new PhutilMethodNotImplementedException();477}478479public function didMarkupText(480$field,481$output,482PhutilMarkupEngine $engine) {483return $output;484}485486public function shouldUseMarkupCache($field) {487return true;488}489490}491492493