Correct Aphlict websocket URI construction after PHP8 compatibility changes
[phabricator.git] / src / infrastructure / editor / PhabricatorEditorURIEngine.php
blob56ea54d3ca4be38d3cb3774913fb4b398d7b7fed
1 <?php
3 final class PhabricatorEditorURIEngine
4 extends Phobject {
6 private $viewer;
7 private $repository;
8 private $pattern;
9 private $rawTokens;
10 private $repositoryTokens;
12 public static function newForViewer(PhabricatorUser $viewer) {
13 if (!$viewer->isLoggedIn()) {
14 return null;
17 $pattern = $viewer->getUserSetting(PhabricatorEditorSetting::SETTINGKEY);
19 if ($pattern === null || !strlen(trim($pattern))) {
20 return null;
23 $engine = id(new self())
24 ->setViewer($viewer)
25 ->setPattern($pattern);
27 // If there's a problem with the pattern,
29 try {
30 $engine->validatePattern();
31 } catch (PhabricatorEditorURIParserException $ex) {
32 return null;
35 return $engine;
38 public function setViewer(PhabricatorUser $viewer) {
39 $this->viewer = $viewer;
40 return $this;
43 public function getViewer() {
44 return $this->viewer;
47 public function setRepository(PhabricatorRepository $repository) {
48 $this->repository = $repository;
49 return $this;
52 public function getRepository() {
53 return $this->repository;
56 public function setPattern($pattern) {
57 $this->pattern = $pattern;
58 return $this;
61 public function getPattern() {
62 return $this->pattern;
65 public function validatePattern() {
66 $this->getRawURITokens();
67 return true;
70 public function getURIForPath($path, $line) {
71 $tokens = $this->getURITokensForRepository($path);
73 $variables = array(
74 'f' => $this->escapeToken($path),
75 'l' => $this->escapeToken($line),
78 $tokens = $this->newTokensWithVariables($tokens, $variables);
80 return $this->newStringFromTokens($tokens);
83 public function getURITokensForPath($path) {
84 $tokens = $this->getURITokensForRepository($path);
86 $variables = array(
87 'f' => $this->escapeToken($path),
90 return $this->newTokensWithVariables($tokens, $variables);
93 public static function getVariableDefinitions() {
94 return array(
95 'f' => array(
96 'name' => pht('File Name'),
97 'example' => pht('path/to/source.c'),
99 'l' => array(
100 'name' => pht('Line Number'),
101 'example' => '777',
103 'n' => array(
104 'name' => pht('Repository Short Name'),
105 'example' => 'arcanist',
107 'd' => array(
108 'name' => pht('Repository ID'),
109 'example' => '42',
111 'p' => array(
112 'name' => pht('Repository PHID'),
113 'example' => 'PHID-REPO-abcdefghijklmnopqrst',
115 'r' => array(
116 'name' => pht('Repository Callsign'),
117 'example' => 'XYZ',
119 '%' => array(
120 'name' => pht('Literal Percent Symbol'),
121 'example' => '%',
126 private function getURITokensForRepository() {
127 if (!$this->repositoryTokens) {
128 $this->repositoryTokens = $this->newURITokensForRepository();
131 return $this->repositoryTokens;
134 private function newURITokensForRepository() {
135 $tokens = $this->getRawURITokens();
137 $repository = $this->getRepository();
138 if (!$repository) {
139 throw new PhutilInvalidStateException('setRepository');
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()),
149 return $this->newTokensWithVariables($tokens, $variables);
152 private function getRawURITokens() {
153 if (!$this->rawTokens) {
154 $this->rawTokens = $this->newRawURITokens();
156 return $this->rawTokens;
159 private function newRawURITokens() {
160 $raw_pattern = $this->getPattern();
161 $raw_tokens = self::newPatternTokens($raw_pattern);
163 $variable_definitions = self::getVariableDefinitions();
165 foreach ($raw_tokens as $token) {
166 if ($token['type'] !== 'variable') {
167 continue;
170 $value = $token['value'];
172 if (isset($variable_definitions[$value])) {
173 continue;
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));
185 $variables = array(
186 '%' => '%',
189 $tokens = $this->newTokensWithVariables($raw_tokens, $variables);
191 $first_literal = null;
192 if ($tokens) {
193 foreach ($tokens as $token) {
194 if ($token['type'] === 'literal') {
195 $first_literal = $token['value'];
197 break;
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));
210 $uri = new PhutilURI($first_literal);
211 $editor_protocol = $uri->getProtocol();
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));
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));
233 return $tokens;
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'];
242 if ($type == 'variable') {
243 $variable = $token['value'];
244 if (isset($variables[$variable])) {
245 $tokens[$key] = array(
246 'type' => 'literal',
247 'value' => $variables[$variable],
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');
258 if (!$is_literal) {
259 $last_literal = null;
260 continue;
263 if ($last_literal !== null) {
264 $tokens[$key]['value'] =
265 $tokens[$last_literal]['value'].$token['value'];
266 unset($tokens[$last_literal]);
269 $last_literal = $key;
272 $tokens = array_values($tokens);
274 return $tokens;
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 "/../".
281 // Find anything that looks like "/../" and mangle it.
283 $token = preg_replace('((^|/)\.\.(/|\z))', '\1dot-dot\2', $token);
285 return phutil_escape_uri($token);
288 private function newStringFromTokens(array $tokens) {
289 $result = array();
291 foreach ($tokens as $token) {
292 $token_type = $token['type'];
293 $token_value = $token['value'];
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));
305 $result[] = $token_value;
308 $result = implode('', $result);
310 return $result;
313 public static function newPatternTokens($raw_pattern) {
314 $token_positions = array();
316 $len = strlen($raw_pattern);
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));
330 $token_positions[] = $ii;
331 $ii++;
335 // Add a final marker past the end of the string, so we'll collect any
336 // trailing literal bytes.
337 $token_positions[] = $len;
339 $tokens = array();
340 $cursor = 0;
341 foreach ($token_positions as $pos) {
342 $token_len = ($pos - $cursor);
344 if ($token_len > 0) {
345 $tokens[] = array(
346 'type' => 'literal',
347 'value' => substr($raw_pattern, $cursor, $token_len),
351 $cursor = $pos;
353 if ($cursor < $len) {
354 $tokens[] = array(
355 'type' => 'variable',
356 'value' => substr($raw_pattern, $cursor + 1, 1),
360 $cursor = $pos + 2;
363 return $tokens;