@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
at recaptime-dev/main 189 lines 5.4 kB view raw
1<?php 2 3final class PhutilRemarkupDocumentLinkRule extends PhutilRemarkupRule { 4 5 public function getPriority() { 6 return 150.0; 7 } 8 9 public function apply($text) { 10 // Handle mediawiki-style links: [[ href | name ]] 11 $text = preg_replace_callback( 12 '@\B\\[\\[([^|\\]]+)(?:\\|([^\\]]+))?\\]\\]\B@U', 13 array($this, 'markupDocumentLink'), 14 $text); 15 16 // Handle markdown-style links: [name](href) 17 $text = preg_replace_callback( 18 '@'. 19 '\B'. 20 '\\[([^\\]]+)\\]'. 21 '\\('. 22 '(\s*'. 23 // See T12343. This is making some kind of effort to implement 24 // parenthesis balancing rules. It won't get nested parentheses 25 // right, but should do OK for Wikipedia pages, which seem to be 26 // the most important use case. 27 28 // Match zero or more non-parenthesis, non-space characters. 29 '[^\s()]*'. 30 // Match zero or more sequences of "(...)", where two balanced 31 // parentheses enclose zero or more normal characters. If we 32 // match some, optionally match more stuff at the end. 33 '(?:(?:\\([^ ()]*\\))+[^\s()]*)?'. 34 '\s*)'. 35 '\\)'. 36 '\B'. 37 '@U', 38 array($this, 'markupAlternateLink'), 39 $text); 40 41 return $text; 42 } 43 44 protected function renderHyperlink($link, $name) { 45 $engine = $this->getEngine(); 46 47 $uri = new PhutilURIHelper($link); 48 $is_anchor = $uri->isAnchor(); 49 $starts_with_slash = $uri->isStartingWithSlash(); 50 if ($starts_with_slash) { 51 $base = phutil_string_cast($engine->getConfig('uri.base')); 52 $base = rtrim($base, '/'); 53 $link = $base.$link; 54 } else if ($is_anchor) { 55 $here = $engine->getConfig('uri.here'); 56 $link = $here.$link; 57 } 58 59 if ($engine->isTextMode()) { 60 // If present, strip off "mailto:" or "tel:". 61 $link = preg_replace('/^(?:mailto|tel):/', '', $link); 62 63 if (!strlen($name)) { 64 return $link; 65 } 66 67 return $name.' <'.$link.'>'; 68 } 69 70 if (!strlen($name)) { 71 $name = $link; 72 $name = preg_replace('/^(?:mailto|tel):/', '', $name); 73 } 74 75 if ($engine->getState('toc')) { 76 return $name; 77 } 78 79 // Check if this link points to Phorge itself. Micro-optimized. 80 $is_self = $is_anchor || $starts_with_slash || $uri->isSelf(); 81 82 // For historical reasons, links opened in a different tab 83 // for most links as default. 84 // Now internal resources keep internal link, as default. 85 $same_window = $engine->getConfig('uri.same-window', $is_self); 86 if ($same_window) { 87 $target = null; 88 } else { 89 $target = '_blank'; 90 } 91 92 // For anchors on the same page, always stay here. 93 if ($is_anchor) { 94 $target = null; 95 } 96 97 return phutil_tag( 98 'a', 99 array( 100 'href' => $link, 101 'class' => $this->getRemarkupLinkClass($is_self), 102 'target' => $target, 103 'rel' => 'noreferrer', 104 ), 105 $name); 106 } 107 108 public function markupAlternateLink(array $matches) { 109 $uri = trim($matches[2]); 110 111 if (!strlen($uri)) { 112 return $matches[0]; 113 } 114 115 // NOTE: We apply some special rules to avoid false positives here. The 116 // major concern is that we do not want to convert `x[0][1](y)` in a 117 // discussion about C source code into a link. To this end, we: 118 // 119 // - Don't match at word boundaries; 120 // - require the URI to contain a "/" character or "@" character; and 121 // - reject URIs which being with a quote character. 122 123 if ($uri[0] == '"' || $uri[0] == "'" || $uri[0] == '`') { 124 return $matches[0]; 125 } 126 127 if (strpos($uri, '/') === false && 128 strpos($uri, '@') === false && 129 strncmp($uri, 'tel:', 4)) { 130 return $matches[0]; 131 } 132 133 return $this->markupDocumentLink( 134 array( 135 $matches[0], 136 $matches[2], 137 $matches[1], 138 )); 139 } 140 141 public function markupDocumentLink(array $matches) { 142 $uri = trim($matches[1]); 143 $name = trim(idx($matches, 2, '')); 144 145 if (!$this->isFlatText($uri)) { 146 return $matches[0]; 147 } 148 149 if (!$this->isFlatText($name)) { 150 return $matches[0]; 151 } 152 153 // If whatever is being linked to begins with "/" or "#", or has "://", 154 // or is "mailto:" or "tel:", treat it as a URI instead of a wiki page. 155 $is_uri = preg_match('@(^/)|(://)|(^#)|(^(?:mailto|tel):)@', $uri); 156 157 if ($is_uri && strncmp('/', $uri, 1) && strncmp('#', $uri, 1)) { 158 $protocols = $this->getEngine()->getConfig( 159 'uri.allowed-protocols', 160 array()); 161 162 try { 163 $protocol = id(new PhutilURI($uri))->getProtocol(); 164 if (!idx($protocols, $protocol)) { 165 // Don't treat this as a URI if it's not an allowed protocol. 166 $is_uri = false; 167 } 168 } catch (Exception $ex) { 169 // We can end up here if we try to parse an ambiguous URI, see 170 // T12796. 171 $is_uri = false; 172 } 173 } 174 175 // As a special case, skip "[[ / ]]" so that Phriction picks it up as a 176 // link to the Phriction root. It is more useful to be able to use this 177 // syntax to link to the root document than the home page of the install. 178 if ($uri == '/') { 179 $is_uri = false; 180 } 181 182 if (!$is_uri) { 183 return $matches[0]; 184 } 185 186 return $this->getEngine()->storeText($this->renderHyperlink($uri, $name)); 187 } 188 189}