@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 PhrictionRemarkupRule extends PhutilRemarkupRule {
4
5 const KEY_RULE_PHRICTION_LINK = 'phriction.link';
6
7 public function getPriority() {
8 return 175.0;
9 }
10
11 public function apply($text) {
12 return preg_replace_callback(
13 '@\B\\[\\[([^|\\]]+)(?:\\|([^\\]]+))?\\]\\]\B@U',
14 array($this, 'markupDocumentLink'),
15 $text);
16 }
17
18 public function markupDocumentLink(array $matches) {
19 $name = trim(idx($matches, 2, ''));
20 if (empty($matches[2])) {
21 $name = null;
22 }
23
24 $path = trim($matches[1]);
25
26 if (!$this->isFlatText($name)) {
27 return $matches[0];
28 }
29
30 if (!$this->isFlatText($path)) {
31 return $matches[0];
32 }
33
34 // If the link contains an anchor, separate that off first.
35 $parts = explode('#', $path, 2);
36 if (count($parts) == 2) {
37 $link = $parts[0];
38 $anchor = $parts[1];
39 } else {
40 $link = $parts[0];
41 $anchor = null;
42 }
43
44 // Handle relative links.
45 if ((substr($link, 0, 2) === './') || (substr($link, 0, 3) === '../')) {
46 $base = $this->getRelativeBaseURI();
47 if ($base !== null) {
48 $base_parts = explode('/', rtrim($base, '/'));
49 $rel_parts = explode('/', rtrim($link, '/'));
50 foreach ($rel_parts as $part) {
51 if ($part === '.') {
52 // Consume standalone dots in a relative path, and do
53 // nothing with them.
54 } else if ($part === '..') {
55 if (count($base_parts) > 0) {
56 array_pop($base_parts);
57 }
58 } else {
59 array_push($base_parts, $part);
60 }
61 }
62 $link = implode('/', $base_parts).'/';
63 }
64 }
65
66 // Link is now used for slug detection, so append a slash if one
67 // is needed.
68 $link = rtrim($link, '/').'/';
69
70 $engine = $this->getEngine();
71 $token = $engine->storeText('x');
72 $metadata = $engine->getTextMetadata(
73 self::KEY_RULE_PHRICTION_LINK,
74 array());
75 $metadata[] = array(
76 'token' => $token,
77 'link' => $link,
78 'anchor' => $anchor,
79 'explicitName' => $name,
80 );
81 $engine->setTextMetadata(self::KEY_RULE_PHRICTION_LINK, $metadata);
82
83 return $token;
84 }
85
86 public function didMarkupText() {
87 $engine = $this->getEngine();
88 $metadata = $engine->getTextMetadata(
89 self::KEY_RULE_PHRICTION_LINK,
90 array());
91
92 if (!$metadata) {
93 return;
94 }
95
96 $viewer = $engine->getConfig('viewer');
97
98 $slugs = ipull($metadata, 'link');
99
100 $load_map = array();
101 foreach ($slugs as $key => $raw_slug) {
102 $lookup = PhabricatorSlug::normalize($raw_slug);
103 $load_map[$lookup][] = $key;
104
105 // Also try to lookup the slug with URL decoding applied. The right
106 // way to link to a page titled "$cash" is to write "[[ $cash ]]" (and
107 // not the URL encoded form "[[ %24cash ]]"), but users may reasonably
108 // have copied URL encoded variations out of their browser location
109 // bar or be skeptical that "[[ $cash ]]" will actually work.
110
111 $lookup = phutil_unescape_uri_path_component($raw_slug);
112 $lookup = phutil_utf8ize($lookup);
113 $lookup = PhabricatorSlug::normalize($lookup);
114 $load_map[$lookup][] = $key;
115 }
116
117 $visible_documents = id(new PhrictionDocumentQuery())
118 ->setViewer($viewer)
119 ->withSlugs(array_keys($load_map))
120 ->needContent(true)
121 ->execute();
122 $visible_documents = mpull($visible_documents, null, 'getSlug');
123 $document_map = array();
124 foreach ($load_map as $lookup => $keys) {
125 $visible = idx($visible_documents, $lookup);
126 if (!$visible) {
127 continue;
128 }
129
130 foreach ($keys as $key) {
131 $document_map[$key] = array(
132 'visible' => true,
133 'document' => $visible,
134 );
135 }
136
137 unset($load_map[$lookup]);
138 }
139
140 // For each document we found, remove all remaining requests for it from
141 // the load map. If we remove all requests for a slug, remove the slug.
142 // This stops us from doing unnecessary lookups on alternate names for
143 // documents we already found.
144 foreach ($load_map as $lookup => $keys) {
145 foreach ($keys as $lookup_key => $key) {
146 if (isset($document_map[$key])) {
147 unset($keys[$lookup_key]);
148 }
149 }
150
151 if (!$keys) {
152 unset($load_map[$lookup]);
153 continue;
154 }
155
156 $load_map[$lookup] = $keys;
157 }
158
159
160 // If we still have links we haven't found a document for, do another
161 // query with the omnipotent viewer so we can distinguish between pages
162 // which do not exist and pages which exist but which the viewer does not
163 // have permission to see.
164 if ($load_map) {
165 $existent_documents = id(new PhrictionDocumentQuery())
166 ->setViewer(PhabricatorUser::getOmnipotentUser())
167 ->withSlugs(array_keys($load_map))
168 ->execute();
169 $existent_documents = mpull($existent_documents, null, 'getSlug');
170
171 foreach ($load_map as $lookup => $keys) {
172 $existent = idx($existent_documents, $lookup);
173 if (!$existent) {
174 continue;
175 }
176
177 foreach ($keys as $key) {
178 $document_map[$key] = array(
179 'visible' => false,
180 'document' => null,
181 );
182 }
183 }
184 }
185
186 foreach ($metadata as $key => $spec) {
187 $link = $spec['link'];
188 $slug = PhabricatorSlug::normalize($link);
189 $name = $spec['explicitName'];
190 $class = 'phriction-link';
191
192 // If the name is something meaningful to humans, we'll render this
193 // in text as: "Title" <link>. Otherwise, we'll just render: <link>.
194 $is_interesting_name = phutil_nonempty_string($name);
195
196 $target = idx($document_map, $key, null);
197
198 if ($target === null) {
199 // The target document doesn't exist.
200 if ($name === null) {
201 $name = explode('/', trim($link, '/'));
202 $name = end($name);
203 }
204 $class = 'phriction-link-missing';
205 } else if (!$target['visible']) {
206 // The document exists, but the user can't see it.
207 if ($name === null) {
208 $name = explode('/', trim($link, '/'));
209 $name = end($name);
210 }
211 $class = 'phriction-link-lock';
212 } else {
213 if ($name === null) {
214 // Use the title of the document if no name is set.
215 $name = $target['document']
216 ->getContent()
217 ->getTitle();
218
219 $is_interesting_name = true;
220 }
221 }
222
223 $uri = new PhutilURI($link);
224 $slug = $uri->getPath();
225 $slug = PhabricatorSlug::normalize($slug);
226 $slug = PhrictionDocument::getSlugURI($slug);
227
228 $anchor = idx($spec, 'anchor');
229 $href = (string)id(new PhutilURI($slug))->setFragment($anchor);
230
231 $text_mode = $this->getEngine()->isTextMode();
232 $mail_mode = $this->getEngine()->isHTMLMailMode();
233
234 if ($this->getEngine()->getState('toc')) {
235 $text = $name;
236 } else if ($text_mode || $mail_mode) {
237 $href = PhabricatorEnv::getProductionURI($href);
238 if ($is_interesting_name) {
239 $text = pht('"%s" <%s>', $name, $href);
240 } else {
241 $text = pht('<%s>', $href);
242 }
243 } else {
244 if ($class === 'phriction-link-lock') {
245 $name = array(
246 $this->newTag(
247 'i',
248 array(
249 'class' => 'phui-icon-view phui-font-fa fa-lock',
250 ),
251 ''),
252 ' ',
253 $name,
254 );
255 }
256 $text = $this->newTag(
257 'a',
258 array(
259 'href' => $href,
260 'class' => $class,
261 ),
262 $name);
263 }
264
265 $this->getEngine()->overwriteStoredText($spec['token'], $text);
266 }
267 }
268
269 private function getRelativeBaseURI() {
270 $context = $this->getEngine()->getConfig('contextObject');
271
272 if (!$context) {
273 return null;
274 }
275
276 if ($context instanceof PhrictionContent) {
277 return $context->getSlug();
278 }
279
280 if ($context instanceof PhrictionDocument) {
281 return $context->getContent()->getSlug();
282 }
283
284 return null;
285 }
286
287
288}