diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -5789,6 +5789,8 @@ 'PhutilTranslatedHTMLTestCase' => 'infrastructure/markup/__tests__/PhutilTranslatedHTMLTestCase.php', 'PhutilTwitchAuthAdapter' => 'applications/auth/adapter/PhutilTwitchAuthAdapter.php', 'PhutilTwitterAuthAdapter' => 'applications/auth/adapter/PhutilTwitterAuthAdapter.php', + 'PhutilURIGoodie' => 'infrastructure/parser/PhutilURIGoodie.php', + 'PhutilURIGoodieTestCase' => 'infrastructure/parser/__tests__/PhutilURIGoodieTestCase.php', 'PhutilWordPressAuthAdapter' => 'applications/auth/adapter/PhutilWordPressAuthAdapter.php', 'PhutilXHPASTSyntaxHighlighter' => 'infrastructure/markup/syntax/highlighter/PhutilXHPASTSyntaxHighlighter.php', 'PhutilXHPASTSyntaxHighlighterFuture' => 'infrastructure/markup/syntax/highlighter/xhpast/PhutilXHPASTSyntaxHighlighterFuture.php', @@ -12675,6 +12677,8 @@ 'PhutilTranslatedHTMLTestCase' => 'PhutilTestCase', 'PhutilTwitchAuthAdapter' => 'PhutilOAuthAuthAdapter', 'PhutilTwitterAuthAdapter' => 'PhutilOAuth1AuthAdapter', + 'PhutilURIGoodie' => 'Phobject', + 'PhutilURIGoodieTestCase' => 'PhabricatorTestCase', 'PhutilWordPressAuthAdapter' => 'PhutilOAuthAuthAdapter', 'PhutilXHPASTSyntaxHighlighter' => 'Phobject', 'PhutilXHPASTSyntaxHighlighterFuture' => 'FutureProxy', diff --git a/src/infrastructure/markup/markuprule/PhutilRemarkupDocumentLinkRule.php b/src/infrastructure/markup/markuprule/PhutilRemarkupDocumentLinkRule.php --- a/src/infrastructure/markup/markuprule/PhutilRemarkupDocumentLinkRule.php +++ b/src/infrastructure/markup/markuprule/PhutilRemarkupDocumentLinkRule.php @@ -44,16 +44,15 @@ protected function renderHyperlink($link, $name) { $engine = $this->getEngine(); - $is_anchor = false; - if (strncmp($link, '/', 1) == 0) { + $uri = new PhutilURIGoodie($link); + $is_anchor = $uri->isAnchor(); + if ($uri->isStartingWithSlash()) { + $here = $engine->getConfig('uri.here'); + $link = $here.$link; + } else if ($is_anchor) { $base = phutil_string_cast($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()) { @@ -76,7 +75,8 @@ return $name; } - $same_window = $engine->getConfig('uri.same-window', false); + $is_internal = $uri->isInternal(); + $same_window = $engine->getConfig('uri.same-window', $is_internal); if ($same_window) { $target = null; } else { @@ -92,7 +92,7 @@ 'a', array( 'href' => $link, - 'class' => 'remarkup-link', + 'class' => $this->getRemarkupLinkClass($is_internal), 'target' => $target, 'rel' => 'noreferrer', ), diff --git a/src/infrastructure/markup/markuprule/PhutilRemarkupHyperlinkRule.php b/src/infrastructure/markup/markuprule/PhutilRemarkupHyperlinkRule.php --- a/src/infrastructure/markup/markuprule/PhutilRemarkupHyperlinkRule.php +++ b/src/infrastructure/markup/markuprule/PhutilRemarkupHyperlinkRule.php @@ -116,7 +116,9 @@ $engine = $this->getEngine(); - $same_window = $engine->getConfig('uri.same-window', false); + $uri = new PhutilURIGoodie($link); + $is_internal = $uri->isInternal(); + $same_window = $engine->getConfig('uri.same-window', $is_internal); if ($same_window) { $target = null; } else { @@ -127,7 +129,7 @@ 'a', array( 'href' => $link, - 'class' => 'remarkup-link', + 'class' => $this->getRemarkupLinkClass($is_internal), 'target' => $target, 'rel' => 'noreferrer', ), diff --git a/src/infrastructure/markup/markuprule/PhutilRemarkupRule.php b/src/infrastructure/markup/markuprule/PhutilRemarkupRule.php --- a/src/infrastructure/markup/markuprule/PhutilRemarkupRule.php +++ b/src/infrastructure/markup/markuprule/PhutilRemarkupRule.php @@ -106,4 +106,20 @@ return (strpos($text, PhutilRemarkupBlockStorage::MAGIC_BYTE) === false); } + /** + * Get the CSS class="" attribute for a Remarkup link. + * It's just "remarkup-link" for all cases, plus the possibility for + * designers to style external links differently. + * @param boolean $is_internal Whenever the link was internal or not. + * @return string + */ + protected function getRemarkupLinkClass($is_internal) { + // Allow developers to style esternal links differently + $classes = array('remarkup-link'); + if (!$is_internal) { + $classes[] = 'remarkup-link-ext'; + } + return implode(' ', $classes); + } + } diff --git a/src/infrastructure/parser/PhutilURIGoodie.php b/src/infrastructure/parser/PhutilURIGoodie.php new file mode 100644 --- /dev/null +++ b/src/infrastructure/parser/PhutilURIGoodie.php @@ -0,0 +1,138 @@ +uriObj = new PhutilURI($uri); + $this->uriOriginalStr = $uri; + } + + /** + * Get the original URI as string. + * @return string + */ + public function getOriginalURI() { + return $this->uriOriginalStr; + } + + /** + * Get the full URI as string. + * @return object + */ + public function getURI() { + return $this->uriObj; + } + + /** + * Check whenever an URI *is* just a simple HTML anchor. + * @param string $uri + * @return bool + */ + public function isAnchor() { + return $this->isStartingWithChar('#'); + } + + /** + * Check whenever an URI starts with a slash (no protocol, etc.) + * @param string $uri + * @return bool + */ + public function isStartingWithSlash() { + return $this->isStartingWithChar('/'); + } + + /** + * Check if this URI points to Phorge itself. + * This is a simple compromise between performance and a sane logic. + * At the moment we don't read the crystal ball, so these are + * considered different websites: + * - http://example.com (the input) + * - https://example.com (the base-uri) + * @param string $uri Example 'http://example.com/bar/' + * @return bool + */ + public function isInternal() { + + // If this URL has not a domain, indeed its the current domain. + // Probably this is a simple anchor, or just '/something/' etc. + $uri = $this->uriObj; + if ($uri->getProtocol() === '' && $uri->getDomain() === '') { + return true; + } + + // Assume a safe default in case of missing base URI. + $base = self::baseURIObj(); + if (!$base) { + return false; + } + + return $uri->getDomain() === $base->getDomain() + && $uri->getProtocol() === $base->getProtocol() + && $uri->getPort() === $base->getPort(); + } + + /** + * Get a fresh URI object, cached. + * This is safe to be called a lot of times. + * @return PhutilURI|null + */ + public static function baseURIObj() { + if (self::$siteBaseUriObj === null) { + self::$siteBaseUriObj = self::baseURIObjUncached(); + } + return self::$siteBaseUriObj; + } + + /** + * Get a fresh URI object (if any), uncached. + * @return PhutilURI|false + */ + private static function baseURIObjUncached() { + $base = PhabricatorEnv::getEnvConfigIfExists('phabricator.base-uri'); + if ($base) { + return new PhutilURI($base); + } + return false; + } + + /** + * Check whenever the URI starts with the provided character. + * @param string $char String that MUST be of length = 1. + * @return boolean + */ + private function isStartingWithChar($char) { + return strncmp($this->uriOriginalStr, $char, 1) === 0; + } + +} diff --git a/src/infrastructure/parser/__tests__/PhutilURIGoodieTestCase.php b/src/infrastructure/parser/__tests__/PhutilURIGoodieTestCase.php new file mode 100644 --- /dev/null +++ b/src/infrastructure/parser/__tests__/PhutilURIGoodieTestCase.php @@ -0,0 +1,34 @@ +isInternal(); + + $this->assertEqual($tested_value, $test_expected, $test_name); + } + } +}