@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
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}