Skip to content

Commit 0b3d10c

Browse files
author
epriestley
committed
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
1 parent e84693f commit 0b3d10c

File tree

8 files changed

+299
-15
lines changed

8 files changed

+299
-15
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
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);
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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+
}

src/applications/diffusion/controller/DiffusionRepositoryEditBasicController.php

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@ public function handleRequest(AphrontRequest $request) {
1717

1818
$v_name = $repository->getName();
1919
$v_desc = $repository->getDetail('description');
20-
$v_clone_name = $repository->getDetail('clone-name');
20+
$v_clone_name = $repository->getRepositorySlug();
2121
$v_projects = PhabricatorEdgeQuery::loadDestinationPHIDs(
2222
$repository->getPHID(),
2323
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
2424
$e_name = true;
25+
$e_slug = null;
2526
$errors = array();
2627

28+
$validation_exception = null;
2729
if ($request->isFormPost()) {
2830
$v_name = $request->getStr('name');
2931
$v_desc = $request->getStr('description');
@@ -71,13 +73,20 @@ public function handleRequest(AphrontRequest $request) {
7173
'=' => array_fuse($v_projects),
7274
));
7375

74-
id(new PhabricatorRepositoryEditor())
76+
$editor = id(new PhabricatorRepositoryEditor())
7577
->setContinueOnNoEffect(true)
7678
->setContentSourceFromRequest($request)
77-
->setActor($viewer)
78-
->applyTransactions($repository, $xactions);
79+
->setActor($viewer);
7980

80-
return id(new AphrontRedirectResponse())->setURI($edit_uri);
81+
try {
82+
$editor->applyTransactions($repository, $xactions);
83+
84+
return id(new AphrontRedirectResponse())->setURI($edit_uri);
85+
} catch (PhabricatorApplicationTransactionValidationException $ex) {
86+
$validation_exception = $ex;
87+
88+
$e_slug = $ex->getShortMessage($type_clone_name);
89+
}
8190
}
8291
}
8392

