diff --git a/src/applications/phriction/markup/PhrictionRemarkupRule.php b/src/applications/phriction/markup/PhrictionRemarkupRule.php
index d17de331e5..11994e0aa5 100644
--- a/src/applications/phriction/markup/PhrictionRemarkupRule.php
+++ b/src/applications/phriction/markup/PhrictionRemarkupRule.php
@@ -1,285 +1,295 @@
isFlatText($name)) {
+ return $matches[0];
+ }
+
+ if (!$this->isFlatText($path)) {
+ return $matches[0];
+ }
+
// If the link contains an anchor, separate that off first.
- $parts = explode('#', trim($matches[1]), 2);
+ $parts = explode('#', $path, 2);
if (count($parts) == 2) {
$link = $parts[0];
$anchor = $parts[1];
} else {
$link = $parts[0];
$anchor = null;
}
// Handle relative links.
if ((substr($link, 0, 2) === './') || (substr($link, 0, 3) === '../')) {
$base = $this->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');
$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;
}
$visible_documents = id(new PhrictionDocumentQuery())
->setViewer($viewer)
->withSlugs(array_keys($load_map))
->needContent(true)
->execute();
$visible_documents = mpull($visible_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,
);
}
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);
$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 (!$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 = $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->getContent()->getSlug();
}
return null;
}
}
diff --git a/src/infrastructure/markup/markuprule/PhutilRemarkupDocumentLinkRule.php b/src/infrastructure/markup/markuprule/PhutilRemarkupDocumentLinkRule.php
index a6effa00ac..2170d9ae5e 100644
--- a/src/infrastructure/markup/markuprule/PhutilRemarkupDocumentLinkRule.php
+++ b/src/infrastructure/markup/markuprule/PhutilRemarkupDocumentLinkRule.php
@@ -1,175 +1,183 @@
getEngine();
$is_anchor = false;
if (strncmp($link, '/', 1) == 0) {
$base = $engine->getConfig('uri.base');
$base = rtrim($base, '/');
$link = $base.$link;
} else if (strncmp($link, '#', 1) == 0) {
$here = $engine->getConfig('uri.here');
$link = $here.$link;
$is_anchor = true;
}
if ($engine->isTextMode()) {
// If present, strip off "mailto:" or "tel:".
$link = preg_replace('/^(?:mailto|tel):/', '', $link);
if (!strlen($name)) {
return $link;
}
return $name.' <'.$link.'>';
}
if (!strlen($name)) {
$name = $link;
$name = preg_replace('/^(?:mailto|tel):/', '', $name);
}
if ($engine->getState('toc')) {
return $name;
}
$same_window = $engine->getConfig('uri.same-window', false);
if ($same_window) {
$target = null;
} else {
$target = '_blank';
}
// For anchors on the same page, always stay here.
if ($is_anchor) {
$target = null;
}
return phutil_tag(
'a',
array(
'href' => $link,
'class' => 'remarkup-link',
'target' => $target,
'rel' => 'noreferrer',
),
$name);
}
public function markupAlternateLink(array $matches) {
$uri = trim($matches[2]);
if (!strlen($uri)) {
return $matches[0];
}
// NOTE: We apply some special rules to avoid false positives here. The
// major concern is that we do not want to convert `x[0][1](y)` in a
// discussion about C source code into a link. To this end, we:
//
// - Don't match at word boundaries;
// - require the URI to contain a "/" character or "@" character; and
// - reject URIs which being with a quote character.
if ($uri[0] == '"' || $uri[0] == "'" || $uri[0] == '`') {
return $matches[0];
}
if (strpos($uri, '/') === false &&
strpos($uri, '@') === false &&
strncmp($uri, 'tel:', 4)) {
return $matches[0];
}
return $this->markupDocumentLink(
array(
$matches[0],
$matches[2],
$matches[1],
));
}
public function markupDocumentLink(array $matches) {
$uri = trim($matches[1]);
$name = trim(idx($matches, 2));
+ if (!$this->isFlatText($uri)) {
+ return $matches[0];
+ }
+
+ if (!$this->isFlatText($name)) {
+ return $matches[0];
+ }
+
// If whatever is being linked to begins with "/" or "#", or has "://",
// or is "mailto:" or "tel:", treat it as a URI instead of a wiki page.
$is_uri = preg_match('@(^/)|(://)|(^#)|(^(?:mailto|tel):)@', $uri);
if ($is_uri && strncmp('/', $uri, 1) && strncmp('#', $uri, 1)) {
$protocols = $this->getEngine()->getConfig(
'uri.allowed-protocols',
array());
try {
$protocol = id(new PhutilURI($uri))->getProtocol();
if (!idx($protocols, $protocol)) {
// Don't treat this as a URI if it's not an allowed protocol.
$is_uri = false;
}
} catch (Exception $ex) {
// We can end up here if we try to parse an ambiguous URI, see
// T12796.
$is_uri = false;
}
}
// As a special case, skip "[[ / ]]" so that Phriction picks it up as a
// link to the Phriction root. It is more useful to be able to use this
// syntax to link to the root document than the home page of the install.
if ($uri == '/') {
$is_uri = false;
}
if (!$is_uri) {
return $matches[0];
}
return $this->getEngine()->storeText($this->renderHyperlink($uri, $name));
}
}