From fffc10a23084f1bb4b47de926dc1034c8f9ee9b8 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 29 Nov 2010 14:46:10 -0800 Subject: [PATCH] Nickname class to encapsulate validation & common regexes for nickname formats. This provides initial infrastructure for decoupling display names from internal canonical names, but continues to have us storing and using the canonical forms. It should be/become possible to provide mixed-case and underscore-containing names in links, @-mention, !-group, etc, but we don't store those alternate forms generally. --- lib/nickname.php | 176 +++++++++++++++++++++++++++++++++++++++++ tests/NicknameTest.php | 8 +- 2 files changed, 180 insertions(+), 4 deletions(-) create mode 100644 lib/nickname.php diff --git a/lib/nickname.php b/lib/nickname.php new file mode 100644 index 0000000000..48269f3b9f --- /dev/null +++ b/lib/nickname.php @@ -0,0 +1,176 @@ +. + */ + +class Nickname +{ + /** + * Regex fragment for pulling an arbitrarily-formated nickname. + * + * Not guaranteed to be valid after normalization; run the string through + * Nickname::normalize() to get the canonical form, or Nickname::validate() + * if you just need to check if it's properly formatted. + * + * This and CANONICAL_FMT replace the old NICKNAME_FMT, but be aware + * that these should not be enclosed in []s. + */ + const DISPLAY_FMT = '[0-9a-zA-Z_]+'; + + /** + * Regex fragment for checking a canonical nickname. + * + * Any non-matching string is not a valid canonical/normalized nickname. + * Matching strings are valid and canonical form, but may still be + * unavailable for registration due to blacklisting et. + * + * Only the canonical forms should be stored as keys in the database; + * there are multiple possible denormalized forms for each valid + * canonical-form name. + * + * This and DISPLAY_FMT replace the old NICKNAME_FMT, but be aware + * that these should not be enclosed in []s. + */ + const CANONICAL_FMT = '[0-9a-z]{1,64}'; + + /** + * Maximum number of characters in a canonical-form nickname. + */ + const MAX_LEN = 64; + + /** + * Nice simple check of whether the given string is a valid input nickname, + * which can be normalized into an internally canonical form. + * + * Note that valid nicknames may be in use or reserved. + * + * @param string $str + * @return boolean + */ + public static function validate($str) + { + try { + self::normalize($str); + return true; + } catch (NicknameException $e) { + return false; + } + } + + /** + * Validate an input nickname string, and normalize it to its canonical form. + * The canonical form will be returned, or an exception thrown if invalid. + * + * @param string $str + * @return string Normalized canonical form of $str + * + * @throws NicknameException (base class) + * @throws NicknameInvalidException + * @throws NicknameEmptyException + * @throws NicknameTooLongException + */ + public static function normalize($str) + { + $str = trim($str); + $str = str_replace('_', '', $str); + $str = mb_strtolower($str); + + $len = mb_strlen($str); + if ($len < 1) { + throw new NicknameEmptyException(); + } else if ($len > self::MAX_LEN) { + throw new NicknameTooLongException(); + } + if (!self::isCanonical($str)) { + throw new NicknameInvalidException(); + } + + return $str; + } + + /** + * Is the given string a valid canonical nickname form? + * + * @param string $str + * @return boolean + */ + public static function isCanonical($str) + { + return preg_match('/^(?:' . self::CANONICAL_FMT . ')$/', $str); + } +} + +class NicknameException extends ClientException +{ + function __construct($msg=null, $code=400) + { + if ($msg === null) { + $msg = $this->defaultMessage(); + } + parent::__construct($msg, $code); + } + + /** + * Default localized message for this type of exception. + * @return string + */ + protected function defaultMessage() + { + return null; + } +} + +class NicknameInvalidException extends NicknameException { + /** + * Default localized message for this type of exception. + * @return string + */ + protected function defaultMessage() + { + // TRANS: Validation error in form for registration, profile and group settings, etc. + return _('Nickname must have only lowercase letters and numbers and no spaces.'); + } +} + +class NicknameEmptyException extends NicknameException +{ + /** + * Default localized message for this type of exception. + * @return string + */ + protected function defaultMessage() + { + // TRANS: Validation error in form for registration, profile and group settings, etc. + return _('Nickname cannot be empty.'); + } +} + +class NicknameTooLongException extends NicknameInvalidException +{ + /** + * Default localized message for this type of exception. + * @return string + */ + protected function defaultMessage() + { + // TRANS: Validation error in form for registration, profile and group settings, etc. + return sprintf(_m('Nickname cannot be more than %d character long.', + 'Nickname cannot be more than %d characters long.', + Nickname::MAX_LEN), + Nickname::MAX_LEN); + } +} diff --git a/tests/NicknameTest.php b/tests/NicknameTest.php index f1d9808228..a59cada7ad 100644 --- a/tests/NicknameTest.php +++ b/tests/NicknameTest.php @@ -26,18 +26,18 @@ class NicknameTest extends PHPUnit_Framework_TestCase $exception = null; $normalized = false; try { - $normalized = Nickname::normalize($normalized); + $normalized = Nickname::normalize($input); } catch (NicknameException $e) { $exception = $e; } if ($expected === false) { if ($expectedException) { - $this->assert($exception && $exception instanceof $expectedException, + $this->assertTrue($exception && $exception instanceof $expectedException, "invalid input '$input' expected to fail with $expectedException, " . "got " . get_class($exception) . ': ' . $exception->getMessage()); } else { - $this->assert($normalized == false, + $this->assertTrue($normalized == false, "invalid input '$input' expected to fail"); } } else { @@ -47,7 +47,7 @@ class NicknameTest extends PHPUnit_Framework_TestCase } else { $msg .= "'$normalized'"; } - $this->assertEquals($expected, $norm, $msg); + $this->assertEquals($expected, $normalized, $msg); } }