@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
at recaptime-dev/main 369 lines 9.4 kB view raw
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}