@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
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Tokenize external editor links so they can be safely materialized on the client

Summary:
Ref T13515. Currently, opening a file to a particular line in an external editor relies on replacing "%l" with "%l" (which is escaped as "%25l") on the server, and then replacing "%25l" with the line number on the client. This will fail if the file path (or any other variable) contains "%l" in its unencoded form.

The parser also can't identify invalid variables.

Pull the parser out, formalize it, and make it generate an intermediate representation which can be sent to the client and reconstituted.

(This temporarily breaks Diffusion and permanently removes the weird, ancient integration in Dark Console.)

Test Plan:
- Added a bunch of tests for the actual parser.
- Used "Open in Editor" in Differential.

Maniphest Tasks: T13515

Differential Revision: https://secure.phabricator.com/D21143

+494 -126
+6 -2
src/__phutil_library_map__.php
··· 3275 3275 'PhabricatorEditorExtensionModule' => 'applications/transactions/engineextension/PhabricatorEditorExtensionModule.php', 3276 3276 'PhabricatorEditorMailEngineExtension' => 'applications/transactions/engineextension/PhabricatorEditorMailEngineExtension.php', 3277 3277 'PhabricatorEditorSetting' => 'applications/settings/setting/PhabricatorEditorSetting.php', 3278 + 'PhabricatorEditorURIEngine' => 'infrastructure/editor/PhabricatorEditorURIEngine.php', 3279 + 'PhabricatorEditorURIEngineTestCase' => 'infrastructure/editor/__tests__/PhabricatorEditorURIEngineTestCase.php', 3280 + 'PhabricatorEditorURIParserException' => 'infrastructure/editor/PhabricatorEditorURIParserException.php', 3278 3281 'PhabricatorElasticFulltextStorageEngine' => 'applications/search/fulltextstorage/PhabricatorElasticFulltextStorageEngine.php', 3279 3282 'PhabricatorElasticsearchHost' => 'infrastructure/cluster/search/PhabricatorElasticsearchHost.php', 3280 3283 'PhabricatorElasticsearchQueryBuilder' => 'applications/search/fulltextstorage/PhabricatorElasticsearchQueryBuilder.php', ··· 3543 3546 'PhabricatorHelpApplication' => 'applications/help/application/PhabricatorHelpApplication.php', 3544 3547 'PhabricatorHelpController' => 'applications/help/controller/PhabricatorHelpController.php', 3545 3548 'PhabricatorHelpDocumentationController' => 'applications/help/controller/PhabricatorHelpDocumentationController.php', 3546 - 'PhabricatorHelpEditorProtocolController' => 'applications/help/controller/PhabricatorHelpEditorProtocolController.php', 3547 3549 'PhabricatorHelpKeyboardShortcutController' => 'applications/help/controller/PhabricatorHelpKeyboardShortcutController.php', 3548 3550 'PhabricatorHeraldApplication' => 'applications/herald/application/PhabricatorHeraldApplication.php', 3549 3551 'PhabricatorHeraldContentSource' => 'applications/herald/contentsource/PhabricatorHeraldContentSource.php', ··· 9735 9737 'PhabricatorEditorExtensionModule' => 'PhabricatorConfigModule', 9736 9738 'PhabricatorEditorMailEngineExtension' => 'PhabricatorMailEngineExtension', 9737 9739 'PhabricatorEditorSetting' => 'PhabricatorStringSetting', 9740 + 'PhabricatorEditorURIEngine' => 'Phobject', 9741 + 'PhabricatorEditorURIEngineTestCase' => 'PhabricatorTestCase', 9742 + 'PhabricatorEditorURIParserException' => 'Exception', 9738 9743 'PhabricatorElasticFulltextStorageEngine' => 'PhabricatorFulltextStorageEngine', 9739 9744 'PhabricatorElasticsearchHost' => 'PhabricatorSearchHost', 9740 9745 'PhabricatorElasticsearchSetupCheck' => 'PhabricatorSetupCheck', ··· 10054 10059 'PhabricatorHelpApplication' => 'PhabricatorApplication', 10055 10060 'PhabricatorHelpController' => 'PhabricatorController', 10056 10061 'PhabricatorHelpDocumentationController' => 'PhabricatorHelpController', 10057 - 'PhabricatorHelpEditorProtocolController' => 'PhabricatorHelpController', 10058 10062 'PhabricatorHelpKeyboardShortcutController' => 'PhabricatorHelpController', 10059 10063 'PhabricatorHeraldApplication' => 'PhabricatorApplication', 10060 10064 'PhabricatorHeraldContentSource' => 'PhabricatorContentSource',
+2 -13
src/applications/console/plugin/DarkConsoleErrorLogPlugin.php
··· 65 65 $href = null; 66 66 if (isset($entry['file'])) { 67 67 $line .= ' called at ['.$entry['file'].':'.$entry['line'].']'; 68 - try { 69 - $user = $this->getRequest()->getUser(); 70 - $href = $user->loadEditorLink($entry['file'], $entry['line'], null); 71 - } catch (Exception $ex) { 72 - // The database can be inaccessible. 73 - } 68 + 74 69 } 75 - 76 - $details[] = phutil_tag( 77 - 'a', 78 - array( 79 - 'href' => $href, 80 - ), 81 - $line); 70 + $details[] = $line; 82 71 $details[] = "\n"; 83 72 } 84 73
+9 -6
src/applications/differential/view/DifferentialChangesetDetailView.php
··· 252 252 } 253 253 254 254 private function getEditorURI() { 255 + $repository = $this->getRepository(); 256 + if (!$repository) { 257 + return null; 258 + } 259 + 255 260 $viewer = $this->getViewer(); 256 261 257 - if (!$viewer->isLoggedIn()) { 262 + $link_engine = PhabricatorEditorURIEngine::newForViewer($viewer); 263 + if (!$link_engine) { 258 264 return null; 259 265 } 260 266 261 - $repository = $this->getRepository(); 262 - if (!$repository) { 263 - return null; 264 - } 267 + $link_engine->setRepository($repository); 265 268 266 269 $changeset = $this->getChangeset(); 267 270 $diff = $this->getDiff(); ··· 271 274 272 275 $line = idx($changeset->getMetadata(), 'line:first', 1); 273 276 274 - return $viewer->loadEditorLink($path, $line, $repository); 277 + return $link_engine->getURIForPath($path, $line); 275 278 } 276 279 277 280 private function getEditorConfigureURI() {
+3 -2
src/applications/diffusion/controller/DiffusionBrowseController.php
··· 478 478 $line = nonempty((int)$drequest->getLine(), 1); 479 479 $buttons = array(); 480 480 481 - $editor_link = $user->loadEditorLink($path, $line, $repository); 482 - $template = $user->loadEditorLink($path, '%l', $repository); 481 + // TODO: Restore these behaviors. 482 + $editor_link = null; 483 + $template = null; 483 484 484 485 $buttons[] = 485 486 id(new PHUIButtonView())
-1
src/applications/help/application/PhabricatorHelpApplication.php
··· 18 18 return array( 19 19 '/help/' => array( 20 20 'keyboardshortcut/' => 'PhabricatorHelpKeyboardShortcutController', 21 - 'editorprotocol/' => 'PhabricatorHelpEditorProtocolController', 22 21 'documentation/(?P<application>\w+)/' 23 22 => 'PhabricatorHelpDocumentationController', 24 23 ),
-47
src/applications/help/controller/PhabricatorHelpEditorProtocolController.php
··· 1 - <?php 2 - 3 - final class PhabricatorHelpEditorProtocolController 4 - extends PhabricatorHelpController { 5 - 6 - public function shouldAllowPublic() { 7 - return true; 8 - } 9 - 10 - public function handleRequest(AphrontRequest $request) { 11 - $viewer = $request->getViewer(); 12 - 13 - return $this->newDialog() 14 - ->setMethod('GET') 15 - ->setSubmitURI('/settings/panel/display/') 16 - ->setTitle(pht('Unsupported Editor Protocol')) 17 - ->appendParagraph( 18 - pht( 19 - 'Your configured editor URI uses an unsupported protocol. Change '. 20 - 'your settings to use a supported protocol, or ask your '. 21 - 'administrator to add support for the chosen protocol by '. 22 - 'configuring: %s', 23 - phutil_tag('tt', array(), 'uri.allowed-editor-protocols'))) 24 - ->addSubmitButton(pht('Change Settings')) 25 - ->addCancelButton('/'); 26 - } 27 - 28 - public static function hasAllowedProtocol($uri) { 29 - $uri = new PhutilURI($uri); 30 - $editor_protocol = $uri->getProtocol(); 31 - if (!$editor_protocol) { 32 - // The URI must have a protocol. 33 - return false; 34 - } 35 - 36 - $allowed_key = 'uri.allowed-editor-protocols'; 37 - $allowed_protocols = PhabricatorEnv::getEnvConfig($allowed_key); 38 - if (empty($allowed_protocols[$editor_protocol])) { 39 - // The protocol must be on the allowed protocol whitelist. 40 - return false; 41 - } 42 - 43 - return true; 44 - } 45 - 46 - 47 - }
-35
src/applications/people/storage/PhabricatorUser.php
··· 471 471 return $this->getUserSetting(PhabricatorPronounSetting::SETTINGKEY); 472 472 } 473 473 474 - public function loadEditorLink( 475 - $path, 476 - $line, 477 - PhabricatorRepository $repository = null) { 478 - 479 - $editor = $this->getUserSetting(PhabricatorEditorSetting::SETTINGKEY); 480 - 481 - if (!strlen($editor)) { 482 - return null; 483 - } 484 - 485 - if ($repository) { 486 - $callsign = $repository->getCallsign(); 487 - } else { 488 - $callsign = null; 489 - } 490 - 491 - $uri = strtr($editor, array( 492 - '%%' => '%', 493 - '%f' => phutil_escape_uri($path), 494 - '%l' => phutil_escape_uri($line), 495 - '%r' => phutil_escape_uri($callsign), 496 - )); 497 - 498 - // The resulting URI must have an allowed protocol. Otherwise, we'll return 499 - // a link to an error page explaining the misconfiguration. 500 - 501 - $ok = PhabricatorHelpEditorProtocolController::hasAllowedProtocol($uri); 502 - if (!$ok) { 503 - return '/help/editorprotocol/'; 504 - } 505 - 506 - return (string)$uri; 507 - } 508 - 509 474 /** 510 475 * Populate the nametoken table, which used to fetch typeahead results. When 511 476 * a user types "linc", we want to match "Abraham Lincoln" from on-demand
+3 -20
src/applications/settings/setting/PhabricatorEditorSetting.php
··· 43 43 return; 44 44 } 45 45 46 - $ok = PhabricatorHelpEditorProtocolController::hasAllowedProtocol($value); 47 - if ($ok) { 48 - return; 49 - } 50 - 51 - $allowed_key = 'uri.allowed-editor-protocols'; 52 - $allowed_protocols = PhabricatorEnv::getEnvConfig($allowed_key); 53 - 54 - $proto_names = array(); 55 - foreach (array_keys($allowed_protocols) as $protocol) { 56 - $proto_names[] = $protocol.'://'; 57 - } 58 - 59 - throw new Exception( 60 - pht( 61 - 'Editor link has an invalid or missing protocol. You must '. 62 - 'use a whitelisted editor protocol from this list: %s. To '. 63 - 'add protocols, update "%s" in Config.', 64 - implode(', ', $proto_names), 65 - $allowed_key)); 46 + id(new PhabricatorEditorURIEngine()) 47 + ->setPattern($value) 48 + ->validatePattern(); 66 49 } 67 50 68 51 }
+335
src/infrastructure/editor/PhabricatorEditorURIEngine.php
··· 1 + <?php 2 + 3 + final 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 (!strlen(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 getURITokensForRepository() { 84 + if (!$this->repositoryTokens) { 85 + $this->repositoryTokens = $this->newURITokensForRepository(); 86 + } 87 + 88 + return $this->repositoryTokens; 89 + } 90 + 91 + public static function getVariableDefinitions() { 92 + return array( 93 + '%' => array( 94 + 'name' => pht('Literal Percent Symbol'), 95 + ), 96 + 'r' => array( 97 + 'name' => pht('Repository Callsign'), 98 + ), 99 + 'f' => array( 100 + 'name' => pht('File Name'), 101 + ), 102 + 'l' => array( 103 + 'name' => pht('Line Number'), 104 + ), 105 + ); 106 + } 107 + 108 + private function newURITokensForRepository() { 109 + $tokens = $this->getRawURITokens(); 110 + 111 + $repository = $this->getRepository(); 112 + if (!$repository) { 113 + throw new PhutilInvalidStateException('setRepository'); 114 + } 115 + 116 + $variables = array( 117 + 'r' => $this->escapeToken($repository->getCallsign()), 118 + ); 119 + 120 + return $this->newTokensWithVariables($tokens, $variables); 121 + } 122 + 123 + private function getRawURITokens() { 124 + if (!$this->rawTokens) { 125 + $this->rawTokens = $this->newRawURITokens(); 126 + } 127 + return $this->rawTokens; 128 + } 129 + 130 + private function newRawURITokens() { 131 + $raw_pattern = $this->getPattern(); 132 + $raw_tokens = self::newPatternTokens($raw_pattern); 133 + 134 + $variable_definitions = self::getVariableDefinitions(); 135 + 136 + foreach ($raw_tokens as $token) { 137 + if ($token['type'] !== 'variable') { 138 + continue; 139 + } 140 + 141 + $value = $token['value']; 142 + 143 + if (isset($variable_definitions[$value])) { 144 + continue; 145 + } 146 + 147 + throw new PhabricatorEditorURIParserException( 148 + pht( 149 + 'Editor pattern "%s" is invalid: the pattern contains an '. 150 + 'unrecognized variable ("%s"). Use "%%%%" to encode a literal '. 151 + 'percent symbol.', 152 + $raw_pattern, 153 + '%'.$value)); 154 + } 155 + 156 + $variables = array( 157 + '%' => '%', 158 + ); 159 + 160 + $tokens = $this->newTokensWithVariables($raw_tokens, $variables); 161 + 162 + $first_literal = null; 163 + if ($tokens) { 164 + foreach ($tokens as $token) { 165 + if ($token['type'] === 'literal') { 166 + $first_literal = $token['value']; 167 + } 168 + break; 169 + } 170 + 171 + if ($first_literal === null) { 172 + throw new PhabricatorEditorURIParserException( 173 + pht( 174 + 'Editor pattern "%s" is invalid: the pattern must begin with '. 175 + 'a valid editor protocol, but begins with a variable. This is '. 176 + 'very sneaky and also very forbidden.', 177 + $raw_pattern)); 178 + } 179 + } 180 + 181 + $uri = new PhutilURI($first_literal); 182 + $editor_protocol = $uri->getProtocol(); 183 + 184 + if (!$editor_protocol) { 185 + throw new PhabricatorEditorURIParserException( 186 + pht( 187 + 'Editor pattern "%s" is invalid: the pattern must begin with '. 188 + 'a valid editor protocol, but does not begin with a recognized '. 189 + 'protocol string.', 190 + $raw_pattern)); 191 + } 192 + 193 + $allowed_key = 'uri.allowed-editor-protocols'; 194 + $allowed_protocols = PhabricatorEnv::getEnvConfig($allowed_key); 195 + if (empty($allowed_protocols[$editor_protocol])) { 196 + throw new PhabricatorEditorURIParserException( 197 + pht( 198 + 'Editor pattern "%s" is invalid: the pattern must begin with '. 199 + 'a valid editor protocol, but the protocol "%s://" is not allowed.', 200 + $raw_pattern, 201 + $editor_protocol)); 202 + } 203 + 204 + return $tokens; 205 + } 206 + 207 + private function newTokensWithVariables(array $tokens, array $variables) { 208 + // Replace all "variable" tokens that we have replacements for with 209 + // the literal value. 210 + foreach ($tokens as $key => $token) { 211 + $type = $token['type']; 212 + 213 + if ($type == 'variable') { 214 + $variable = $token['value']; 215 + if (isset($variables[$variable])) { 216 + $tokens[$key] = array( 217 + 'type' => 'literal', 218 + 'value' => $variables[$variable], 219 + ); 220 + } 221 + } 222 + } 223 + 224 + // Now, merge sequences of adjacent "literal" tokens into a single token. 225 + $last_literal = null; 226 + foreach ($tokens as $key => $token) { 227 + $is_literal = ($token['type'] === 'literal'); 228 + 229 + if (!$is_literal) { 230 + $last_literal = null; 231 + continue; 232 + } 233 + 234 + if ($last_literal !== null) { 235 + $tokens[$key]['value'] = 236 + $tokens[$last_literal]['value'].$token['value']; 237 + unset($tokens[$last_literal]); 238 + } 239 + 240 + $last_literal = $key; 241 + } 242 + 243 + return $tokens; 244 + } 245 + 246 + private function escapeToken($token) { 247 + // Paths are user controlled, so a clever user could potentially make 248 + // editor links do surprising things with paths containing "/../". 249 + 250 + // Find anything that looks like "/../" and mangle it. 251 + 252 + $token = preg_replace('((^|/)\.\.(/|\z))', '\1dot-dot\2', $token); 253 + 254 + return phutil_escape_uri($token); 255 + } 256 + 257 + private function newStringFromTokens(array $tokens) { 258 + $result = array(); 259 + 260 + foreach ($tokens as $token) { 261 + $token_type = $token['type']; 262 + $token_value = $token['value']; 263 + 264 + $is_literal = ($token_type === 'literal'); 265 + if (!$is_literal) { 266 + throw new Exception( 267 + pht( 268 + 'Editor pattern token list can not be converted into a string: '. 269 + 'it still contains a non-literal token ("%s", of type "%s").', 270 + $token_value, 271 + $token_type)); 272 + } 273 + 274 + $result[] = $token_value; 275 + } 276 + 277 + $result = implode('', $result); 278 + 279 + return $result; 280 + } 281 + 282 + public static function newPatternTokens($raw_pattern) { 283 + $token_positions = array(); 284 + 285 + $len = strlen($raw_pattern); 286 + 287 + for ($ii = 0; $ii < $len; $ii++) { 288 + $c = $raw_pattern[$ii]; 289 + if ($c === '%') { 290 + if (!isset($raw_pattern[$ii + 1])) { 291 + throw new PhabricatorEditorURIParserException( 292 + pht( 293 + 'Editor pattern "%s" is invalid: the final character in a '. 294 + 'pattern may not be an unencoded percent symbol ("%%"). '. 295 + 'Use "%%%%" to encode a literal percent symbol.', 296 + $raw_pattern)); 297 + } 298 + 299 + $token_positions[] = $ii; 300 + $ii++; 301 + } 302 + } 303 + 304 + // Add a final marker past the end of the string, so we'll collect any 305 + // trailing literal bytes. 306 + $token_positions[] = $len; 307 + 308 + $tokens = array(); 309 + $cursor = 0; 310 + foreach ($token_positions as $pos) { 311 + $token_len = ($pos - $cursor); 312 + 313 + if ($token_len > 0) { 314 + $tokens[] = array( 315 + 'type' => 'literal', 316 + 'value' => substr($raw_pattern, $cursor, $token_len), 317 + ); 318 + } 319 + 320 + $cursor = $pos; 321 + 322 + if ($cursor < $len) { 323 + $tokens[] = array( 324 + 'type' => 'variable', 325 + 'value' => substr($raw_pattern, $cursor + 1, 1), 326 + ); 327 + } 328 + 329 + $cursor = $pos + 2; 330 + } 331 + 332 + return $tokens; 333 + } 334 + 335 + }
+4
src/infrastructure/editor/PhabricatorEditorURIParserException.php
··· 1 + <?php 2 + 3 + final class PhabricatorEditorURIParserException 4 + extends Exception {}
+132
src/infrastructure/editor/__tests__/PhabricatorEditorURIEngineTestCase.php
··· 1 + <?php 2 + 3 + final class PhabricatorEditorURIEngineTestCase 4 + extends PhabricatorTestCase { 5 + 6 + public function testPatternParsing() { 7 + $map = array( 8 + '' => array(), 9 + '%' => false, 10 + 'aaa%' => false, 11 + 'quack' => array( 12 + array( 13 + 'type' => 'literal', 14 + 'value' => 'quack', 15 + ), 16 + ), 17 + '%a' => array( 18 + array( 19 + 'type' => 'variable', 20 + 'value' => 'a', 21 + ), 22 + ), 23 + '%%' => array( 24 + array( 25 + 'type' => 'variable', 26 + 'value' => '%', 27 + ), 28 + ), 29 + 'x%y' => array( 30 + array( 31 + 'type' => 'literal', 32 + 'value' => 'x', 33 + ), 34 + array( 35 + 'type' => 'variable', 36 + 'value' => 'y', 37 + ), 38 + ), 39 + '%xy' => array( 40 + array( 41 + 'type' => 'variable', 42 + 'value' => 'x', 43 + ), 44 + array( 45 + 'type' => 'literal', 46 + 'value' => 'y', 47 + ), 48 + ), 49 + 'x%yz' => array( 50 + array( 51 + 'type' => 'literal', 52 + 'value' => 'x', 53 + ), 54 + array( 55 + 'type' => 'variable', 56 + 'value' => 'y', 57 + ), 58 + array( 59 + 'type' => 'literal', 60 + 'value' => 'z', 61 + ), 62 + ), 63 + ); 64 + 65 + foreach ($map as $input => $expect) { 66 + try { 67 + $actual = PhabricatorEditorURIEngine::newPatternTokens($input); 68 + } catch (Exception $ex) { 69 + if ($expect !== false) { 70 + throw $ex; 71 + } 72 + $actual = false; 73 + } 74 + 75 + $this->assertEqual( 76 + $expect, 77 + $actual, 78 + pht('Parse of: %s', $input)); 79 + } 80 + } 81 + 82 + public function testPatternProtocols() { 83 + $protocols = array( 84 + 'xyz', 85 + ); 86 + $protocols = array_fuse($protocols); 87 + 88 + $env = PhabricatorEnv::beginScopedEnv(); 89 + $env->overrideEnvConfig('uri.allowed-editor-protocols', $protocols); 90 + 91 + $map = array( 92 + 'xyz:' => true, 93 + 'xyz:%%' => true, 94 + 'xyz://a' => true, 95 + 'xyz://open/?file=%f' => true, 96 + 97 + '' => false, 98 + '%%' => false, 99 + '%r' => false, 100 + 'aaa' => false, 101 + 'xyz%%://' => false, 102 + 'http://' => false, 103 + 104 + // These are fragments that "PhutilURI" can't figure out the protocol 105 + // for. In theory, they would be safe to allow, they just probably are 106 + // not very useful. 107 + 108 + 'xyz://' => false, 109 + 'xyz://%%' => false, 110 + ); 111 + 112 + foreach ($map as $input => $expect) { 113 + try { 114 + id(new PhabricatorEditorURIEngine()) 115 + ->setPattern($input) 116 + ->validatePattern(); 117 + 118 + $actual = true; 119 + } catch (PhabricatorEditorURIParserException $ex) { 120 + $actual = false; 121 + } 122 + 123 + $this->assertEqual( 124 + $expect, 125 + $actual, 126 + pht( 127 + 'Allowed editor "xyz://" template: %s.', 128 + $input)); 129 + } 130 + } 131 + 132 + }