Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/infrastructure/editor/PhabricatorEditorURIEngine.php
12241 views
1
<?php
2
3
final 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 || !strlen(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($path);
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($path);
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' => $this->escapeToken($repository->getRepositorySlug()),
145
'd' => $this->escapeToken($repository->getID()),
146
'p' => $this->escapeToken($repository->getPHID()),
147
);
148
149
return $this->newTokensWithVariables($tokens, $variables);
150
}
151
152
private function getRawURITokens() {
153
if (!$this->rawTokens) {
154
$this->rawTokens = $this->newRawURITokens();
155
}
156
return $this->rawTokens;
157
}
158
159
private function newRawURITokens() {
160
$raw_pattern = $this->getPattern();
161
$raw_tokens = self::newPatternTokens($raw_pattern);
162
163
$variable_definitions = self::getVariableDefinitions();
164
165
foreach ($raw_tokens as $token) {
166
if ($token['type'] !== 'variable') {
167
continue;
168
}
169
170
$value = $token['value'];
171
172
if (isset($variable_definitions[$value])) {
173
continue;
174
}
175
176
throw new PhabricatorEditorURIParserException(
177
pht(
178
'Editor pattern "%s" is invalid: the pattern contains an '.
179
'unrecognized variable ("%s"). Use "%%%%" to encode a literal '.
180
'percent symbol.',
181
$raw_pattern,
182
'%'.$value));
183
}
184
185
$variables = array(
186
'%' => '%',
187
);
188
189
$tokens = $this->newTokensWithVariables($raw_tokens, $variables);
190
191
$first_literal = null;
192
if ($tokens) {
193
foreach ($tokens as $token) {
194
if ($token['type'] === 'literal') {
195
$first_literal = $token['value'];
196
}
197
break;
198
}
199
200
if ($first_literal === null) {
201
throw new PhabricatorEditorURIParserException(
202
pht(
203
'Editor pattern "%s" is invalid: the pattern must begin with '.
204
'a valid editor protocol, but begins with a variable. This is '.
205
'very sneaky and also very forbidden.',
206
$raw_pattern));
207
}
208
}
209
210
$uri = new PhutilURI($first_literal);
211
$editor_protocol = $uri->getProtocol();
212
213
if (!$editor_protocol) {
214
throw new PhabricatorEditorURIParserException(
215
pht(
216
'Editor pattern "%s" is invalid: the pattern must begin with '.
217
'a valid editor protocol, but does not begin with a recognized '.
218
'protocol string.',
219
$raw_pattern));
220
}
221
222
$allowed_key = 'uri.allowed-editor-protocols';
223
$allowed_protocols = PhabricatorEnv::getEnvConfig($allowed_key);
224
if (empty($allowed_protocols[$editor_protocol])) {
225
throw new PhabricatorEditorURIParserException(
226
pht(
227
'Editor pattern "%s" is invalid: the pattern must begin with '.
228
'a valid editor protocol, but the protocol "%s://" is not allowed.',
229
$raw_pattern,
230
$editor_protocol));
231
}
232
233
return $tokens;
234
}
235
236
private function newTokensWithVariables(array $tokens, array $variables) {
237
// Replace all "variable" tokens that we have replacements for with
238
// the literal value.
239
foreach ($tokens as $key => $token) {
240
$type = $token['type'];
241
242
if ($type == 'variable') {
243
$variable = $token['value'];
244
if (isset($variables[$variable])) {
245
$tokens[$key] = array(
246
'type' => 'literal',
247
'value' => $variables[$variable],
248
);
249
}
250
}
251
}
252
253
// Now, merge sequences of adjacent "literal" tokens into a single token.
254
$last_literal = null;
255
foreach ($tokens as $key => $token) {
256
$is_literal = ($token['type'] === 'literal');
257
258
if (!$is_literal) {
259
$last_literal = null;
260
continue;
261
}
262
263
if ($last_literal !== null) {
264
$tokens[$key]['value'] =
265
$tokens[$last_literal]['value'].$token['value'];
266
unset($tokens[$last_literal]);
267
}
268
269
$last_literal = $key;
270
}
271
272
$tokens = array_values($tokens);
273
274
return $tokens;
275
}
276
277
private function escapeToken($token) {
278
// Paths are user controlled, so a clever user could potentially make
279
// editor links do surprising things with paths containing "/../".
280
281
// Find anything that looks like "/../" and mangle it.
282
283
$token = preg_replace('((^|/)\.\.(/|\z))', '\1dot-dot\2', $token);
284
285
return phutil_escape_uri($token);
286
}
287
288
private function newStringFromTokens(array $tokens) {
289
$result = array();
290
291
foreach ($tokens as $token) {
292
$token_type = $token['type'];
293
$token_value = $token['value'];
294
295
$is_literal = ($token_type === 'literal');
296
if (!$is_literal) {
297
throw new Exception(
298
pht(
299
'Editor pattern token list can not be converted into a string: '.
300
'it still contains a non-literal token ("%s", of type "%s").',
301
$token_value,
302
$token_type));
303
}
304
305
$result[] = $token_value;
306
}
307
308
$result = implode('', $result);
309
310
return $result;
311
}
312
313
public static function newPatternTokens($raw_pattern) {
314
$token_positions = array();
315
316
$len = strlen($raw_pattern);
317
318
for ($ii = 0; $ii < $len; $ii++) {
319
$c = $raw_pattern[$ii];
320
if ($c === '%') {
321
if (!isset($raw_pattern[$ii + 1])) {
322
throw new PhabricatorEditorURIParserException(
323
pht(
324
'Editor pattern "%s" is invalid: the final character in a '.
325
'pattern may not be an unencoded percent symbol ("%%"). '.
326
'Use "%%%%" to encode a literal percent symbol.',
327
$raw_pattern));
328
}
329
330
$token_positions[] = $ii;
331
$ii++;
332
}
333
}
334
335
// Add a final marker past the end of the string, so we'll collect any
336
// trailing literal bytes.
337
$token_positions[] = $len;
338
339
$tokens = array();
340
$cursor = 0;
341
foreach ($token_positions as $pos) {
342
$token_len = ($pos - $cursor);
343
344
if ($token_len > 0) {
345
$tokens[] = array(
346
'type' => 'literal',
347
'value' => substr($raw_pattern, $cursor, $token_len),
348
);
349
}
350
351
$cursor = $pos;
352
353
if ($cursor < $len) {
354
$tokens[] = array(
355
'type' => 'variable',
356
'value' => substr($raw_pattern, $cursor + 1, 1),
357
);
358
}
359
360
$cursor = $pos + 2;
361
}
362
363
return $tokens;
364
}
365
366
}
367
368