@@ -102,6 +111,7 @@ public function handleRequest(AphrontRequest $request) {
102111
->setName('cloneName')
103112
->setLabel(pht('Clone/Checkout As'))
104113
->setValue($v_clone_name)
114+
->setError($e_slug)
105115
->setCaption(
106116
pht(
107117
'Optional directory name to use when cloning or checking out '.
@@ -130,6 +140,7 @@ public function handleRequest(AphrontRequest $request) {
130140

131141
$object_box = id(new PHUIObjectBoxView())
132142
->setHeaderText($title)
143+
->setValidationException($validation_exception)
133144
->setForm($form)
134145
->setFormErrors($errors);
135146

src/applications/diffusion/controller/DiffusionRepositoryEditMainController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ private function buildBasicProperties(
286286
$view->addProperty(pht('Type'), $type);
287287
$view->addProperty(pht('Callsign'), $repository->getCallsign());
288288

289-
$clone_name = $repository->getDetail('clone-name');
289+
$clone_name = $repository->getRepositorySlug();
290290

291291
if ($repository->isHosted()) {
292292
$view->addProperty(

src/applications/repository/editor/PhabricatorRepositoryEditor.php

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ protected function getCustomTransactionOldValue(
9999
case PhabricatorRepositoryTransaction::TYPE_DANGEROUS:
100100
return $object->shouldAllowDangerousChanges();
101101
case PhabricatorRepositoryTransaction::TYPE_CLONE_NAME:
102-
return $object->getDetail('clone-name');
102+
return $object->getRepositorySlug();
103103
case PhabricatorRepositoryTransaction::TYPE_SERVICE:
104104
return $object->getAlmanacServicePHID();
105105
case PhabricatorRepositoryTransaction::TYPE_SYMBOLS_LANGUAGE:
@@ -141,13 +141,18 @@ protected function getCustomTransactionNewValue(
141141
case PhabricatorRepositoryTransaction::TYPE_PUSH_POLICY:
142142
case PhabricatorRepositoryTransaction::TYPE_CREDENTIAL:
143143
case PhabricatorRepositoryTransaction::TYPE_DANGEROUS:
144-
case PhabricatorRepositoryTransaction::TYPE_CLONE_NAME:
145144
case PhabricatorRepositoryTransaction::TYPE_SERVICE:
146145
case PhabricatorRepositoryTransaction::TYPE_SYMBOLS_LANGUAGE:
147146
case PhabricatorRepositoryTransaction::TYPE_SYMBOLS_SOURCES:
148147
case PhabricatorRepositoryTransaction::TYPE_STAGING_URI:
149148
case PhabricatorRepositoryTransaction::TYPE_AUTOMATION_BLUEPRINTS:
150149
return $xaction->getNewValue();
150+
case PhabricatorRepositoryTransaction::TYPE_CLONE_NAME:
151+
$name = $xaction->getNewValue();
152+
if (strlen($name)) {
153+
return $name;
154+
}
155+
return null;
151156
case PhabricatorRepositoryTransaction::TYPE_NOTIFY:
152157
case PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE:
153158
return (int)$xaction->getNewValue();
@@ -216,7 +221,7 @@ protected function applyCustomInternalTransaction(
216221
$object->setDetail('allow-dangerous-changes', $xaction->getNewValue());
217222
return;
218223
case PhabricatorRepositoryTransaction::TYPE_CLONE_NAME:
219-
$object->setDetail('clone-name', $xaction->getNewValue());
224+
$object->setRepositorySlug($xaction->getNewValue());
220225
return;
221226
case PhabricatorRepositoryTransaction::TYPE_SERVICE:
222227
$object->setAlmanacServicePHID($xaction->getNewValue());
@@ -448,9 +453,69 @@ protected function validateTransaction(
448453
}
449454
}
450455
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+
451498
}
452499

453500
return $errors;
454501
}
455502

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);
519+
}
520+
456521
}

src/applications/repository/query/PhabricatorRepositoryQuery.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ final class PhabricatorRepositoryQuery
1111
private $nameContains;
1212
private $remoteURIs;
1313
private $datasourceQuery;
14+
private $slugs;
1415

1516
private $numericIdentifiers;
1617
private $callsignIdentifiers;
@@ -114,6 +115,11 @@ public function withDatasourceQuery($query) {
114115
return $this;
115116
}
116117

118+
public function withSlugs(array $slugs) {
119+
$this->slugs = $slugs;
120+
return $this;
121+
}
122+
117123
public function needCommitCounts($need_counts) {
118124
$this->needCommitCounts = $need_counts;
119125
return $this;
@@ -564,6 +570,13 @@ protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
564570
$callsign);
565571
}
566572

573+
if ($this->slugs !== null) {
574+
$where[] = qsprintf(
575+
$conn,
576+
'r.repositorySlug IN (%Ls)',
577+
$this->slugs);
578+
}
579+
567580
return $where;
568581
}
569582

src/applications/repository/storage/PhabricatorRepository.php

Lines changed: 83 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
4646

4747
protected $name;
4848
protected $callsign;
49+
protected $repositorySlug;
4950
protected $uuid;
5051
protected $viewPolicy;
5152
protected $editPolicy;
@@ -93,18 +94,14 @@ protected function getConfiguration() {
9394
self::CONFIG_COLUMN_SCHEMA => array(
9495
'name' => 'sort255',
9596
'callsign' => 'sort32',
97+
'repositorySlug' => 'sort64?',
9698
'versionControlSystem' => 'text32',
9799
'uuid' => 'text64?',
98100
'pushPolicy' => 'policy',
99101
'credentialPHID' => 'phid?',
100102
'almanacServicePHID' => 'phid?',
101103
),
102104
self::CONFIG_KEY_SCHEMA => array(
103-
'key_phid' => null,
104-
'phid' => array(
105-
'columns' => array('phid'),
106-
'unique' => true,
107-
),
108105
'callsign' => array(
109106
'columns' => array('callsign'),
110107
'unique' => true,
@@ -115,6 +112,10 @@ protected function getConfiguration() {
115112
'key_vcs' => array(
116113
'columns' => array('versionControlSystem'),
117114
),
115+
'key_slug' => array(
116+
'columns' => array('repositorySlug'),
117+
'unique' => true,
118+
),
118119
),
119120
) + parent::getConfiguration();
120121
}
@@ -297,7 +298,7 @@ public function getProjectPHIDs() {
297298
* @return string
298299
*/
299300
public function getCloneName() {
300-
$name = $this->getDetail('clone-name');
301+
$name = $this->getRepositorySlug();
301302

302303
// Make some reasonable effort to produce reasonable default directory
303304
// names from repository names.
@@ -314,6 +315,82 @@ public function getCloneName() {
314315
return $name;
315316
}
316317

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+
}
392+
}
393+
317394

318395
/* -( Remote Command Execution )------------------------------------------- */
319396

0 commit comments

Comments
 (0)