From d0771f77bcd65cd2c9bd602c10ac8e4195e671ce Mon Sep 17 00:00:00 2001 From: Hugo Sales Date: Tue, 21 Jul 2020 21:33:50 +0000 Subject: [PATCH] [UTIL][NICKNAME] Import nickname utilities and exceptions from v2 --- src/Kernel.php | 1 + src/Util/Exception/ClientException.php | 47 ++++ src/Util/Exception/NicknameEmptyException.php | 48 ++++ src/Util/Exception/NicknameException.php | 55 +++++ .../Exception/NicknameInvalidException.php | 48 ++++ .../Exception/NicknameReservedException.php | 48 ++++ src/Util/Exception/NicknameTakenException.php | 56 +++++ .../Exception/NicknameTooLongException.php | 50 ++++ src/Util/Nickname.php | 215 ++++++++++++++++++ 9 files changed, 568 insertions(+) create mode 100644 src/Util/Exception/ClientException.php create mode 100644 src/Util/Exception/NicknameEmptyException.php create mode 100644 src/Util/Exception/NicknameException.php create mode 100644 src/Util/Exception/NicknameInvalidException.php create mode 100644 src/Util/Exception/NicknameReservedException.php create mode 100644 src/Util/Exception/NicknameTakenException.php create mode 100644 src/Util/Exception/NicknameTooLongException.php create mode 100644 src/Util/Nickname.php diff --git a/src/Kernel.php b/src/Kernel.php index 635c965e8a..0425631060 100644 --- a/src/Kernel.php +++ b/src/Kernel.php @@ -64,6 +64,7 @@ class Kernel extends BaseKernel define('GNUSOCIAL_LIFECYCLE', 'dev'); define('GNUSOCIAL_VERSION', GNUSOCIAL_BASE_VERSION . '-' . GNUSOCIAL_LIFECYCLE); define('GNUSOCIAL_CODENAME', 'Big bang'); + define('URL_REGEX_DOMAIN_NAME', '(?:(?!-)[A-Za-z0-9\-]{1,63}(?. +// }}} + +namespace App\Util\Exception; + +/** + * Client exception. Indicates a client request contains some sort of + * error. HTTP code 400 + * + * @category Exception + * @package GNUsocial + * + * @author Evan Prodromou + * @copyright 2009 StatusNet Inc. + * @copyright 2009-2014 Free Software Foundation, Inc http://www.fsf.org + * @author Hugo Sales + * @copyright 2018-2020 Free Software Foundation, Inc http://www.fsf.org + * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later + */ +class ClientException extends Exception +{ + public function __construct(string $message = null, int $code = 400) + { + parent::__construct($message, $code); + } + + public function __toString() + { + return __CLASS__ . ": [{$this->code}]: {$this->message}\n"; + } +} diff --git a/src/Util/Exception/NicknameEmptyException.php b/src/Util/Exception/NicknameEmptyException.php new file mode 100644 index 0000000000..0cd7f80528 --- /dev/null +++ b/src/Util/Exception/NicknameEmptyException.php @@ -0,0 +1,48 @@ +. +// }}} + +namespace App\Util\Exception; + +/** + * Nickname empty exception + * + * @category Exception + * @package GNUsocial + * + * @author Zach Copley + * @copyright 2010 StatusNet Inc. + * @author Brion Vibber + * @author Mikael Nordfeldth + * @author Nym Coy + * @copyright 2009-2014 Free Software Foundation, Inc http://www.fsf.org + * @auuthor Daniel Supernault + * @auuthor Diogo Cordeiro + * + * @author Hugo Sales + * @copyright 2018-2020 Free Software Foundation, Inc http://www.fsf.org + * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later + */ +class NicknameEmptyException extends NicknameInvalidException +{ + protected function defaultMessage(): string + { + // TRANS: Validation error in form for registration, profile and group settings, etc. + return _m('Nickname cannot be empty.'); + } +} diff --git a/src/Util/Exception/NicknameException.php b/src/Util/Exception/NicknameException.php new file mode 100644 index 0000000000..4fdda6801f --- /dev/null +++ b/src/Util/Exception/NicknameException.php @@ -0,0 +1,55 @@ +. +// }}} + +namespace App\Util\Exception; + +/** + * Nickname empty exception + * + * @category Exception + * @package GNUsocial + * + * @author Zach Copley + * @copyright 2010 StatusNet Inc. + * @author Brion Vibber + * @author Mikael Nordfeldth + * @author Nym Coy + * @copyright 2009-2014 Free Software Foundation, Inc http://www.fsf.org + * @auuthor Daniel Supernault + * @auuthor Diogo Cordeiro + * + * @author Hugo Sales + * @copyright 2018-2020 Free Software Foundation, Inc http://www.fsf.org + * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later + */ +class NicknameException extends ClientException +{ + public function __construct(string $msg = null, int $code = 400) + { + if ($msg === null) { + $msg = $this->defaultMessage(); + } + parent::__construct($msg, $code); + } + + protected function defaultMessage(): string + { + return ''; + } +} diff --git a/src/Util/Exception/NicknameInvalidException.php b/src/Util/Exception/NicknameInvalidException.php new file mode 100644 index 0000000000..3c4674ea84 --- /dev/null +++ b/src/Util/Exception/NicknameInvalidException.php @@ -0,0 +1,48 @@ +. +// }}} + +namespace App\Util\Exception; + +/** + * Nickname invalid exception + * + * @category Exception + * @package GNUsocial + * + * @author Zach Copley + * @copyright 2010 StatusNet Inc. + * @author Brion Vibber + * @author Mikael Nordfeldth + * @author Nym Coy + * @copyright 2009-2014 Free Software Foundation, Inc http://www.fsf.org + * @auuthor Daniel Supernault + * @auuthor Diogo Cordeiro + * + * @author Hugo Sales + * @copyright 2018-2020 Free Software Foundation, Inc http://www.fsf.org + * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later + */ +class NicknameInvalidException extends NicknameException +{ + protected function defaultMessage() + { + // TRANS: Validation error in form for registration, profile and group settings, etc. + return _m('Nickname must have only lowercase letters and numbers and no spaces.'); + } +} diff --git a/src/Util/Exception/NicknameReservedException.php b/src/Util/Exception/NicknameReservedException.php new file mode 100644 index 0000000000..e17e37e9b4 --- /dev/null +++ b/src/Util/Exception/NicknameReservedException.php @@ -0,0 +1,48 @@ +. +// }}} + +namespace App\Util\Exception; + +/** + * Nickname reserved exception + * + * @category Exception + * @package GNUsocial + * + * @author Zach Copley + * @copyright 2010 StatusNet Inc. + * @author Brion Vibber + * @author Mikael Nordfeldth + * @author Nym Coy + * @copyright 2009-2014 Free Software Foundation, Inc http://www.fsf.org + * @auuthor Daniel Supernault + * @auuthor Diogo Cordeiro + * + * @author Hugo Sales + * @copyright 2018-2020 Free Software Foundation, Inc http://www.fsf.org + * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later + */ +class NicknameReservedException extends NicknameException +{ + protected function defaultMessage() + { + // TRANS: Validation error in form for registration, profile and group settings, etc. + return _m('Nickname is reserved.'); + } +} diff --git a/src/Util/Exception/NicknameTakenException.php b/src/Util/Exception/NicknameTakenException.php new file mode 100644 index 0000000000..2f31bcd25c --- /dev/null +++ b/src/Util/Exception/NicknameTakenException.php @@ -0,0 +1,56 @@ +. +// }}} + +namespace App\Util\Exception; + +/** + * Nickname empty exception + * + * @category Exception + * @package GNUsocial + * + * @author Zach Copley + * @copyright 2010 StatusNet Inc. + * @author Brion Vibber + * @author Mikael Nordfeldth + * @author Nym Coy + * @copyright 2009-2014 Free Software Foundation, Inc http://www.fsf.org + * @auuthor Daniel Supernault + * @auuthor Diogo Cordeiro + * + * @author Hugo Sales + * @copyright 2018-2020 Free Software Foundation, Inc http://www.fsf.org + * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later + */ +class NicknameTakenException extends NicknameException +{ + public ?Profile $profile = null; // the Profile which occupies the nickname + + public function __construct(Profile $profile, string $msg = null, int $code = 400) + { + $this->profile = $profile; + parent::__construct($msg, $code); + } + + protected function defaultMessage() + { + // TRANS: Validation error in form for registration, profile and group settings, etc. + return _m('Nickname is already in use on this server.'); + } +} diff --git a/src/Util/Exception/NicknameTooLongException.php b/src/Util/Exception/NicknameTooLongException.php new file mode 100644 index 0000000000..0d4db0d0b7 --- /dev/null +++ b/src/Util/Exception/NicknameTooLongException.php @@ -0,0 +1,50 @@ +. +// }}} + +namespace App\Util\Exception; + +use App\Util\Nickname; + +/** + * Nickname too long exception + * + * @category Exception + * @package GNUsocial + * + * @author Zach Copley + * @copyright 2010 StatusNet Inc. + * @author Brion Vibber + * @author Mikael Nordfeldth + * @author Nym Coy + * @copyright 2009-2014 Free Software Foundation, Inc http://www.fsf.org + * @auuthor Daniel Supernault + * @auuthor Diogo Cordeiro + * + * @author Hugo Sales + * @copyright 2018-2020 Free Software Foundation, Inc http://www.fsf.org + * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later + */ +class NicknameTooLongException extends NicknameInvalidException +{ + protected function defaultMessage() + { + // TRANS: Validation error in form for registration, profile and group settings, etc. + return _m('Nickname cannot be more than # character long.', ['count' => Nickname::MAX_LEN]); + } +} diff --git a/src/Util/Nickname.php b/src/Util/Nickname.php new file mode 100644 index 0000000000..ff4aa4ec9d --- /dev/null +++ b/src/Util/Nickname.php @@ -0,0 +1,215 @@ +. +// }}} + +namespace App\Util; + +use Normalizer; + +/** + * Nickname validation + * + * @category Validation + * @package GNUsocial + * + * @author Zach Copley + * @copyright 2010 StatusNet Inc. + * @author Brion Vibber + * @author Mikael Nordfeldth + * @author Nym Coy + * @copyright 2009-2014 Free Software Foundation, Inc http://www.fsf.org + * @auuthor Daniel Supernault + * @auuthor Diogo Cordeiro + * + * @author Hugo Sales + * @copyright 2018-2020 Free Software Foundation, Inc http://www.fsf.org + * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later + */ +class Nickname +{ + /** + * Regex fragment for pulling a formated nickname *OR* ID number. + * Suitable for router def of 'id' parameters on API actions. + * + * Not guaranteed to be valid after normalization; run the string through + * Nickname::normalize() to get the canonical form, or Nickname::isValid() + * if you just need to check if it's properly formatted. + * + * This, DISPLAY_FMT, and CANONICAL_FMT should not be enclosed in []s. + * + * @fixme would prefer to define in reference to the other constants + */ + const INPUT_FMT = '(?:[0-9]+|[0-9a-zA-Z_]{1,64})'; + + /** + * Regex fragment for acceptable user-formatted variant of a nickname. + * + * This includes some chars such as underscore which will be removed + * from the normalized canonical form, but still must fit within + * field length limits. + * + * Not guaranteed to be valid after normalization; run the string through + * Nickname::normalize() to get the canonical form, or Nickname::isValid() + * if you just need to check if it's properly formatted. + * + * This, INPUT_FMT and CANONICAL_FMT should not be enclosed in []s. + */ + const DISPLAY_FMT = '[0-9a-zA-Z_]{1,64}'; + + /** + * Simplified regex fragment for acceptable full WebFinger ID of a user + * + * We could probably use an email regex here, but mainly we are interested + * in matching it in our URLs, like https://social.example/user@example.com + */ + const WEBFINGER_FMT = '(?:\w+[\w\-\_\.]*)?\w+\@' . URL_REGEX_DOMAIN_NAME; + + /** + * 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, INPUT_FMT and DISPLAY_FMT 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; + + /** + * Regex with non-capturing group that matches whitespace and some + * characters which are allowed right before an @ or ! when mentioning + * other users. Like: 'This goes out to:@mmn (@chimo too) (!awwyiss).' + * + * FIXME: Make this so you can have multiple whitespace but not multiple + * parenthesis or something. '(((@n_n@)))' might as well be a smiley. + */ + const BEFORE_MENTIONS = '(?:^|[\s\.\,\:\;\[\(]+)'; + + /** + * Validate an input $nickname, and normalize it to its canonical form. + * The canonical form will be returned, or an exception thrown if invalid. + * + * @throws NicknameException (base class) + * @throws NicknameBlacklistedException + * @throws NicknameEmptyException + * @throws NicknameInvalidException + * @throws NicknamePathCollisionException + * @throws NicknameTakenException + * @throws NicknameTooLongException + */ + public static function normalize(string $nickname, bool $check_already_used = false): string + { + if (mb_strlen($nickname) > self::MAX_LEN) { + // Display forms must also fit! + throw new NicknameTooLongException(); + } + + $nickname = trim($nickname); + $nickname = str_replace('_', '', $nickname); + $nickname = mb_strtolower($nickname); + $nickname = Normalizer::normalize($nickname, Normalizer::FORM_C); + + if (mb_strlen($nickname) < 1) { + throw new NicknameEmptyException(); + } elseif (!self::isCanonical($nickname) && !filter_var($nickname, FILTER_VALIDATE_EMAIL)) { + throw new NicknameInvalidException(); + } elseif (self::isReserved($nickname) || Common::isSystemPath($nickname)) { + throw new NicknameReservedException(); + } elseif ($check_already_used) { + $profile = self::isTaken($nickname); + if ($profile instanceof Profile) { + throw new NicknameTakenException($profile); + } + } + + return $nickname; + } + + /** + * 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. + * + * @return bool True if nickname is valid. False if invalid (or taken if $check_already_used == true). + */ + public static function isValid(string $nickname, bool $check_already_used = false): bool + { + try { + self::normalize($nickname, $check_already_used); + } catch (NicknameException $e) { + return false; + } + + return true; + } + + /** + * Is the given string a valid canonical nickname form? + */ + public static function isCanonical(string $nickname): bool + { + return preg_match('/^(?:' . self::CANONICAL_FMT . ')$/', $nickname); + } + + /** + * Is the given string in our nickname blacklist? + */ + public static function isReserved(string $nickname): bool + { + $reserved = Common::config('nickname', 'reserved'); + if (!$reserved) { + return false; + } + return in_array($nickname, $reserved); + } + + /** + * Is the nickname already in use locally? Checks the User table. + * + * @return null|Profile Returns Profile if nickname found, otherwise null + */ + public static function isTaken(string $nickname): ?Profile + { + $found = DB::find('user', ['nickname' => $nickname]); + if ($found instanceof User) { + return $found->getProfile(); + } + + $found = DB::find('local_group', ['nickname' => $nickname]); + if ($found instanceof Local_group) { + return $found->getProfile(); + } + + $found = DB::find('group_alias', ['nickname' => $nickname]); + if ($found instanceof Group_alias) { + return $found->getProfile(); + } + + return null; + } +}