Path: blob/master/src/infrastructure/markup/rule/PhabricatorObjectRemarkupRule.php
12241 views
<?php12abstract class PhabricatorObjectRemarkupRule extends PhutilRemarkupRule {34private $referencePattern;5private $embedPattern;67const KEY_RULE_OBJECT = 'rule.object';8const KEY_MENTIONED_OBJECTS = 'rule.object.mentioned';910abstract protected function getObjectNamePrefix();11abstract protected function loadObjects(array $ids);1213public function getPriority() {14return 450.0;15}1617protected function getObjectNamePrefixBeginsWithWordCharacter() {18$prefix = $this->getObjectNamePrefix();19return preg_match('/^\w/', $prefix);20}2122protected function getObjectIDPattern() {23return '[1-9]\d*';24}2526protected function shouldMarkupObject(array $params) {27return true;28}2930protected function getObjectNameText(31$object,32PhabricatorObjectHandle $handle,33$id) {34return $this->getObjectNamePrefix().$id;35}3637protected function loadHandles(array $objects) {38$phids = mpull($objects, 'getPHID');3940$viewer = $this->getEngine()->getConfig('viewer');41$handles = $viewer->loadHandles($phids);42$handles = iterator_to_array($handles);4344$result = array();45foreach ($objects as $id => $object) {46$result[$id] = $handles[$object->getPHID()];47}48return $result;49}5051protected function getObjectHref(52$object,53PhabricatorObjectHandle $handle,54$id) {5556$uri = $handle->getURI();5758if ($this->getEngine()->getConfig('uri.full')) {59$uri = PhabricatorEnv::getURI($uri);60}6162return $uri;63}6465protected function renderObjectRefForAnyMedia(66$object,67PhabricatorObjectHandle $handle,68$anchor,69$id) {7071$href = $this->getObjectHref($object, $handle, $id);72$text = $this->getObjectNameText($object, $handle, $id);7374if ($anchor) {75$href = $href.'#'.$anchor;76$text = $text.'#'.$anchor;77}7879if ($this->getEngine()->isTextMode()) {80return $text.' <'.PhabricatorEnv::getProductionURI($href).'>';81} else if ($this->getEngine()->isHTMLMailMode()) {82$href = PhabricatorEnv::getProductionURI($href);83return $this->renderObjectTagForMail($text, $href, $handle);84}8586return $this->renderObjectRef($object, $handle, $anchor, $id);8788}8990protected function renderObjectRef(91$object,92PhabricatorObjectHandle $handle,93$anchor,94$id) {9596$href = $this->getObjectHref($object, $handle, $id);97$text = $this->getObjectNameText($object, $handle, $id);98$status_closed = PhabricatorObjectHandle::STATUS_CLOSED;99100if ($anchor) {101$href = $href.'#'.$anchor;102$text = $text.'#'.$anchor;103}104105$attr = array(106'phid' => $handle->getPHID(),107'closed' => ($handle->getStatus() == $status_closed),108);109110return $this->renderHovertag($text, $href, $attr);111}112113protected function renderObjectEmbedForAnyMedia(114$object,115PhabricatorObjectHandle $handle,116$options) {117118$name = $handle->getFullName();119$href = $handle->getURI();120121if ($this->getEngine()->isTextMode()) {122return $name.' <'.PhabricatorEnv::getProductionURI($href).'>';123} else if ($this->getEngine()->isHTMLMailMode()) {124$href = PhabricatorEnv::getProductionURI($href);125return $this->renderObjectTagForMail($name, $href, $handle);126}127128// See T13678. If we're already rendering embedded content, render a129// default reference instead to avoid cycles.130if (PhabricatorMarkupEngine::isRenderingEmbeddedContent()) {131return $this->renderDefaultObjectEmbed($object, $handle);132}133134return $this->renderObjectEmbed($object, $handle, $options);135}136137protected function renderObjectEmbed(138$object,139PhabricatorObjectHandle $handle,140$options) {141return $this->renderDefaultObjectEmbed($object, $handle);142}143144final protected function renderDefaultObjectEmbed(145$object,146PhabricatorObjectHandle $handle) {147148$name = $handle->getFullName();149$href = $handle->getURI();150$status_closed = PhabricatorObjectHandle::STATUS_CLOSED;151$attr = array(152'phid' => $handle->getPHID(),153'closed' => ($handle->getStatus() == $status_closed),154);155156return $this->renderHovertag($name, $href, $attr);157}158159protected function renderObjectTagForMail(160$text,161$href,162PhabricatorObjectHandle $handle) {163164$status_closed = PhabricatorObjectHandle::STATUS_CLOSED;165$strikethrough = $handle->getStatus() == $status_closed ?166'text-decoration: line-through;' :167'text-decoration: none;';168169return phutil_tag(170'a',171array(172'href' => $href,173'style' => 'background-color: #e7e7e7;174border-color: #e7e7e7;175border-radius: 3px;176padding: 0 4px;177font-weight: bold;178color: black;'179.$strikethrough,180),181$text);182}183184protected function renderHovertag($name, $href, array $attr = array()) {185return id(new PHUITagView())186->setName($name)187->setHref($href)188->setType(PHUITagView::TYPE_OBJECT)189->setPHID(idx($attr, 'phid'))190->setClosed(idx($attr, 'closed'))191->render();192}193194public function apply($text) {195$text = preg_replace_callback(196$this->getObjectEmbedPattern(),197array($this, 'markupObjectEmbed'),198$text);199200$text = preg_replace_callback(201$this->getObjectReferencePattern(),202array($this, 'markupObjectReference'),203$text);204205return $text;206}207208private function getObjectEmbedPattern() {209if ($this->embedPattern === null) {210$prefix = $this->getObjectNamePrefix();211$prefix = preg_quote($prefix);212$id = $this->getObjectIDPattern();213214$this->embedPattern =215'(\B{'.$prefix.'('.$id.')([,\s](?:[^}\\\\]|\\\\.)*)?}\B)u';216}217218return $this->embedPattern;219}220221private function getObjectReferencePattern() {222if ($this->referencePattern === null) {223$prefix = $this->getObjectNamePrefix();224$prefix = preg_quote($prefix);225226$id = $this->getObjectIDPattern();227228// If the prefix starts with a word character (like "D"), we want to229// require a word boundary so that we don't match "XD1" as "D1". If the230// prefix does not start with a word character, we want to require no word231// boundary for the same reasons. Test if the prefix starts with a word232// character.233if ($this->getObjectNamePrefixBeginsWithWordCharacter()) {234$boundary = '\\b';235} else {236$boundary = '\\B';237}238239// The "(?<![#@-])" prevents us from linking "#abcdef" or similar, and240// "ABC-T1" (see T5714), and from matching "@T1" as a task (it is a user)241// (see T9479).242243// The "\b" allows us to link "(abcdef)" or similar without linking things244// in the middle of words.245246$this->referencePattern =247'((?<![#@-])'.$boundary.$prefix.'('.$id.')(?:#([-\w\d]+))?(?!\w))u';248}249250return $this->referencePattern;251}252253254/**255* Extract matched object references from a block of text.256*257* This is intended to make it easy to write unit tests for object remarkup258* rules. Production code is not normally expected to call this method.259*260* @param string Text to match rules against.261* @return wild Matches, suitable for writing unit tests against.262*/263public function extractReferences($text) {264$embed_matches = null;265preg_match_all(266$this->getObjectEmbedPattern(),267$text,268$embed_matches,269PREG_OFFSET_CAPTURE | PREG_SET_ORDER);270271$ref_matches = null;272preg_match_all(273$this->getObjectReferencePattern(),274$text,275$ref_matches,276PREG_OFFSET_CAPTURE | PREG_SET_ORDER);277278$results = array();279$sets = array(280'embed' => $embed_matches,281'ref' => $ref_matches,282);283foreach ($sets as $type => $matches) {284$formatted = array();285foreach ($matches as $match) {286$format = array(287'offset' => $match[1][1],288'id' => $match[1][0],289);290if (isset($match[2][0])) {291$format['tail'] = $match[2][0];292}293$formatted[] = $format;294}295$results[$type] = $formatted;296}297298return $results;299}300301public function markupObjectEmbed(array $matches) {302if (!$this->isFlatText($matches[0])) {303return $matches[0];304}305306// If we're rendering a table of contents, just render the raw input.307// This could perhaps be handled more gracefully but it seems unusual to308// put something like "{P123}" in a header and it's not obvious what users309// expect? See T8845.310$engine = $this->getEngine();311if ($engine->getState('toc')) {312return $matches[0];313}314315return $this->markupObject(array(316'type' => 'embed',317'id' => $matches[1],318'options' => idx($matches, 2),319'original' => $matches[0],320'quote.depth' => $engine->getQuoteDepth(),321));322}323324public function markupObjectReference(array $matches) {325if (!$this->isFlatText($matches[0])) {326return $matches[0];327}328329// If we're rendering a table of contents, just render the monogram.330$engine = $this->getEngine();331if ($engine->getState('toc')) {332return $matches[0];333}334335return $this->markupObject(array(336'type' => 'ref',337'id' => $matches[1],338'anchor' => idx($matches, 2),339'original' => $matches[0],340'quote.depth' => $engine->getQuoteDepth(),341));342}343344private function markupObject(array $params) {345if (!$this->shouldMarkupObject($params)) {346return $params['original'];347}348349$regex = trim(350PhabricatorEnv::getEnvConfig('remarkup.ignored-object-names'));351if ($regex && preg_match($regex, $params['original'])) {352return $params['original'];353}354355$engine = $this->getEngine();356$token = $engine->storeText('x');357358$metadata_key = self::KEY_RULE_OBJECT.'.'.$this->getObjectNamePrefix();359$metadata = $engine->getTextMetadata($metadata_key, array());360361$metadata[] = array(362'token' => $token,363) + $params;364365$engine->setTextMetadata($metadata_key, $metadata);366367return $token;368}369370public function didMarkupText() {371$engine = $this->getEngine();372$metadata_key = self::KEY_RULE_OBJECT.'.'.$this->getObjectNamePrefix();373$metadata = $engine->getTextMetadata($metadata_key, array());374375if (!$metadata) {376return;377}378379380$ids = ipull($metadata, 'id');381$objects = $this->loadObjects($ids);382383// For objects that are invalid or which the user can't see, just render384// the original text.385386// TODO: We should probably distinguish between these cases and render a387// "you can't see this" state for nonvisible objects.388389foreach ($metadata as $key => $spec) {390if (empty($objects[$spec['id']])) {391$engine->overwriteStoredText(392$spec['token'],393$spec['original']);394unset($metadata[$key]);395}396}397398$phids = $engine->getTextMetadata(self::KEY_MENTIONED_OBJECTS, array());399foreach ($objects as $object) {400$phids[$object->getPHID()] = $object->getPHID();401}402$engine->setTextMetadata(self::KEY_MENTIONED_OBJECTS, $phids);403404$handles = $this->loadHandles($objects);405foreach ($metadata as $key => $spec) {406$handle = $handles[$spec['id']];407$object = $objects[$spec['id']];408switch ($spec['type']) {409case 'ref':410411$view = $this->renderObjectRefForAnyMedia(412$object,413$handle,414$spec['anchor'],415$spec['id']);416break;417case 'embed':418$spec['options'] = $this->assertFlatText($spec['options']);419$view = $this->renderObjectEmbedForAnyMedia(420$object,421$handle,422$spec['options']);423break;424}425$engine->overwriteStoredText($spec['token'], $view);426}427428$engine->setTextMetadata($metadata_key, array());429}430431}432433434