diff --git a/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php b/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php index f6b74801e6..8c17fc07e7 100644 --- a/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php +++ b/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php @@ -310,7 +310,6 @@ final class PhabricatorProjectCoreTestCase extends PhabricatorTestCase { $slugs = mpull($slugs, 'getSlug'); $this->assertTrue(in_array($name2, $slugs)); - } public function testDuplicateSlugs() { diff --git a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php index ff9b23e477..70daac64ed 100644 --- a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php +++ b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php @@ -268,6 +268,17 @@ final class PhabricatorProjectTransactionEditor } $name = last($xactions)->getNewValue(); + + if (!PhabricatorSlug::isValidProjectSlug($name)) { + $errors[] = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Invalid'), + pht( + 'Project names must contain at least one letter or number.'), + last($xactions)); + break; + } + $name_used_already = id(new PhabricatorProjectQuery()) ->setViewer($this->getActor()) ->withNames(array($name)) @@ -304,6 +315,27 @@ final class PhabricatorProjectTransactionEditor $slug_xaction = last($xactions); $new = $slug_xaction->getNewValue(); + + $invalid = array(); + foreach ($new as $slug) { + if (!PhabricatorSlug::isValidProjectSlug($slug)) { + $invalid[] = $slug; + } + } + + if ($invalid) { + $errors[] = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Invalid'), + pht( + 'Hashtags must contain at least one letter or number. %s '. + 'project hashtag(s) are invalid: %s.', + phutil_count($invalid), + implode(', ', $invalid)), + $slug_xaction); + break; + } + $new = $this->normalizeSlugs($new); if ($new) { diff --git a/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php b/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php index 762910413c..ff4174522c 100644 --- a/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php +++ b/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php @@ -774,6 +774,14 @@ final class PhabricatorUSEnglishTranslation '%s changed project hashtag(s), added %d: %s; removed %d: %s.' => '%s changed project hashtags, added %3$s; removed %5$s.', + 'Hashtags must contain at least one letter or number. %s '. + 'project hashtag(s) are invalid: %s.' => array( + 'Hashtags must contain at least one letter or number. The '. + 'hashtag "%2$s" is not valid.', + 'Hashtags must contain at least one letter or number. These '. + 'hashtags are invalid: %2$s.', + ), + '%s added %d project hashtag(s): %s.' => array( array( '%s added a hashtag: %3$s.', diff --git a/src/infrastructure/util/PhabricatorSlug.php b/src/infrastructure/util/PhabricatorSlug.php index fd169914fe..c977c21d70 100644 --- a/src/infrastructure/util/PhabricatorSlug.php +++ b/src/infrastructure/util/PhabricatorSlug.php @@ -8,6 +8,11 @@ final class PhabricatorSlug extends Phobject { return rtrim($slug, '/'); } + public static function isValidProjectSlug($slug) { + $slug = self::normalizeProjectSlug($slug); + return ($slug != '_'); + } + public static function normalize($slug, $hashtag = false) { $slug = preg_replace('@/+@', '/', $slug); $slug = trim($slug, '/');