@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 PhabricatorEditorURIEngine
4 extends Phobject {
5
6 private $viewer;
7 private $repository;
8 private $pattern;
9 private $rawTokens;
10 private $repositoryTokens;
11
12 public static function newForViewer(PhabricatorUser $viewer) {
13 if (!$viewer->isLoggedIn()) {
14 return null;
15 }
16
17 $pattern = $viewer->getUserSetting(PhabricatorEditorSetting::SETTINGKEY);
18
19 if ($pattern === null || trim($pattern) === '') {
20 return null;
21 }
22
23 $engine = id(new self())
24 ->setViewer($viewer)
25 ->setPattern($pattern);
26
27 // If there's a problem with the pattern,
28
29 try {
30 $engine->validatePattern();
31 } catch (PhabricatorEditorURIParserException $ex) {
32 return null;
33 }
34
35 return $engine;
36 }
37
38 public function setViewer(PhabricatorUser $viewer) {
39 $this->viewer = $viewer;
40 return $this;
41 }
42
43 public function getViewer() {
44 return $this->viewer;
45 }
46
47 public function setRepository(PhabricatorRepository $repository) {
48 $this->repository = $repository;
49 return $this;
50 }
51
52 public function getRepository() {
53 return $this->repository;
54 }
55
56 public function setPattern($pattern) {
57 $this->pattern = $pattern;
58 return $this;
59 }
60
61 public function getPattern() {
62 return $this->pattern;
63 }
64
65 public function validatePattern() {
66 $this->getRawURITokens();
67 return true;
68 }
69
70 public function getURIForPath($path, $line) {
71 $tokens = $this->getURITokensForRepository();
72
73 $variables = array(
74 'f' => $this->escapeToken($path),
75 'l' => $this->escapeToken($line),
76 );
77
78 $tokens = $this->newTokensWithVariables($tokens, $variables);
79
80 return $this->newStringFromTokens($tokens);
81 }
82
83 public function getURITokensForPath($path) {
84 $tokens = $this->getURITokensForRepository();
85
86 $variables = array(
87 'f' => $this->escapeToken($path),
88 );
89
90 return $this->newTokensWithVariables($tokens, $variables);
91 }
92
93 public static function getVariableDefinitions() {
94 return array(
95 'f' => array(
96 'name' => pht('File Name'),
97 'example' => pht('path/to/source.c'),
98 ),
99 'l' => array(
100 'name' => pht('Line Number'),
101 'example' => '777',
102 ),
103 'n' => array(
104 'name' => pht('Repository Short Name'),
105 'example' => 'arcanist',
106 ),
107 'd' => array(
108 'name' => pht('Repository ID'),
109 'example' => '42',
110 ),
111 'p' => array(
112 'name' => pht('Repository PHID'),
113 'example' => 'PHID-REPO-abcdefghijklmnopqrst',
114 ),
115 'r' => array(
116 'name' => pht('Repository Callsign'),
117 'example' => 'XYZ',
118 ),
119 '%' => array(
120 'name' => pht('Literal Percent Symbol'),
121 'example' => '%',
122 ),
123 );
124 }
125
126 private function getURITokensForRepository() {
127 if (!$this->repositoryTokens) {
128 $this->repositoryTokens = $this->newURITokensForRepository();
129 }
130
131 return $this->repositoryTokens;
132 }
133
134 private function newURITokensForRepository() {
135 $tokens = $this->getRawURITokens();
136
137 $repository = $this->getRepository();
138 if (!$repository) {
139 throw new PhutilInvalidStateException('setRepository');
140 }
141
142 $variables = array(
143 'r' => $this->escapeToken($repository->getCallsign()),
144 'n' => null,
145 'd' => $this->escapeToken($repository->getID()),
146 'p' => $this->escapeToken($repository->getPHID()),
147 );
148 if ($repository->getRepositorySlug()) {
149 $variables['n'] = $this->escapeToken($repository->getRepositorySlug());
150 }
151
152 return $this->newTokensWithVariables($tokens, $variables);
153 }
154
155 private function getRawURITokens() {
156 if (!$this->rawTokens) {
157 $this->rawTokens = $this->newRawURITokens();
158 }
159 return $this->rawTokens;
160 }
161
162 private function newRawURITokens() {
163 $raw_pattern = $this->getPattern();
164 $raw_tokens = self::newPatternTokens($raw_pattern);
165
166 $variable_definitions = self::getVariableDefinitions();
167
168 foreach ($raw_tokens as $token) {
169 if ($token['type'] !== 'variable') {
170 continue;
171 }
172
173 $value = $token['value'];
174
175 if (isset($variable_definitions[$value])) {
176 continue;
177 }
178
179 throw new PhabricatorEditorURIParserException(
180 pht(
181 'Editor pattern "%s" is invalid: the pattern contains an '.
182 'unrecognized variable ("%s"). Use "%%%%" to encode a literal '.
183 'percent symbol.',
184 $raw_pattern,
185 '%'.$value));
186 }
187
188 $variables = array(
189 '%' => '%',
190 );
191
192 $tokens = $this->newTokensWithVariables($raw_tokens, $variables);
193
194 $first_literal = null;
195 if ($tokens) {
196 foreach ($tokens as $token) {
197 if ($token['type'] === 'literal') {
198 $first_literal = $token['value'];
199 }
200 break;
201 }
202
203 if ($first_literal === null) {
204 throw new PhabricatorEditorURIParserException(
205 pht(
206 'Editor pattern "%s" is invalid: the pattern must begin with '.
207 'a valid editor protocol, but begins with a variable. This is '.
208 'very sneaky and also very forbidden.',
209 $raw_pattern));
210 }
211 }
212
213 $uri = new PhutilURI($first_literal);
214 $editor_protocol = $uri->getProtocol();
215
216 if (!$editor_protocol) {
217 throw new PhabricatorEditorURIParserException(
218 pht(
219 'Editor pattern "%s" is invalid: the pattern must begin with '.
220 'a valid editor protocol, but does not begin with a recognized '.
221 'protocol string.',
222 $raw_pattern));
223 }
224
225 $allowed_key = 'uri.allowed-editor-protocols';
226 $allowed_protocols = PhabricatorEnv::getEnvConfig($allowed_key);
227 if (empty($allowed_protocols[$editor_protocol])) {
228 throw new PhabricatorEditorURIParserException(
229 pht(
230 'Editor pattern "%s" is invalid: the pattern must begin with '.
231 'a valid editor protocol, but the protocol "%s://" is not allowed.',
232 $raw_pattern,
233 $editor_protocol));
234 }
235
236 return $tokens;
237 }
238
239 private function newTokensWithVariables(array $tokens, array $variables) {
240 // Replace all "variable" tokens that we have replacements for with
241 // the literal value.
242 foreach ($tokens as $key => $token) {
243 $type = $token['type'];
244
245 if ($type == 'variable') {
246 $variable = $token['value'];
247 if (isset($variables[$variable])) {
248 $tokens[$key] = array(
249 'type' => 'literal',
250 'value' => $variables[$variable],
251 );
252 }
253 }
254 }
255
256 // Now, merge sequences of adjacent "literal" tokens into a single token.
257 $last_literal = null;
258 foreach ($tokens as $key => $token) {
259 $is_literal = ($token['type'] === 'literal');
260
261 if (!$is_literal) {
262 $last_literal = null;
263 continue;
264 }
265
266 if ($last_literal !== null) {
267 $tokens[$key]['value'] =
268 $tokens[$last_literal]['value'].$token['value'];
269 unset($tokens[$last_literal]);
270 }
271
272 $last_literal = $key;
273 }
274
275 $tokens = array_values($tokens);
276
277 return $tokens;
278 }
279
280 private function escapeToken($token) {
281 // Paths are user controlled, so a clever user could potentially make
282 // editor links do surprising things with paths containing "/../".
283
284 // Find anything that looks like "/../" and mangle it.
285
286 $token = preg_replace('((^|/)\.\.(/|\z))', '\1dot-dot\2', $token);
287
288 return phutil_escape_uri($token);
289 }
290
291 private function newStringFromTokens(array $tokens) {
292 $result = array();
293
294 foreach ($tokens as $token) {
295 $token_type = $token['type'];
296 $token_value = $token['value'];
297
298 $is_literal = ($token_type === 'literal');
299 if (!$is_literal) {
300 throw new Exception(
301 pht(
302 'Editor pattern token list can not be converted into a string: '.
303 'it still contains a non-literal token ("%s", of type "%s").',
304 $token_value,
305 $token_type));
306 }
307
308 $result[] = $token_value;
309 }
310
311 $result = implode('', $result);
312
313 return $result;
314 }
315
316 public static function newPatternTokens($raw_pattern) {
317 $token_positions = array();
318
319 $len = strlen($raw_pattern);
320
321 for ($ii = 0; $ii < $len; $ii++) {
322 $c = $raw_pattern[$ii];
323 if ($c === '%') {
324 if (!isset($raw_pattern[$ii + 1])) {
325 throw new PhabricatorEditorURIParserException(
326 pht(
327 'Editor pattern "%s" is invalid: the final character in a '.
328 'pattern may not be an unencoded percent symbol ("%%"). '.
329 'Use "%%%%" to encode a literal percent symbol.',
330 $raw_pattern));
331 }
332
333 $token_positions[] = $ii;
334 $ii++;
335 }
336 }
337
338 // Add a final marker past the end of the string, so we'll collect any
339 // trailing literal bytes.
340 $token_positions[] = $len;
341
342 $tokens = array();
343 $cursor = 0;
344 foreach ($token_positions as $pos) {
345 $token_len = ($pos - $cursor);
346
347 if ($token_len > 0) {
348 $tokens[] = array(
349 'type' => 'literal',
350 'value' => substr($raw_pattern, $cursor, $token_len),
351 );
352 }
353
354 $cursor = $pos;
355
356 if ($cursor < $len) {
357 $tokens[] = array(
358 'type' => 'variable',
359 'value' => substr($raw_pattern, $cursor + 1, 1),
360 );
361 }
362
363 $cursor = $pos + 2;
364 }
365
366 return $tokens;
367 }
368
369}