diff --git a/src/applications/phriction/markup/PhrictionRemarkupRule.php b/src/applications/phriction/markup/PhrictionRemarkupRule.php index c4bc1f7249..a6362d1624 100644 --- a/src/applications/phriction/markup/PhrictionRemarkupRule.php +++ b/src/applications/phriction/markup/PhrictionRemarkupRule.php @@ -1,214 +1,285 @@ getRelativeBaseURI(); if ($base !== null) { $base_parts = explode('/', rtrim($base, '/')); $rel_parts = explode('/', rtrim($link, '/')); foreach ($rel_parts as $part) { if ($part === '.') { // Consume standalone dots in a relative path, and do // nothing with them. } else if ($part === '..') { if (count($base_parts) > 0) { array_pop($base_parts); } } else { array_push($base_parts, $part); } } $link = implode('/', $base_parts).'/'; } } $name = trim(idx($matches, 2, '')); if (empty($matches[2])) { $name = null; } // Link is now used for slug detection, so append a slash if one // is needed. $link = rtrim($link, '/').'/'; $engine = $this->getEngine(); $token = $engine->storeText('x'); $metadata = $engine->getTextMetadata( self::KEY_RULE_PHRICTION_LINK, array()); $metadata[] = array( 'token' => $token, 'link' => $link, 'anchor' => $anchor, 'explicitName' => $name, ); $engine->setTextMetadata(self::KEY_RULE_PHRICTION_LINK, $metadata); return $token; } public function didMarkupText() { $engine = $this->getEngine(); $metadata = $engine->getTextMetadata( self::KEY_RULE_PHRICTION_LINK, array()); if (!$metadata) { return; } + $viewer = $engine->getConfig('viewer'); + $slugs = ipull($metadata, 'link'); - foreach ($slugs as $key => $slug) { - $slugs[$key] = PhabricatorSlug::normalize($slug); + + $load_map = array(); + foreach ($slugs as $key => $raw_slug) { + $lookup = PhabricatorSlug::normalize($raw_slug); + $load_map[$lookup][] = $key; + + // Also try to lookup the slug with URL decoding applied. The right + // way to link to a page titled "$cash" is to write "[[ $cash ]]" (and + // not the URL encoded form "[[ %24cash ]]"), but users may reasonably + // have copied URL encoded variations out of their browser location + // bar or be skeptical that "[[ $cash ]]" will actually work. + + $lookup = phutil_unescape_uri_path_component($raw_slug); + $lookup = phutil_utf8ize($lookup); + $lookup = PhabricatorSlug::normalize($lookup); + $load_map[$lookup][] = $key; } - // We have to make two queries here to distinguish between - // documents the user can't see, and documents that don't - // exist. $visible_documents = id(new PhrictionDocumentQuery()) - ->setViewer($engine->getConfig('viewer')) - ->withSlugs($slugs) + ->setViewer($viewer) + ->withSlugs(array_keys($load_map)) ->needContent(true) ->execute(); - $existant_documents = id(new PhrictionDocumentQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withSlugs($slugs) - ->execute(); - $visible_documents = mpull($visible_documents, null, 'getSlug'); - $existant_documents = mpull($existant_documents, null, 'getSlug'); + $document_map = array(); + foreach ($load_map as $lookup => $keys) { + $visible = idx($visible_documents, $lookup); + if (!$visible) { + continue; + } + + foreach ($keys as $key) { + $document_map[$key] = array( + 'visible' => true, + 'document' => $visible, + ); + } - foreach ($metadata as $spec) { + unset($load_map[$lookup]); + } + + // For each document we found, remove all remaining requests for it from + // the load map. If we remove all requests for a slug, remove the slug. + // This stops us from doing unnecessary lookups on alternate names for + // documents we already found. + foreach ($load_map as $lookup => $keys) { + foreach ($keys as $lookup_key => $key) { + if (isset($document_map[$key])) { + unset($keys[$lookup_key]); + } + } + + if (!$keys) { + unset($load_map[$lookup]); + continue; + } + + $load_map[$lookup] = $keys; + } + + + // If we still have links we haven't found a document for, do another + // query with the omnipotent viewer so we can distinguish between pages + // which do not exist and pages which exist but which the viewer does not + // have permission to see. + if ($load_map) { + $existent_documents = id(new PhrictionDocumentQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withSlugs(array_keys($load_map)) + ->execute(); + $existent_documents = mpull($existent_documents, null, 'getSlug'); + + foreach ($load_map as $lookup => $keys) { + $existent = idx($existent_documents, $lookup); + if (!$existent) { + continue; + } + + foreach ($keys as $key) { + $document_map[$key] = array( + 'visible' => false, + 'document' => null, + ); + } + } + } + + foreach ($metadata as $key => $spec) { $link = $spec['link']; $slug = PhabricatorSlug::normalize($link); $name = $spec['explicitName']; $class = 'phriction-link'; // If the name is something meaningful to humans, we'll render this // in text as: "Title" . Otherwise, we'll just render: . $is_interesting_name = (bool)strlen($name); - if (idx($existant_documents, $slug) === null) { + $target = idx($document_map, $key, null); + + if ($target === null) { // The target document doesn't exist. if ($name === null) { $name = explode('/', trim($link, '/')); $name = end($name); } $class = 'phriction-link-missing'; - } else if (idx($visible_documents, $slug) === null) { + } else if (!$target['visible']) { // The document exists, but the user can't see it. if ($name === null) { $name = explode('/', trim($link, '/')); $name = end($name); } $class = 'phriction-link-lock'; } else { if ($name === null) { // Use the title of the document if no name is set. - $name = $visible_documents[$slug] + $name = $target['document'] ->getContent() ->getTitle(); $is_interesting_name = true; } } $uri = new PhutilURI($link); $slug = $uri->getPath(); $slug = PhabricatorSlug::normalize($slug); $slug = PhrictionDocument::getSlugURI($slug); $anchor = idx($spec, 'anchor'); $href = (string)id(new PhutilURI($slug))->setFragment($anchor); $text_mode = $this->getEngine()->isTextMode(); $mail_mode = $this->getEngine()->isHTMLMailMode(); if ($this->getEngine()->getState('toc')) { $text = $name; } else if ($text_mode || $mail_mode) { $href = PhabricatorEnv::getProductionURI($href); if ($is_interesting_name) { $text = pht('"%s" <%s>', $name, $href); } else { $text = pht('<%s>', $href); } } else { if ($class === 'phriction-link-lock') { $name = array( $this->newTag( 'i', array( 'class' => 'phui-icon-view phui-font-fa fa-lock', ), ''), ' ', $name, ); } $text = $this->newTag( 'a', array( 'href' => $href, 'class' => $class, ), $name); } $this->getEngine()->overwriteStoredText($spec['token'], $text); } } private function getRelativeBaseURI() { $context = $this->getEngine()->getConfig('contextObject'); if (!$context) { return null; } // Handle content when it's a preview for the Phriction editor. if (is_array($context)) { if (idx($context, 'phriction.isPreview')) { return idx($context, 'phriction.slug'); } } if ($context instanceof PhrictionContent) { return $context->getSlug(); } if ($context instanceof PhrictionDocument) { return $context->getSlug(); } return null; } }