diff --git a/src/applications/people/markup/PhabricatorMentionRemarkupRule.php b/src/applications/people/markup/PhabricatorMentionRemarkupRule.php index 42efb05603..8049866118 100644 --- a/src/applications/people/markup/PhabricatorMentionRemarkupRule.php +++ b/src/applications/people/markup/PhabricatorMentionRemarkupRule.php @@ -1,181 +1,185 @@ getEngine(); if ($engine->isTextMode()) { return $engine->storeText($matches[0]); } $token = $engine->storeText(''); // Store the original text exactly so we can preserve casing if it doesn't // resolve into a username. $original_key = self::KEY_RULE_MENTION_ORIGINAL; $original = $engine->getTextMetadata($original_key, array()); $original[$token] = $matches[1]; $engine->setTextMetadata($original_key, $original); $metadata_key = self::KEY_RULE_MENTION; $metadata = $engine->getTextMetadata($metadata_key, array()); $username = strtolower($matches[1]); if (empty($metadata[$username])) { $metadata[$username] = array(); } $metadata[$username][] = $token; $engine->setTextMetadata($metadata_key, $metadata); return $token; } public function didMarkupText() { $engine = $this->getEngine(); $metadata_key = self::KEY_RULE_MENTION; $metadata = $engine->getTextMetadata($metadata_key, array()); if (empty($metadata)) { // No mentions, or we already processed them. return; } $original_key = self::KEY_RULE_MENTION_ORIGINAL; $original = $engine->getTextMetadata($original_key, array()); $usernames = array_keys($metadata); $users = id(new PhabricatorPeopleQuery()) ->setViewer($this->getEngine()->getConfig('viewer')) ->withUsernames($usernames) ->needAvailability(true) ->execute(); $actual_users = array(); $mentioned_key = self::KEY_MENTIONED; $mentioned = $engine->getTextMetadata($mentioned_key, array()); foreach ($users as $row) { $actual_users[strtolower($row->getUserName())] = $row; $mentioned[$row->getPHID()] = $row->getPHID(); } $engine->setTextMetadata($mentioned_key, $mentioned); $context_object = $engine->getConfig('contextObject'); foreach ($metadata as $username => $tokens) { $exists = isset($actual_users[$username]); $user_has_no_permission = false; if ($exists) { $user = $actual_users[$username]; Javelin::initBehavior('phui-hovercards'); // Check if the user has view access to the object she was mentioned in if ($context_object && $context_object instanceof PhabricatorPolicyInterface) { if (!PhabricatorPolicyFilter::hasCapability( $user, $context_object, PhabricatorPolicyCapability::CAN_VIEW)) { // User mentioned has no permission to this object $user_has_no_permission = true; } } $user_href = '/p/'.$user->getUserName().'/'; if ($engine->isHTMLMailMode()) { $user_href = PhabricatorEnv::getProductionURI($user_href); if ($user_has_no_permission) { $colors = ' border-color: #92969D; color: #92969D; background-color: #F7F7F7;'; } else { $colors = ' border-color: #f1f7ff; color: #19558d; background-color: #f1f7ff;'; } $tag = phutil_tag( 'a', array( 'href' => $user_href, 'style' => $colors.' border: 1px solid transparent; border-radius: 3px; font-weight: bold; padding: 0 4px;', ), '@'.$user->getUserName()); } else { + if ($engine->getConfig('uri.full')) { + $user_href = PhabricatorEnv::getURI($user_href); + } + $tag = id(new PHUITagView()) ->setType(PHUITagView::TYPE_PERSON) ->setPHID($user->getPHID()) ->setName('@'.$user->getUserName()) ->setHref($user_href); if ($user_has_no_permission) { $tag->addClass('phabricator-remarkup-mention-nopermission'); } if (!$user->isUserActivated()) { $tag->setDotColor(PHUITagView::COLOR_GREY); } else { if ($user->getAwayUntil()) { $tag->setDotColor(PHUITagView::COLOR_RED); } } } foreach ($tokens as $token) { $engine->overwriteStoredText($token, $tag); } } else { // NOTE: The structure here is different from the 'exists' branch, // because we want to preserve the original text capitalization and it // may differ for each token. foreach ($tokens as $token) { $tag = phutil_tag( 'span', array( 'class' => 'phabricator-remarkup-mention-unknown', ), '@'.idx($original, $token, $username)); $engine->overwriteStoredText($token, $tag); } } } // Don't re-process these mentions. $engine->setTextMetadata($metadata_key, array()); } } diff --git a/src/applications/phame/controller/post/PhamePostViewController.php b/src/applications/phame/controller/post/PhamePostViewController.php index 5579e82656..fdbaf139c2 100644 --- a/src/applications/phame/controller/post/PhamePostViewController.php +++ b/src/applications/phame/controller/post/PhamePostViewController.php @@ -1,269 +1,273 @@ setupLiveEnvironment(); if ($response) { return $response; } $viewer = $request->getViewer(); $moved = $request->getStr('moved'); $post = $this->getPost(); $blog = $this->getBlog(); $is_live = $this->getIsLive(); $is_external = $this->getIsExternal(); $header = id(new PHUIHeaderView()) ->setHeader($post->getTitle()) ->setUser($viewer); if (!$is_external) { $actions = $this->renderActions($post); $header->setPolicyObject($post); $header->setActionList($actions); } $document = id(new PHUIDocumentViewPro()) ->setHeader($header); if ($moved) { $document->appendChild( id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) ->appendChild(pht('Post moved successfully.'))); } if ($post->isDraft()) { $document->appendChild( id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) ->setTitle(pht('Draft Post')) ->appendChild( pht('Only you can see this draft until you publish it. '. 'Use "Publish" to publish this post.'))); } if (!$post->getBlog()) { $document->appendChild( id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_WARNING) ->setTitle(pht('Not On A Blog')) ->appendChild( pht('This post is not associated with a blog (the blog may have '. 'been deleted). Use "Move Post" to move it to a new blog.'))); } $engine = id(new PhabricatorMarkupEngine()) ->setViewer($viewer) ->addObject($post, PhamePost::MARKUP_FIELD_BODY) ->process(); $document->appendChild( phutil_tag( 'div', array( 'class' => 'phabricator-remarkup', ), $engine->getOutput($post, PhamePost::MARKUP_FIELD_BODY))); $blogger = id(new PhabricatorPeopleQuery()) ->setViewer($viewer) ->withPHIDs(array($post->getBloggerPHID())) ->needProfileImage(true) ->executeOne(); $blogger_profile = $blogger->loadUserProfile(); + + $author_uri = '/p/'.$blogger->getUsername().'/'; + $author_uri = PhabricatorEnv::getURI($author_uri); + $author = phutil_tag( 'a', array( - 'href' => '/p/'.$blogger->getUsername().'/', + 'href' => $author_uri, ), $blogger->getUsername()); $date = phabricator_datetime($post->getDatePublished(), $viewer); if ($post->isDraft()) { $subtitle = pht('Unpublished draft by %s.', $author); } else { $subtitle = pht('Written by %s on %s.', $author, $date); } $user_icon = $blogger_profile->getIcon(); $user_icon = PhabricatorPeopleIconSet::getIconIcon($user_icon); $user_icon = id(new PHUIIconView())->setIcon($user_icon); $about = id(new PhameDescriptionView()) ->setTitle($subtitle) ->setDescription( array( $user_icon, ' ', $blogger_profile->getTitle(), )) ->setImage($blogger->getProfileImageURI()) - ->setImageHref('/p/'.$blogger->getUsername()); + ->setImageHref($author_uri); $timeline = $this->buildTransactionTimeline( $post, id(new PhamePostTransactionQuery()) ->withTransactionTypes(array(PhabricatorTransactions::TYPE_COMMENT))); $timeline = phutil_tag_div('phui-document-view-pro-box', $timeline); if ($is_external) { $add_comment = null; } else { $add_comment = $this->buildCommentForm($post); $add_comment = phutil_tag_div('mlb mlt', $add_comment); } list($prev, $next) = $this->loadAdjacentPosts($post); $properties = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($post); $next_view = new PhameNextPostView(); if ($next) { - $next_view->setNext($next->getTitle(), $next->getViewURI()); + $next_view->setNext($next->getTitle(), $next->getLiveURI()); } if ($prev) { - $next_view->setPrevious($prev->getTitle(), $prev->getViewURI()); + $next_view->setPrevious($prev->getTitle(), $prev->getLiveURI()); } $document->setFoot($next_view); $crumbs = $this->buildApplicationCrumbs(); $properties = phutil_tag_div('phui-document-view-pro-box', $properties); $page = $this->newPage() ->setTitle($post->getTitle()) ->setPageObjectPHIDs(array($post->getPHID())) ->setCrumbs($crumbs) ->appendChild( array( $document, $about, $properties, $timeline, $add_comment, )); if ($is_live) { $page ->setShowChrome(false) ->setShowFooter(false); } return $page; } private function renderActions(PhamePost $post) { $viewer = $this->getViewer(); $actions = id(new PhabricatorActionListView()) ->setObject($post) ->setUser($viewer); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $post, PhabricatorPolicyCapability::CAN_EDIT); $id = $post->getID(); $actions->addAction( id(new PhabricatorActionView()) ->setIcon('fa-pencil') ->setHref($this->getApplicationURI('post/edit/'.$id.'/')) ->setName(pht('Edit Post')) ->setDisabled(!$can_edit)); $actions->addAction( id(new PhabricatorActionView()) ->setIcon('fa-arrows') ->setHref($this->getApplicationURI('post/move/'.$id.'/')) ->setName(pht('Move Post')) ->setDisabled(!$can_edit) ->setWorkflow(true)); $actions->addAction( id(new PhabricatorActionView()) ->setIcon('fa-history') ->setHref($this->getApplicationURI('post/history/'.$id.'/')) ->setName(pht('View History'))); if ($post->isDraft()) { $actions->addAction( id(new PhabricatorActionView()) ->setIcon('fa-eye') ->setHref($this->getApplicationURI('post/publish/'.$id.'/')) ->setName(pht('Publish')) ->setDisabled(!$can_edit) ->setWorkflow(true)); } else { $actions->addAction( id(new PhabricatorActionView()) ->setIcon('fa-eye-slash') ->setHref($this->getApplicationURI('post/unpublish/'.$id.'/')) ->setName(pht('Unpublish')) ->setDisabled(!$can_edit) ->setWorkflow(true)); } if ($post->isDraft()) { $live_name = pht('Preview'); } else { $live_name = pht('View Live'); } $actions->addAction( id(new PhabricatorActionView()) ->setUser($viewer) ->setIcon('fa-globe') ->setHref($post->getLiveURI()) ->setName($live_name)); return $actions; } private function buildCommentForm(PhamePost $post) { $viewer = $this->getViewer(); $draft = PhabricatorDraft::newFromUserAndKey( $viewer, $post->getPHID()); $box = id(new PhabricatorApplicationTransactionCommentView()) ->setUser($viewer) ->setObjectPHID($post->getPHID()) ->setDraft($draft) ->setHeaderText(pht('Add Comment')) ->setAction($this->getApplicationURI('post/comment/'.$post->getID().'/')) ->setSubmitButtonName(pht('Add Comment')); return phutil_tag_div('phui-document-view-pro-box', $box); } private function loadAdjacentPosts(PhamePost $post) { $viewer = $this->getViewer(); $query = id(new PhamePostQuery()) ->setViewer($viewer) ->withVisibility(PhameConstants::VISIBILITY_PUBLISHED) ->withBlogPHIDs(array($post->getBlog()->getPHID())) ->setLimit(1); $prev = id(clone $query) ->setAfterID($post->getID()) ->execute(); $next = id(clone $query) ->setBeforeID($post->getID()) ->execute(); return array(head($prev), head($next)); } } diff --git a/src/applications/phame/view/PhamePostListView.php b/src/applications/phame/view/PhamePostListView.php index fe6da36541..1932b597a1 100644 --- a/src/applications/phame/view/PhamePostListView.php +++ b/src/applications/phame/view/PhamePostListView.php @@ -1,144 +1,154 @@ posts = $posts; return $this; } public function setNodata($nodata) { $this->nodata = $nodata; return $this; } public function showBlog($show) { $this->showBlog = $show; return $this; } public function setIsExternal($is_external) { $this->isExternal = $is_external; return $this; } public function getIsExternal() { return $this->isExternal; } public function setIsLive($is_live) { $this->isLive = $is_live; return $this; } public function getIsLive() { return $this->isLive; } protected function getTagAttributes() { return array(); } protected function getTagContent() { $viewer = $this->getViewer(); $posts = $this->posts; $nodata = $this->nodata; $handle_phids = array(); foreach ($posts as $post) { $handle_phids[] = $post->getBloggerPHID(); if ($post->getBlog()) { $handle_phids[] = $post->getBlog()->getPHID(); } } $handles = $viewer->loadHandles($handle_phids); $list = array(); foreach ($posts as $post) { - $blogger = $handles[$post->getBloggerPHID()]->renderLink(); + $blogger_name = $handles[$post->getBloggerPHID()]->getName(); $blogger_uri = $handles[$post->getBloggerPHID()]->getURI(); + $blogger_uri = PhabricatorEnv::getURI($blogger_uri); + + // Render a link manually to make sure we point at the correct domain. + $blogger = phutil_tag( + 'a', + array( + 'href' => $blogger_uri, + ), + $blogger_name); + $blogger = phutil_tag('strong', array(), $blogger); + $blogger_image = $handles[$post->getBloggerPHID()]->getImageURI(); $phame_post = null; if ($post->getBody()) { $phame_post = PhabricatorMarkupEngine::summarize($post->getBody()); $phame_post = new PHUIRemarkupView($viewer, $phame_post); } else { $phame_post = phutil_tag('em', array(), pht('(Empty Post)')); } - $blogger = phutil_tag('strong', array(), $blogger); $date = phabricator_datetime($post->getDatePublished(), $viewer); $blog = $post->getBlog(); if ($this->getIsLive()) { if ($this->getIsExternal()) { $blog_uri = $blog->getExternalLiveURI(); $post_uri = $post->getExternalLiveURI(); } else { $blog_uri = $blog->getInternalLiveURI(); $post_uri = $post->getInternalLiveURI(); } } else { $blog_uri = $blog->getViewURI(); $post_uri = $post->getViewURI(); } $blog_link = phutil_tag( 'a', array( 'href' => $blog_uri, ), $blog->getName()); if ($this->showBlog) { if ($post->isDraft()) { $subtitle = pht( 'Unpublished draft by %s in %s.', $blogger, $blog_link); } else { $subtitle = pht( 'Written by %s on %s in %s.', $blogger, $date, $blog_link); } } else { if ($post->isDraft()) { $subtitle = pht('Unpublished draft by %s.', $blogger); } else { $subtitle = pht('Written by %s on %s.', $blogger, $date); } } $item = id(new PHUIDocumentSummaryView()) ->setTitle($post->getTitle()) ->setHref($post_uri) ->setSubtitle($subtitle) ->setImage($blogger_image) ->setImageHref($blogger_uri) ->setSummary($phame_post) ->setDraft($post->isDraft()); $list[] = $item; } if (empty($list)) { $list = id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_NODATA) ->appendChild($nodata); } return $list; } } diff --git a/src/infrastructure/markup/PhabricatorMarkupEngine.php b/src/infrastructure/markup/PhabricatorMarkupEngine.php index dc3809f873..7c19cac57f 100644 --- a/src/infrastructure/markup/PhabricatorMarkupEngine.php +++ b/src/infrastructure/markup/PhabricatorMarkupEngine.php @@ -1,659 +1,670 @@ addObject($comment, $field); * } * * Now, call @{method:process} to perform the actual cache/rendering * step. This is a heavyweight call which does batched data access and * transforms the markup into output. * * $engine->process(); * * Finally, do something with the results: * * $results = array(); * foreach ($comments as $comment) { * $results[] = $engine->getOutput($comment, $field); * } * * If you have a single object to render, you can use the convenience method * @{method:renderOneObject}. * * @task markup Markup Pipeline * @task engine Engine Construction */ final class PhabricatorMarkupEngine extends Phobject { private $objects = array(); private $viewer; private $contextObject; private $version = 15; private $engineCaches = array(); private $auxiliaryConfig = array(); /* -( Markup Pipeline )---------------------------------------------------- */ /** * Convenience method for pushing a single object through the markup * pipeline. * * @param PhabricatorMarkupInterface The object to render. * @param string The field to render. * @param PhabricatorUser User viewing the markup. * @param object A context object for policy checks * @return string Marked up output. * @task markup */ public static function renderOneObject( PhabricatorMarkupInterface $object, $field, PhabricatorUser $viewer, $context_object = null) { return id(new PhabricatorMarkupEngine()) ->setViewer($viewer) ->setContextObject($context_object) ->addObject($object, $field) ->process() ->getOutput($object, $field); } /** * Queue an object for markup generation when @{method:process} is * called. You can retrieve the output later with @{method:getOutput}. * * @param PhabricatorMarkupInterface The object to render. * @param string The field to render. * @return this * @task markup */ public function addObject(PhabricatorMarkupInterface $object, $field) { $key = $this->getMarkupFieldKey($object, $field); $this->objects[$key] = array( 'object' => $object, 'field' => $field, ); return $this; } /** * Process objects queued with @{method:addObject}. You can then retrieve * the output with @{method:getOutput}. * * @return this * @task markup */ public function process() { $keys = array(); foreach ($this->objects as $key => $info) { if (!isset($info['markup'])) { $keys[] = $key; } } if (!$keys) { return; } $objects = array_select_keys($this->objects, $keys); // Build all the markup engines. We need an engine for each field whether // we have a cache or not, since we still need to postprocess the cache. $engines = array(); foreach ($objects as $key => $info) { $engines[$key] = $info['object']->newMarkupEngine($info['field']); $engines[$key]->setConfig('viewer', $this->viewer); $engines[$key]->setConfig('contextObject', $this->contextObject); foreach ($this->auxiliaryConfig as $aux_key => $aux_value) { $engines[$key]->setConfig($aux_key, $aux_value); } } // Load or build the preprocessor caches. $blocks = $this->loadPreprocessorCaches($engines, $objects); $blocks = mpull($blocks, 'getCacheData'); $this->engineCaches = $blocks; // Finalize the output. foreach ($objects as $key => $info) { $engine = $engines[$key]; $field = $info['field']; $object = $info['object']; $output = $engine->postprocessText($blocks[$key]); $output = $object->didMarkupText($field, $output, $engine); $this->objects[$key]['output'] = $output; } return $this; } /** * Get the output of markup processing for a field queued with * @{method:addObject}. Before you can call this method, you must call * @{method:process}. * * @param PhabricatorMarkupInterface The object to retrieve. * @param string The field to retrieve. * @return string Processed output. * @task markup */ public function getOutput(PhabricatorMarkupInterface $object, $field) { $key = $this->getMarkupFieldKey($object, $field); $this->requireKeyProcessed($key); return $this->objects[$key]['output']; } /** * Retrieve engine metadata for a given field. * * @param PhabricatorMarkupInterface The object to retrieve. * @param string The field to retrieve. * @param string The engine metadata field to retrieve. * @param wild Optional default value. * @task markup */ public function getEngineMetadata( PhabricatorMarkupInterface $object, $field, $metadata_key, $default = null) { $key = $this->getMarkupFieldKey($object, $field); $this->requireKeyProcessed($key); return idx($this->engineCaches[$key]['metadata'], $metadata_key, $default); } /** * @task markup */ private function requireKeyProcessed($key) { if (empty($this->objects[$key])) { throw new Exception( pht( "Call %s before using results (key = '%s').", 'addObject()', $key)); } if (!isset($this->objects[$key]['output'])) { throw new PhutilInvalidStateException('process'); } } /** * @task markup */ private function getMarkupFieldKey( PhabricatorMarkupInterface $object, $field) { static $custom; if ($custom === null) { $custom = array_merge( self::loadCustomInlineRules(), self::loadCustomBlockRules()); $custom = mpull($custom, 'getRuleVersion', null); ksort($custom); $custom = PhabricatorHash::digestForIndex(serialize($custom)); } return $object->getMarkupFieldKey($field).'@'.$this->version.'@'.$custom; } /** * @task markup */ private function loadPreprocessorCaches(array $engines, array $objects) { $blocks = array(); $use_cache = array(); foreach ($objects as $key => $info) { if ($info['object']->shouldUseMarkupCache($info['field'])) { $use_cache[$key] = true; } } if ($use_cache) { try { $blocks = id(new PhabricatorMarkupCache())->loadAllWhere( 'cacheKey IN (%Ls)', array_keys($use_cache)); $blocks = mpull($blocks, null, 'getCacheKey'); } catch (Exception $ex) { phlog($ex); } } $is_readonly = PhabricatorEnv::isReadOnly(); foreach ($objects as $key => $info) { // False check in case MySQL doesn't support unicode characters // in the string (T1191), resulting in unserialize returning false. if (isset($blocks[$key]) && $blocks[$key]->getCacheData() !== false) { // If we already have a preprocessing cache, we don't need to rebuild // it. continue; } $text = $info['object']->getMarkupText($info['field']); $data = $engines[$key]->preprocessText($text); // NOTE: This is just debugging information to help sort out cache issues. // If one machine is misconfigured and poisoning caches you can use this // field to hunt it down. $metadata = array( 'host' => php_uname('n'), ); $blocks[$key] = id(new PhabricatorMarkupCache()) ->setCacheKey($key) ->setCacheData($data) ->setMetadata($metadata); if (isset($use_cache[$key]) && !$is_readonly) { // This is just filling a cache and always safe, even on a read pathway. $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $blocks[$key]->replace(); unset($unguarded); } } return $blocks; } /** * Set the viewing user. Used to implement object permissions. * * @param PhabricatorUser The viewing user. * @return this * @task markup */ public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; return $this; } /** * Set the context object. Used to implement object permissions. * * @param The object in which context this remarkup is used. * @return this * @task markup */ public function setContextObject($object) { $this->contextObject = $object; return $this; } public function setAuxiliaryConfig($key, $value) { // TODO: This is gross and should be removed. Avoid use. $this->auxiliaryConfig[$key] = $value; return $this; } /* -( Engine Construction )------------------------------------------------ */ /** * @task engine */ public static function newManiphestMarkupEngine() { return self::newMarkupEngine(array( )); } /** * @task engine */ public static function newPhrictionMarkupEngine() { return self::newMarkupEngine(array( 'header.generate-toc' => true, )); } /** * @task engine */ public static function newPhameMarkupEngine() { - return self::newMarkupEngine(array( - 'macros' => false, - 'uri.full' => true, - )); + return self::newMarkupEngine( + array( + 'macros' => false, + 'uri.full' => true, + 'uri.same-window' => true, + 'uri.base' => PhabricatorEnv::getURI('/'), + )); } /** * @task engine */ public static function newFeedMarkupEngine() { return self::newMarkupEngine( array( 'macros' => false, 'youtube' => false, )); } /** * @task engine */ public static function newCalendarMarkupEngine() { return self::newMarkupEngine(array( )); } /** * @task engine */ public static function newDifferentialMarkupEngine(array $options = array()) { return self::newMarkupEngine(array( 'differential.diff' => idx($options, 'differential.diff'), )); } /** * @task engine */ public static function newDiffusionMarkupEngine(array $options = array()) { return self::newMarkupEngine(array( 'header.generate-toc' => true, )); } /** * @task engine */ public static function getEngine($ruleset = 'default') { static $engines = array(); if (isset($engines[$ruleset])) { return $engines[$ruleset]; } $engine = null; switch ($ruleset) { case 'default': $engine = self::newMarkupEngine(array()); break; case 'nolinebreaks': $engine = self::newMarkupEngine(array()); $engine->setConfig('preserve-linebreaks', false); break; case 'diffusion-readme': $engine = self::newMarkupEngine(array()); $engine->setConfig('preserve-linebreaks', false); $engine->setConfig('header.generate-toc', true); break; case 'diviner': $engine = self::newMarkupEngine(array()); $engine->setConfig('preserve-linebreaks', false); // $engine->setConfig('diviner.renderer', new DivinerDefaultRenderer()); $engine->setConfig('header.generate-toc', true); break; case 'extract': // Engine used for reference/edge extraction. Turn off anything which // is slow and doesn't change reference extraction. $engine = self::newMarkupEngine(array()); $engine->setConfig('pygments.enabled', false); break; default: throw new Exception(pht('Unknown engine ruleset: %s!', $ruleset)); } $engines[$ruleset] = $engine; return $engine; } /** * @task engine */ private static function getMarkupEngineDefaultConfiguration() { return array( 'pygments' => PhabricatorEnv::getEnvConfig('pygments.enabled'), 'youtube' => PhabricatorEnv::getEnvConfig( 'remarkup.enable-embedded-youtube'), 'differential.diff' => null, 'header.generate-toc' => false, 'macros' => true, 'uri.allowed-protocols' => PhabricatorEnv::getEnvConfig( 'uri.allowed-protocols'), 'uri.full' => false, 'syntax-highlighter.engine' => PhabricatorEnv::getEnvConfig( 'syntax-highlighter.engine'), 'preserve-linebreaks' => true, ); } /** * @task engine */ public static function newMarkupEngine(array $options) { $options += self::getMarkupEngineDefaultConfiguration(); $engine = new PhutilRemarkupEngine(); $engine->setConfig('preserve-linebreaks', $options['preserve-linebreaks']); $engine->setConfig('pygments.enabled', $options['pygments']); $engine->setConfig( 'uri.allowed-protocols', $options['uri.allowed-protocols']); $engine->setConfig('differential.diff', $options['differential.diff']); $engine->setConfig('header.generate-toc', $options['header.generate-toc']); $engine->setConfig( 'syntax-highlighter.engine', $options['syntax-highlighter.engine']); $style_map = id(new PhabricatorDefaultSyntaxStyle()) ->getRemarkupStyleMap(); $engine->setConfig('phutil.codeblock.style-map', $style_map); $engine->setConfig('uri.full', $options['uri.full']); + if (isset($options['uri.base'])) { + $engine->setConfig('uri.base', $options['uri.base']); + } + + if (isset($options['uri.same-window'])) { + $engine->setConfig('uri.same-window', $options['uri.same-window']); + } + $rules = array(); $rules[] = new PhutilRemarkupEscapeRemarkupRule(); $rules[] = new PhutilRemarkupMonospaceRule(); $rules[] = new PhutilRemarkupDocumentLinkRule(); $rules[] = new PhabricatorNavigationRemarkupRule(); if ($options['youtube']) { $rules[] = new PhabricatorYoutubeRemarkupRule(); } $rules[] = new PhabricatorIconRemarkupRule(); $rules[] = new PhabricatorEmojiRemarkupRule(); $rules[] = new PhabricatorHandleRemarkupRule(); $applications = PhabricatorApplication::getAllInstalledApplications(); foreach ($applications as $application) { foreach ($application->getRemarkupRules() as $rule) { $rules[] = $rule; } } $rules[] = new PhutilRemarkupHyperlinkRule(); if ($options['macros']) { $rules[] = new PhabricatorImageMacroRemarkupRule(); $rules[] = new PhabricatorMemeRemarkupRule(); } $rules[] = new PhutilRemarkupBoldRule(); $rules[] = new PhutilRemarkupItalicRule(); $rules[] = new PhutilRemarkupDelRule(); $rules[] = new PhutilRemarkupUnderlineRule(); $rules[] = new PhutilRemarkupHighlightRule(); foreach (self::loadCustomInlineRules() as $rule) { $rules[] = clone $rule; } $blocks = array(); $blocks[] = new PhutilRemarkupQuotesBlockRule(); $blocks[] = new PhutilRemarkupReplyBlockRule(); $blocks[] = new PhutilRemarkupLiteralBlockRule(); $blocks[] = new PhutilRemarkupHeaderBlockRule(); $blocks[] = new PhutilRemarkupHorizontalRuleBlockRule(); $blocks[] = new PhutilRemarkupListBlockRule(); $blocks[] = new PhutilRemarkupCodeBlockRule(); $blocks[] = new PhutilRemarkupNoteBlockRule(); $blocks[] = new PhutilRemarkupTableBlockRule(); $blocks[] = new PhutilRemarkupSimpleTableBlockRule(); $blocks[] = new PhutilRemarkupInterpreterBlockRule(); $blocks[] = new PhutilRemarkupDefaultBlockRule(); foreach (self::loadCustomBlockRules() as $rule) { $blocks[] = $rule; } foreach ($blocks as $block) { $block->setMarkupRules($rules); } $engine->setBlockRules($blocks); return $engine; } public static function extractPHIDsFromMentions( PhabricatorUser $viewer, array $content_blocks) { $mentions = array(); $engine = self::newDifferentialMarkupEngine(); $engine->setConfig('viewer', $viewer); foreach ($content_blocks as $content_block) { $engine->markupText($content_block); $phids = $engine->getTextMetadata( PhabricatorMentionRemarkupRule::KEY_MENTIONED, array()); $mentions += $phids; } return $mentions; } public static function extractFilePHIDsFromEmbeddedFiles( PhabricatorUser $viewer, array $content_blocks) { $files = array(); $engine = self::newDifferentialMarkupEngine(); $engine->setConfig('viewer', $viewer); foreach ($content_blocks as $content_block) { $engine->markupText($content_block); $phids = $engine->getTextMetadata( PhabricatorEmbedFileRemarkupRule::KEY_EMBED_FILE_PHIDS, array()); foreach ($phids as $phid) { $files[$phid] = $phid; } } return array_values($files); } /** * Produce a corpus summary, in a way that shortens the underlying text * without truncating it somewhere awkward. * * TODO: We could do a better job of this. * * @param string Remarkup corpus to summarize. * @return string Summarized corpus. */ public static function summarize($corpus) { // Major goals here are: // - Don't split in the middle of a character (utf-8). // - Don't split in the middle of, e.g., **bold** text, since // we end up with hanging '**' in the summary. // - Try not to pick an image macro, header, embedded file, etc. // - Hopefully don't return too much text. We don't explicitly limit // this right now. $blocks = preg_split("/\n *\n\s*/", $corpus); $best = null; foreach ($blocks as $block) { // This is a test for normal spaces in the block, i.e. a heuristic to // distinguish standard paragraphs from things like image macros. It may // not work well for non-latin text. We prefer to summarize with a // paragraph of normal words over an image macro, if possible. $has_space = preg_match('/\w\s\w/', $block); // This is a test to find embedded images and headers. We prefer to // summarize with a normal paragraph over a header or an embedded object, // if possible. $has_embed = preg_match('/^[{=]/', $block); if ($has_space && !$has_embed) { // This seems like a good summary, so return it. return $block; } if (!$best) { // This is the first block we found; if everything is garbage just // use the first block. $best = $block; } } return $best; } private static function loadCustomInlineRules() { return id(new PhutilClassMapQuery()) ->setAncestorClass('PhabricatorRemarkupCustomInlineRule') ->execute(); } private static function loadCustomBlockRules() { return id(new PhutilClassMapQuery()) ->setAncestorClass('PhabricatorRemarkupCustomBlockRule') ->execute(); } } diff --git a/src/infrastructure/markup/view/PHUIRemarkupView.php b/src/infrastructure/markup/view/PHUIRemarkupView.php index e30c09ce7c..e5772e9d84 100644 --- a/src/infrastructure/markup/view/PHUIRemarkupView.php +++ b/src/infrastructure/markup/view/PHUIRemarkupView.php @@ -1,96 +1,96 @@ appendChild($fancy_text); * */ final class PHUIRemarkupView extends AphrontView { private $corpus; private $contextObject; private $options; // TODO: In the long run, rules themselves should define available options. // For now, just define constants here so we can more easily replace things // later once this is cleaned up. const OPTION_PRESERVE_LINEBREAKS = 'preserve-linebreaks'; public function __construct(PhabricatorUser $viewer, $corpus) { $this->setUser($viewer); $this->corpus = $corpus; } public function setContextObject($context_object) { $this->contextObject = $context_object; return $this; } public function getContextObject() { return $this->contextObject; } public function setRemarkupOption($key, $value) { $this->options[$key] = $value; return $this; } public function setRemarkupOptions(array $options) { foreach ($options as $key => $value) { $this->setRemarkupOption($key, $value); } return $this; } public function render() { $viewer = $this->getViewer(); $corpus = $this->corpus; $context = $this->getContextObject(); $options = $this->options; $oneoff = id(new PhabricatorMarkupOneOff()) ->setContent($corpus); if ($options) { $oneoff->setEngine($this->getEngine()); } else { $oneoff->setPreserveLinebreaks(true); } $content = PhabricatorMarkupEngine::renderOneObject( $oneoff, 'default', $viewer, $context); return $content; } private function getEngine() { $options = $this->options; $viewer = $this->getViewer(); $viewer_key = $viewer->getCacheFragment(); ksort($options); $engine_key = serialize($options); $engine_key = PhabricatorHash::digestForIndex($engine_key); $cache = PhabricatorCaches::getRequestCache(); - $cache_key = "remarkup.engine({$viewer}, {$engine_key})"; + $cache_key = "remarkup.engine({$viewer_key}, {$engine_key})"; $engine = $cache->getKey($cache_key); if (!$engine) { $engine = PhabricatorMarkupEngine::newMarkupEngine($options); $cache->setKey($cache_key, $engine); } return $engine; } }