@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.

Enforce sensible, unique clone/checkout names for repositories

Summary:
Fixes T7938.

- Primarily, users can currently shoot themselves in the foot by putting `../../etc/passwd` and other similar nonsense in these fields (this is not dangerous, but also does not work). Require sensible names.
- Enforce uniqueness so these names can be used in URIs and as identifiers in the future.
- (This doesn't start actually using them for anything fancy yet.)

Test Plan:
- Gave several repositories clone names: a valid name, two duplicate names, an invalid, name, some with no names.
- Ran migrations.
- Got clean conversion for valid names, appropriate errors for invalid/duplicate names.

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T7938

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

+299 -15
+5
resources/sql/autopatches/20160110.repo.01.slug.sql
··· 1 + ALTER TABLE {$NAMESPACE}_repository.repository 2 + ADD repositorySlug VARCHAR(64) COLLATE {$COLLATE_SORT}; 3 + 4 + ALTER TABLE {$NAMESPACE}_repository.repository 5 + ADD UNIQUE KEY `key_slug` (repositorySlug);
+49
resources/sql/autopatches/20160110.repo.02.slug.php
··· 1 + <?php 2 + 3 + $table = new PhabricatorRepository(); 4 + $conn_w = $table->establishConnection('w'); 5 + 6 + foreach (new LiskMigrationIterator($table) as $repository) { 7 + $slug = $repository->getRepositorySlug(); 8 + 9 + if ($slug !== null) { 10 + continue; 11 + } 12 + 13 + $clone_name = $repository->getDetail('clone-name'); 14 + 15 + if (!strlen($clone_name)) { 16 + continue; 17 + } 18 + 19 + if (!PhabricatorRepository::isValidRepositorySlug($clone_name)) { 20 + echo tsprintf( 21 + "%s\n", 22 + pht( 23 + 'Repository "%s" has a "Clone/Checkout As" name which is no longer '. 24 + 'valid ("%s"). You can edit the repository to give it a new, valid '. 25 + 'short name.', 26 + $repository->getDisplayName(), 27 + $clone_name)); 28 + continue; 29 + } 30 + 31 + try { 32 + queryfx( 33 + $conn_w, 34 + 'UPDATE %T SET repositorySlug = %s WHERE id = %d', 35 + $table->getTableName(), 36 + $clone_name, 37 + $repository->getID()); 38 + } catch (AphrontDuplicateKeyQueryException $ex) { 39 + echo tsprintf( 40 + "%s\n", 41 + pht( 42 + 'Repository "%s" has a duplicate "Clone/Checkout As" name ("%s"). '. 43 + 'Each name must now be unique. You can edit the repository to give '. 44 + 'it a new, unique short name.', 45 + $repository->getDisplayName(), 46 + $clone_name)); 47 + } 48 + 49 + }
+16 -5
src/applications/diffusion/controller/DiffusionRepositoryEditBasicController.php
··· 17 17 18 18 $v_name = $repository->getName(); 19 19 $v_desc = $repository->getDetail('description'); 20 - $v_clone_name = $repository->getDetail('clone-name'); 20 + $v_clone_name = $repository->getRepositorySlug(); 21 21 $v_projects = PhabricatorEdgeQuery::loadDestinationPHIDs( 22 22 $repository->getPHID(), 23 23 PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); 24 24 $e_name = true; 25 + $e_slug = null; 25 26 $errors = array(); 26 27 28 + $validation_exception = null; 27 29 if ($request->isFormPost()) { 28 30 $v_name = $request->getStr('name'); 29 31 $v_desc = $request->getStr('description'); ··· 71 73 '=' => array_fuse($v_projects), 72 74 )); 73 75 74 - id(new PhabricatorRepositoryEditor()) 76 + $editor = id(new PhabricatorRepositoryEditor()) 75 77 ->setContinueOnNoEffect(true) 76 78 ->setContentSourceFromRequest($request) 77 - ->setActor($viewer) 78 - ->applyTransactions($repository, $xactions); 79 + ->setActor($viewer); 80 + 81 + try { 82 + $editor->applyTransactions($repository, $xactions); 83 + 84 + return id(new AphrontRedirectResponse())->setURI($edit_uri); 85 + } catch (PhabricatorApplicationTransactionValidationException $ex) { 86 + $validation_exception = $ex; 79 87 80 - return id(new AphrontRedirectResponse())->setURI($edit_uri); 88 + $e_slug = $ex->getShortMessage($type_clone_name); 89 + } 81 90 } 82 91 } 83 92 ··· 102 111 ->setName('cloneName') 103 112 ->setLabel(pht('Clone/Checkout As')) 104 113 ->setValue($v_clone_name) 114 + ->setError($e_slug) 105 115 ->setCaption( 106 116 pht( 107 117 'Optional directory name to use when cloning or checking out '. ··· 130 140 131 141 $object_box = id(new PHUIObjectBoxView()) 132 142 ->setHeaderText($title) 143 + ->setValidationException($validation_exception) 133 144 ->setForm($form) 134 145 ->setFormErrors($errors); 135 146
+1 -1
src/applications/diffusion/controller/DiffusionRepositoryEditMainController.php
··· 286 286 $view->addProperty(pht('Type'), $type); 287 287 $view->addProperty(pht('Callsign'), $repository->getCallsign()); 288 288 289 - $clone_name = $repository->getDetail('clone-name'); 289 + $clone_name = $repository->getRepositorySlug(); 290 290 291 291 if ($repository->isHosted()) { 292 292 $view->addProperty(
+68 -3
src/applications/repository/editor/PhabricatorRepositoryEditor.php
··· 99 99 case PhabricatorRepositoryTransaction::TYPE_DANGEROUS: 100 100 return $object->shouldAllowDangerousChanges(); 101 101 case PhabricatorRepositoryTransaction::TYPE_CLONE_NAME: 102 - return $object->getDetail('clone-name'); 102 + return $object->getRepositorySlug(); 103 103 case PhabricatorRepositoryTransaction::TYPE_SERVICE: 104 104 return $object->getAlmanacServicePHID(); 105 105 case PhabricatorRepositoryTransaction::TYPE_SYMBOLS_LANGUAGE: ··· 141 141 case PhabricatorRepositoryTransaction::TYPE_PUSH_POLICY: 142 142 case PhabricatorRepositoryTransaction::TYPE_CREDENTIAL: 143 143 case PhabricatorRepositoryTransaction::TYPE_DANGEROUS: 144 - case PhabricatorRepositoryTransaction::TYPE_CLONE_NAME: 145 144 case PhabricatorRepositoryTransaction::TYPE_SERVICE: 146 145 case PhabricatorRepositoryTransaction::TYPE_SYMBOLS_LANGUAGE: 147 146 case PhabricatorRepositoryTransaction::TYPE_SYMBOLS_SOURCES: 148 147 case PhabricatorRepositoryTransaction::TYPE_STAGING_URI: 149 148 case PhabricatorRepositoryTransaction::TYPE_AUTOMATION_BLUEPRINTS: 150 149 return $xaction->getNewValue(); 150 + case PhabricatorRepositoryTransaction::TYPE_CLONE_NAME: 151 + $name = $xaction->getNewValue(); 152 + if (strlen($name)) { 153 + return $name; 154 + } 155 + return null; 151 156 case PhabricatorRepositoryTransaction::TYPE_NOTIFY: 152 157 case PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE: 153 158 return (int)$xaction->getNewValue(); ··· 216 221 $object->setDetail('allow-dangerous-changes', $xaction->getNewValue()); 217 222 return; 218 223 case PhabricatorRepositoryTransaction::TYPE_CLONE_NAME: 219 - $object->setDetail('clone-name', $xaction->getNewValue()); 224 + $object->setRepositorySlug($xaction->getNewValue()); 220 225 return; 221 226 case PhabricatorRepositoryTransaction::TYPE_SERVICE: 222 227 $object->setAlmanacServicePHID($xaction->getNewValue()); ··· 448 453 } 449 454 } 450 455 break; 456 + 457 + case PhabricatorRepositoryTransaction::TYPE_CLONE_NAME: 458 + foreach ($xactions as $xaction) { 459 + $old = $xaction->getOldValue(); 460 + $new = $xaction->getNewValue(); 461 + 462 + if (!strlen($new)) { 463 + continue; 464 + } 465 + 466 + if ($new === $old) { 467 + continue; 468 + } 469 + 470 + try { 471 + PhabricatorRepository::asssertValidRepositorySlug($new); 472 + } catch (Exception $ex) { 473 + $errors[] = new PhabricatorApplicationTransactionValidationError( 474 + $type, 475 + pht('Invalid'), 476 + $ex->getMessage(), 477 + $xaction); 478 + continue; 479 + } 480 + 481 + $other = id(new PhabricatorRepositoryQuery()) 482 + ->setViewer(PhabricatorUser::getOmnipotentUser()) 483 + ->withSlugs(array($new)) 484 + ->executeOne(); 485 + if ($other && ($other->getID() !== $object->getID())) { 486 + $errors[] = new PhabricatorApplicationTransactionValidationError( 487 + $type, 488 + pht('Duplicate'), 489 + pht( 490 + 'The selected repository short name is already in use by '. 491 + 'another repository. Choose a unique short name.'), 492 + $xaction); 493 + continue; 494 + } 495 + } 496 + break; 497 + 451 498 } 452 499 453 500 return $errors; 501 + } 502 + 503 + protected function didCatchDuplicateKeyException( 504 + PhabricatorLiskDAO $object, 505 + array $xactions, 506 + Exception $ex) { 507 + 508 + $errors = array(); 509 + 510 + $errors[] = new PhabricatorApplicationTransactionValidationError( 511 + null, 512 + pht('Invalid'), 513 + pht( 514 + 'The chosen callsign or repository short name is already in '. 515 + 'use by another repository.'), 516 + null); 517 + 518 + throw new PhabricatorApplicationTransactionValidationException($errors); 454 519 } 455 520 456 521 }
+13
src/applications/repository/query/PhabricatorRepositoryQuery.php
··· 11 11 private $nameContains; 12 12 private $remoteURIs; 13 13 private $datasourceQuery; 14 + private $slugs; 14 15 15 16 private $numericIdentifiers; 16 17 private $callsignIdentifiers; ··· 111 112 112 113 public function withDatasourceQuery($query) { 113 114 $this->datasourceQuery = $query; 115 + return $this; 116 + } 117 + 118 + public function withSlugs(array $slugs) { 119 + $this->slugs = $slugs; 114 120 return $this; 115 121 } 116 122 ··· 562 568 'r.name LIKE %> OR r.callsign LIKE %>', 563 569 $query, 564 570 $callsign); 571 + } 572 + 573 + if ($this->slugs !== null) { 574 + $where[] = qsprintf( 575 + $conn, 576 + 'r.repositorySlug IN (%Ls)', 577 + $this->slugs); 565 578 } 566 579 567 580 return $where;
+83 -6
src/applications/repository/storage/PhabricatorRepository.php
··· 46 46 47 47 protected $name; 48 48 protected $callsign; 49 + protected $repositorySlug; 49 50 protected $uuid; 50 51 protected $viewPolicy; 51 52 protected $editPolicy; ··· 93 94 self::CONFIG_COLUMN_SCHEMA => array( 94 95 'name' => 'sort255', 95 96 'callsign' => 'sort32', 97 + 'repositorySlug' => 'sort64?', 96 98 'versionControlSystem' => 'text32', 97 99 'uuid' => 'text64?', 98 100 'pushPolicy' => 'policy', ··· 100 102 'almanacServicePHID' => 'phid?', 101 103 ), 102 104 self::CONFIG_KEY_SCHEMA => array( 103 - 'key_phid' => null, 104 - 'phid' => array( 105 - 'columns' => array('phid'), 106 - 'unique' => true, 107 - ), 108 105 'callsign' => array( 109 106 'columns' => array('callsign'), 110 107 'unique' => true, ··· 114 111 ), 115 112 'key_vcs' => array( 116 113 'columns' => array('versionControlSystem'), 114 + ), 115 + 'key_slug' => array( 116 + 'columns' => array('repositorySlug'), 117 + 'unique' => true, 117 118 ), 118 119 ), 119 120 ) + parent::getConfiguration(); ··· 297 298 * @return string 298 299 */ 299 300 public function getCloneName() { 300 - $name = $this->getDetail('clone-name'); 301 + $name = $this->getRepositorySlug(); 301 302 302 303 // Make some reasonable effort to produce reasonable default directory 303 304 // names from repository names. ··· 312 313 } 313 314 314 315 return $name; 316 + } 317 + 318 + public static function isValidRepositorySlug($slug) { 319 + try { 320 + self::asssertValidRepositorySlug($slug); 321 + return true; 322 + } catch (Exception $ex) { 323 + return false; 324 + } 325 + } 326 + 327 + public static function asssertValidRepositorySlug($slug) { 328 + if (!strlen($slug)) { 329 + throw new Exception( 330 + pht( 331 + 'The empty string is not a valid repository short name. '. 332 + 'Repository short names must be at least one character long.')); 333 + } 334 + 335 + if (strlen($slug) > 64) { 336 + throw new Exception( 337 + pht( 338 + 'The name "%s" is not a valid repository short name. Repository '. 339 + 'short names must not be longer than 64 characters.', 340 + $slug)); 341 + } 342 + 343 + if (preg_match('/[^a-zA-Z0-9._-]/', $slug)) { 344 + throw new Exception( 345 + pht( 346 + 'The name "%s" is not a valid repository short name. Repository '. 347 + 'short names may only contain letters, numbers, periods, hyphens '. 348 + 'and underscores.', 349 + $slug)); 350 + } 351 + 352 + if (!preg_match('/^[a-zA-Z0-9]/', $slug)) { 353 + throw new Exception( 354 + pht( 355 + 'The name "%s" is not a valid repository short name. Repository '. 356 + 'short names must begin with a letter or number.', 357 + $slug)); 358 + } 359 + 360 + if (!preg_match('/[a-zA-Z0-9]\z/', $slug)) { 361 + throw new Exception( 362 + pht( 363 + 'The name "%s" is not a valid repository short name. Repository '. 364 + 'short names must end with a letter or number.', 365 + $slug)); 366 + } 367 + 368 + if (preg_match('/__|--|\\.\\./', $slug)) { 369 + throw new Exception( 370 + pht( 371 + 'The name "%s" is not a valid repository short name. Repository '. 372 + 'short names must not contain multiple consecutive underscores, '. 373 + 'hyphens, or periods.', 374 + $slug)); 375 + } 376 + 377 + if (preg_match('/^[A-Z]+\z/', $slug)) { 378 + throw new Exception( 379 + pht( 380 + 'The name "%s" is not a valid repository short name. Repository '. 381 + 'short names may not contain only uppercase letters.', 382 + $slug)); 383 + } 384 + 385 + if (preg_match('/^\d+\z/', $slug)) { 386 + throw new Exception( 387 + pht( 388 + 'The name "%s" is not a valid repository short name. Repository '. 389 + 'short names may not contain only numbers.', 390 + $slug)); 391 + } 315 392 } 316 393 317 394
+64
src/applications/repository/storage/__tests__/PhabricatorRepositoryTestCase.php
··· 152 152 } 153 153 } 154 154 155 + public function testRepositoryShortNameValidation() { 156 + $good = array( 157 + 'sensible-repository', 158 + 'AReasonableName', 159 + 'ACRONYM-project', 160 + 'sol-123', 161 + '46-helixes', 162 + 'node.io', 163 + 'internet.com', 164 + 'www.internet-site.com.repository', 165 + 'with_under-scores', 166 + 167 + // Can't win them all. 168 + 'A-_._-_._-_._-_._-_._-_._-1', 169 + 170 + // 64-character names are fine. 171 + str_repeat('a', 64), 172 + ); 173 + 174 + $poor = array( 175 + '', 176 + '1', 177 + '.', 178 + '-_-', 179 + 'AAAA', 180 + '..', 181 + 'a/b', 182 + '../../etc/passwd', 183 + '/', 184 + '!', 185 + '@', 186 + 'ca$hmoney', 187 + 'repo with spaces', 188 + 'hyphen-', 189 + '-ated', 190 + '_underscores_', 191 + 'yes!', 192 + 193 + // 65-character names are no good. 194 + str_repeat('a', 65), 195 + ); 196 + 197 + foreach ($good as $nice_name) { 198 + $actual = PhabricatorRepository::isValidRepositorySlug($nice_name); 199 + $this->assertEqual( 200 + true, 201 + $actual, 202 + pht( 203 + 'Expected "%s" to be a valid repository short name.', 204 + $nice_name)); 205 + } 206 + 207 + foreach ($poor as $poor_name) { 208 + $actual = PhabricatorRepository::isValidRepositorySlug($poor_name); 209 + $this->assertEqual( 210 + false, 211 + $actual, 212 + pht( 213 + 'Expected "%s" to be rejected as an invalid repository '. 214 + 'short name.', 215 + $poor_name)); 216 + } 217 + } 218 + 155 219 }