isLoggedIn()) { return null; } $pattern = $viewer->getUserSetting(PhabricatorEditorSetting::SETTINGKEY); if ($pattern === null || trim($pattern) === '') { return null; } $engine = id(new self()) ->setViewer($viewer) ->setPattern($pattern); // If there's a problem with the pattern, try { $engine->validatePattern(); } catch (PhabricatorEditorURIParserException $ex) { return null; } return $engine; } public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; return $this; } public function getViewer() { return $this->viewer; } public function setRepository(PhabricatorRepository $repository) { $this->repository = $repository; return $this; } public function getRepository() { return $this->repository; } public function setPattern($pattern) { $this->pattern = $pattern; return $this; } public function getPattern() { return $this->pattern; } public function validatePattern() { $this->getRawURITokens(); return true; } public function getURIForPath($path, $line) { $tokens = $this->getURITokensForRepository($path); $variables = array( 'f' => $this->escapeToken($path), 'l' => $this->escapeToken($line), ); $tokens = $this->newTokensWithVariables($tokens, $variables); return $this->newStringFromTokens($tokens); } public function getURITokensForPath($path) { $tokens = $this->getURITokensForRepository($path); $variables = array( 'f' => $this->escapeToken($path), ); return $this->newTokensWithVariables($tokens, $variables); } public static function getVariableDefinitions() { return array( 'f' => array( 'name' => pht('File Name'), 'example' => pht('path/to/source.c'), ), 'l' => array( 'name' => pht('Line Number'), 'example' => '777', ), 'n' => array( 'name' => pht('Repository Short Name'), 'example' => 'arcanist', ), 'd' => array( 'name' => pht('Repository ID'), 'example' => '42', ), 'p' => array( 'name' => pht('Repository PHID'), 'example' => 'PHID-REPO-abcdefghijklmnopqrst', ), 'r' => array( 'name' => pht('Repository Callsign'), 'example' => 'XYZ', ), '%' => array( 'name' => pht('Literal Percent Symbol'), 'example' => '%', ), ); } private function getURITokensForRepository() { if (!$this->repositoryTokens) { $this->repositoryTokens = $this->newURITokensForRepository(); } return $this->repositoryTokens; } private function newURITokensForRepository() { $tokens = $this->getRawURITokens(); $repository = $this->getRepository(); if (!$repository) { throw new PhutilInvalidStateException('setRepository'); } $variables = array( 'r' => $this->escapeToken($repository->getCallsign()), 'n' => $this->escapeToken($repository->getRepositorySlug()), 'd' => $this->escapeToken($repository->getID()), 'p' => $this->escapeToken($repository->getPHID()), ); return $this->newTokensWithVariables($tokens, $variables); } private function getRawURITokens() { if (!$this->rawTokens) { $this->rawTokens = $this->newRawURITokens(); } return $this->rawTokens; } private function newRawURITokens() { $raw_pattern = $this->getPattern(); $raw_tokens = self::newPatternTokens($raw_pattern); $variable_definitions = self::getVariableDefinitions(); foreach ($raw_tokens as $token) { if ($token['type'] !== 'variable') { continue; } $value = $token['value']; if (isset($variable_definitions[$value])) { continue; } throw new PhabricatorEditorURIParserException( pht( 'Editor pattern "%s" is invalid: the pattern contains an '. 'unrecognized variable ("%s"). Use "%%%%" to encode a literal '. 'percent symbol.', $raw_pattern, '%'.$value)); } $variables = array( '%' => '%', ); $tokens = $this->newTokensWithVariables($raw_tokens, $variables); $first_literal = null; if ($tokens) { foreach ($tokens as $token) { if ($token['type'] === 'literal') { $first_literal = $token['value']; } break; } if ($first_literal === null) { throw new PhabricatorEditorURIParserException( pht( 'Editor pattern "%s" is invalid: the pattern must begin with '. 'a valid editor protocol, but begins with a variable. This is '. 'very sneaky and also very forbidden.', $raw_pattern)); } } $uri = new PhutilURI($first_literal); $editor_protocol = $uri->getProtocol(); if (!$editor_protocol) { throw new PhabricatorEditorURIParserException( pht( 'Editor pattern "%s" is invalid: the pattern must begin with '. 'a valid editor protocol, but does not begin with a recognized '. 'protocol string.', $raw_pattern)); } $allowed_key = 'uri.allowed-editor-protocols'; $allowed_protocols = PhabricatorEnv::getEnvConfig($allowed_key); if (empty($allowed_protocols[$editor_protocol])) { throw new PhabricatorEditorURIParserException( pht( 'Editor pattern "%s" is invalid: the pattern must begin with '. 'a valid editor protocol, but the protocol "%s://" is not allowed.', $raw_pattern, $editor_protocol)); } return $tokens; } private function newTokensWithVariables(array $tokens, array $variables) { // Replace all "variable" tokens that we have replacements for with // the literal value. foreach ($tokens as $key => $token) { $type = $token['type']; if ($type == 'variable') { $variable = $token['value']; if (isset($variables[$variable])) { $tokens[$key] = array( 'type' => 'literal', 'value' => $variables[$variable], ); } } } // Now, merge sequences of adjacent "literal" tokens into a single token. $last_literal = null; foreach ($tokens as $key => $token) { $is_literal = ($token['type'] === 'literal'); if (!$is_literal) { $last_literal = null; continue; } if ($last_literal !== null) { $tokens[$key]['value'] = $tokens[$last_literal]['value'].$token['value']; unset($tokens[$last_literal]); } $last_literal = $key; } $tokens = array_values($tokens); return $tokens; } private function escapeToken($token) { // Paths are user controlled, so a clever user could potentially make // editor links do surprising things with paths containing "/../". // Find anything that looks like "/../" and mangle it. $token = preg_replace('((^|/)\.\.(/|\z))', '\1dot-dot\2', $token); return phutil_escape_uri($token); } private function newStringFromTokens(array $tokens) { $result = array(); foreach ($tokens as $token) { $token_type = $token['type']; $token_value = $token['value']; $is_literal = ($token_type === 'literal'); if (!$is_literal) { throw new Exception( pht( 'Editor pattern token list can not be converted into a string: '. 'it still contains a non-literal token ("%s", of type "%s").', $token_value, $token_type)); } $result[] = $token_value; } $result = implode('', $result); return $result; } public static function newPatternTokens($raw_pattern) { $token_positions = array(); $len = strlen($raw_pattern); for ($ii = 0; $ii < $len; $ii++) { $c = $raw_pattern[$ii]; if ($c === '%') { if (!isset($raw_pattern[$ii + 1])) { throw new PhabricatorEditorURIParserException( pht( 'Editor pattern "%s" is invalid: the final character in a '. 'pattern may not be an unencoded percent symbol ("%%"). '. 'Use "%%%%" to encode a literal percent symbol.', $raw_pattern)); } $token_positions[] = $ii; $ii++; } } // Add a final marker past the end of the string, so we'll collect any // trailing literal bytes. $token_positions[] = $len; $tokens = array(); $cursor = 0; foreach ($token_positions as $pos) { $token_len = ($pos - $cursor); if ($token_len > 0) { $tokens[] = array( 'type' => 'literal', 'value' => substr($raw_pattern, $cursor, $token_len), ); } $cursor = $pos; if ($cursor < $len) { $tokens[] = array( 'type' => 'variable', 'value' => substr($raw_pattern, $cursor + 1, 1), ); } $cursor = $pos + 2; } return $tokens; } }