Changeset View
Changeset View
Standalone View
Standalone View
src/infrastructure/editor/PhabricatorEditorURIEngine.php
- This file was added.
<?php | |||||
final class PhabricatorEditorURIEngine | |||||
extends Phobject { | |||||
private $viewer; | |||||
private $repository; | |||||
private $pattern; | |||||
private $rawTokens; | |||||
private $repositoryTokens; | |||||
public static function newForViewer(PhabricatorUser $viewer) { | |||||
if (!$viewer->isLoggedIn()) { | |||||
return null; | |||||
} | |||||
$pattern = $viewer->getUserSetting(PhabricatorEditorSetting::SETTINGKEY); | |||||
if (!strlen(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(); | |||||
$variables = array( | |||||
'f' => $this->escapeToken($path), | |||||
'l' => $this->escapeToken($line), | |||||
); | |||||
$tokens = $this->newTokensWithVariables($tokens, $variables); | |||||
return $this->newStringFromTokens($tokens); | |||||
} | |||||
public function getURITokensForRepository() { | |||||
if (!$this->repositoryTokens) { | |||||
$this->repositoryTokens = $this->newURITokensForRepository(); | |||||
} | |||||
return $this->repositoryTokens; | |||||
} | |||||
public static function getVariableDefinitions() { | |||||
return array( | |||||
'%' => array( | |||||
'name' => pht('Literal Percent Symbol'), | |||||
), | |||||
'r' => array( | |||||
'name' => pht('Repository Callsign'), | |||||
), | |||||
'f' => array( | |||||
'name' => pht('File Name'), | |||||
), | |||||
'l' => array( | |||||
'name' => pht('Line Number'), | |||||
), | |||||
); | |||||
} | |||||
private function newURITokensForRepository() { | |||||
$tokens = $this->getRawURITokens(); | |||||
$repository = $this->getRepository(); | |||||
if (!$repository) { | |||||
throw new PhutilInvalidStateException('setRepository'); | |||||
} | |||||
$variables = array( | |||||
'r' => $this->escapeToken($repository->getCallsign()), | |||||
); | |||||
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; | |||||
} | |||||
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; | |||||
} | |||||
} |