[ Index ]

PHP Cross Reference of Phabricator

title

Body

[close]

/src/applications/feed/story/ -> PhabricatorFeedStory.php (source)

   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  }


Generated: Sun Nov 30 09:20:46 2014 Cross-referenced by PHPXref 0.7.1