Path: blob/master/src/infrastructure/editor/PhabricatorEditorURIEngine.php
12241 views
<?php12final class PhabricatorEditorURIEngine3extends Phobject {45private $viewer;6private $repository;7private $pattern;8private $rawTokens;9private $repositoryTokens;1011public static function newForViewer(PhabricatorUser $viewer) {12if (!$viewer->isLoggedIn()) {13return null;14}1516$pattern = $viewer->getUserSetting(PhabricatorEditorSetting::SETTINGKEY);1718if ($pattern === null || !strlen(trim($pattern))) {19return null;20}2122$engine = id(new self())23->setViewer($viewer)24->setPattern($pattern);2526// If there's a problem with the pattern,2728try {29$engine->validatePattern();30} catch (PhabricatorEditorURIParserException $ex) {31return null;32}3334return $engine;35}3637public function setViewer(PhabricatorUser $viewer) {38$this->viewer = $viewer;39return $this;40}4142public function getViewer() {43return $this->viewer;44}4546public function setRepository(PhabricatorRepository $repository) {47$this->repository = $repository;48return $this;49}5051public function getRepository() {52return $this->repository;53}5455public function setPattern($pattern) {56$this->pattern = $pattern;57return $this;58}5960public function getPattern() {61return $this->pattern;62}6364public function validatePattern() {65$this->getRawURITokens();66return true;67}6869public function getURIForPath($path, $line) {70$tokens = $this->getURITokensForRepository($path);7172$variables = array(73'f' => $this->escapeToken($path),74'l' => $this->escapeToken($line),75);7677$tokens = $this->newTokensWithVariables($tokens, $variables);7879return $this->newStringFromTokens($tokens);80}8182public function getURITokensForPath($path) {83$tokens = $this->getURITokensForRepository($path);8485$variables = array(86'f' => $this->escapeToken($path),87);8889return $this->newTokensWithVariables($tokens, $variables);90}9192public static function getVariableDefinitions() {93return array(94'f' => array(95'name' => pht('File Name'),96'example' => pht('path/to/source.c'),97),98'l' => array(99'name' => pht('Line Number'),100'example' => '777',101),102'n' => array(103'name' => pht('Repository Short Name'),104'example' => 'arcanist',105),106'd' => array(107'name' => pht('Repository ID'),108'example' => '42',109),110'p' => array(111'name' => pht('Repository PHID'),112'example' => 'PHID-REPO-abcdefghijklmnopqrst',113),114'r' => array(115'name' => pht('Repository Callsign'),116'example' => 'XYZ',117),118'%' => array(119'name' => pht('Literal Percent Symbol'),120'example' => '%',121),122);123}124125private function getURITokensForRepository() {126if (!$this->repositoryTokens) {127$this->repositoryTokens = $this->newURITokensForRepository();128}129130return $this->repositoryTokens;131}132133private function newURITokensForRepository() {134$tokens = $this->getRawURITokens();135136$repository = $this->getRepository();137if (!$repository) {138throw new PhutilInvalidStateException('setRepository');139}140141$variables = array(142'r' => $this->escapeToken($repository->getCallsign()),143'n' => $this->escapeToken($repository->getRepositorySlug()),144'd' => $this->escapeToken($repository->getID()),145'p' => $this->escapeToken($repository->getPHID()),146);147148return $this->newTokensWithVariables($tokens, $variables);149}150151private function getRawURITokens() {152if (!$this->rawTokens) {153$this->rawTokens = $this->newRawURITokens();154}155return $this->rawTokens;156}157158private function newRawURITokens() {159$raw_pattern = $this->getPattern();160$raw_tokens = self::newPatternTokens($raw_pattern);161162$variable_definitions = self::getVariableDefinitions();163164foreach ($raw_tokens as $token) {165if ($token['type'] !== 'variable') {166continue;167}168169$value = $token['value'];170171if (isset($variable_definitions[$value])) {172continue;173}174175throw new PhabricatorEditorURIParserException(176pht(177'Editor pattern "%s" is invalid: the pattern contains an '.178'unrecognized variable ("%s"). Use "%%%%" to encode a literal '.179'percent symbol.',180$raw_pattern,181'%'.$value));182}183184$variables = array(185'%' => '%',186);187188$tokens = $this->newTokensWithVariables($raw_tokens, $variables);189190$first_literal = null;191if ($tokens) {192foreach ($tokens as $token) {193if ($token['type'] === 'literal') {194$first_literal = $token['value'];195}196break;197}198199if ($first_literal === null) {200throw new PhabricatorEditorURIParserException(201pht(202'Editor pattern "%s" is invalid: the pattern must begin with '.203'a valid editor protocol, but begins with a variable. This is '.204'very sneaky and also very forbidden.',205$raw_pattern));206}207}208209$uri = new PhutilURI($first_literal);210$editor_protocol = $uri->getProtocol();211212if (!$editor_protocol) {213throw new PhabricatorEditorURIParserException(214pht(215'Editor pattern "%s" is invalid: the pattern must begin with '.216'a valid editor protocol, but does not begin with a recognized '.217'protocol string.',218$raw_pattern));219}220221$allowed_key = 'uri.allowed-editor-protocols';222$allowed_protocols = PhabricatorEnv::getEnvConfig($allowed_key);223if (empty($allowed_protocols[$editor_protocol])) {224throw new PhabricatorEditorURIParserException(225pht(226'Editor pattern "%s" is invalid: the pattern must begin with '.227'a valid editor protocol, but the protocol "%s://" is not allowed.',228$raw_pattern,229$editor_protocol));230}231232return $tokens;233}234235private function newTokensWithVariables(array $tokens, array $variables) {236// Replace all "variable" tokens that we have replacements for with237// the literal value.238foreach ($tokens as $key => $token) {239$type = $token['type'];240241if ($type == 'variable') {242$variable = $token['value'];243if (isset($variables[$variable])) {244$tokens[$key] = array(245'type' => 'literal',246'value' => $variables[$variable],247);248}249}250}251252// Now, merge sequences of adjacent "literal" tokens into a single token.253$last_literal = null;254foreach ($tokens as $key => $token) {255$is_literal = ($token['type'] === 'literal');256257if (!$is_literal) {258$last_literal = null;259continue;260}261262if ($last_literal !== null) {263$tokens[$key]['value'] =264$tokens[$last_literal]['value'].$token['value'];265unset($tokens[$last_literal]);266}267268$last_literal = $key;269}270271$tokens = array_values($tokens);272273return $tokens;274}275276private function escapeToken($token) {277// Paths are user controlled, so a clever user could potentially make278// editor links do surprising things with paths containing "/../".279280// Find anything that looks like "/../" and mangle it.281282$token = preg_replace('((^|/)\.\.(/|\z))', '\1dot-dot\2', $token);283284return phutil_escape_uri($token);285}286287private function newStringFromTokens(array $tokens) {288$result = array();289290foreach ($tokens as $token) {291$token_type = $token['type'];292$token_value = $token['value'];293294$is_literal = ($token_type === 'literal');295if (!$is_literal) {296throw new Exception(297pht(298'Editor pattern token list can not be converted into a string: '.299'it still contains a non-literal token ("%s", of type "%s").',300$token_value,301$token_type));302}303304$result[] = $token_value;305}306307$result = implode('', $result);308309return $result;310}311312public static function newPatternTokens($raw_pattern) {313$token_positions = array();314315$len = strlen($raw_pattern);316317for ($ii = 0; $ii < $len; $ii++) {318$c = $raw_pattern[$ii];319if ($c === '%') {320if (!isset($raw_pattern[$ii + 1])) {321throw new PhabricatorEditorURIParserException(322pht(323'Editor pattern "%s" is invalid: the final character in a '.324'pattern may not be an unencoded percent symbol ("%%"). '.325'Use "%%%%" to encode a literal percent symbol.',326$raw_pattern));327}328329$token_positions[] = $ii;330$ii++;331}332}333334// Add a final marker past the end of the string, so we'll collect any335// trailing literal bytes.336$token_positions[] = $len;337338$tokens = array();339$cursor = 0;340foreach ($token_positions as $pos) {341$token_len = ($pos - $cursor);342343if ($token_len > 0) {344$tokens[] = array(345'type' => 'literal',346'value' => substr($raw_pattern, $cursor, $token_len),347);348}349350$cursor = $pos;351352if ($cursor < $len) {353$tokens[] = array(354'type' => 'variable',355'value' => substr($raw_pattern, $cursor + 1, 1),356);357}358359$cursor = $pos + 2;360}361362return $tokens;363}364365}366367368