upstream V3 development https://www.gnusocial.rocks/v3
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

256 lines
9.7 KiB

  1. <?php
  2. declare(strict_types = 1);
  3. // {{{ License
  4. // This file is part of GNU social - https://www.gnu.org/software/social
  5. //
  6. // GNU social is free software: you can redistribute it and/or modify
  7. // it under the terms of the GNU Affero General Public License as published by
  8. // the Free Software Foundation, either version 3 of the License, or
  9. // (at your option) any later version.
  10. //
  11. // GNU social is distributed in the hope that it will be useful,
  12. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. // GNU Affero General Public License for more details.
  15. //
  16. // You should have received a copy of the GNU Affero General Public License
  17. // along with GNU social. If not, see <http://www.gnu.org/licenses/>.
  18. // }}}
  19. namespace App\Util;
  20. use App\Core\DB\DB;
  21. use App\Core\Log;
  22. use App\Util\Exception\DuplicateFoundException;
  23. use App\Util\Exception\NicknameEmptyException;
  24. use App\Util\Exception\NicknameException;
  25. use App\Util\Exception\NicknameInvalidException;
  26. use App\Util\Exception\NicknameNotAllowedException;
  27. use App\Util\Exception\NicknameTakenException;
  28. use App\Util\Exception\NicknameTooLongException;
  29. use App\Util\Exception\NotFoundException;
  30. use App\Util\Exception\NotImplementedException;
  31. use Functional as F;
  32. use InvalidArgumentException;
  33. /**
  34. * Nickname validation
  35. *
  36. * @category Validation
  37. * @package GNUsocial
  38. *
  39. * @author Zach Copley <zach@status.net>
  40. * @copyright 2010 StatusNet Inc.
  41. * @author Brion Vibber <brion@pobox.com>
  42. * @author Mikael Nordfeldth <mmn@hethane.se>
  43. * @author Nym Coy <nymcoy@gmail.com>
  44. * @copyright 2009-2014 Free Software Foundation, Inc http://www.fsf.org
  45. * @author Daniel Supernault <danielsupernault@gmail.com>
  46. * @author Diogo Cordeiro <mail@diogo.site>
  47. * @author Hugo Sales <hugo@hsal.es>
  48. * @copyright 2018-2021 Free Software Foundation, Inc http://www.fsf.org
  49. * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
  50. */
  51. class Nickname
  52. {
  53. /**
  54. * Maximum number of characters in a canonical-form nickname. Changes must validate regexs
  55. */
  56. public const MAX_LEN = 64;
  57. /**
  58. * Regex fragment for pulling a formated nickname *OR* ID number.
  59. * Suitable for router def of 'id' parameters on API actions.
  60. *
  61. * Not guaranteed to be valid after normalization; run the string through
  62. * Nickname::normalize() to get the canonical form, or Nickname::isValid()
  63. * if you just need to check if it's properly formatted.
  64. *
  65. * This, DISPLAY_FMT, and CANONICAL_FMT should not be enclosed in []s.
  66. */
  67. public const INPUT_FMT = '(?:[0-9]+|[0-9a-zA-Z_]{1,' . self::MAX_LEN . '})';
  68. /**
  69. * Regex fragment for acceptable user-formatted variant of a nickname.
  70. *
  71. * This includes some chars such as underscore which will be removed
  72. * from the normalized canonical form, but still must fit within
  73. * field length limits.
  74. *
  75. * Not guaranteed to be valid after normalization; run the string through
  76. * Nickname::normalize() to get the canonical form, or Nickname::isValid()
  77. * if you just need to check if it's properly formatted.
  78. *
  79. * This, INPUT_FMT and CANONICAL_FMT should not be enclosed in []s.
  80. */
  81. public const DISPLAY_FMT = '[0-9a-zA-Z_]{1,' . self::MAX_LEN . '}';
  82. /**
  83. * Simplified regex fragment for acceptable full WebFinger ID of a user
  84. *
  85. * We could probably use an email regex here, but mainly we are interested
  86. * in matching it in our URLs, like https://social.example/user@example.com
  87. */
  88. public const WEBFINGER_FMT = '(?:\w+[\w\-\_\.]*)?\w+\@' . URL_REGEX_DOMAIN_NAME;
  89. /**
  90. * Regex fragment for checking a canonical nickname.
  91. *
  92. * Any non-matching string is not a valid canonical/normalized nickname.
  93. * Matching strings are valid and canonical form, but may still be
  94. * unavailable for registration due to blacklisting et.
  95. *
  96. * Only the canonical forms should be stored as keys in the database;
  97. * there are multiple possible denormalized forms for each valid
  98. * canonical-form name.
  99. *
  100. * This, INPUT_FMT and DISPLAY_FMT should not be enclosed in []s.
  101. */
  102. // const CANONICAL_FMT = '[0-9A-Za-z_]{1,' . self::MAX_LEN . '}';
  103. public const CANONICAL_FMT = self::DISPLAY_FMT;
  104. /**
  105. * Regex with non-capturing group that matches whitespace and some
  106. * characters which are allowed right before an @ or ! when mentioning
  107. * other users. Like: 'This goes out to:@mmn (@chimo too) (!awwyiss).'
  108. *
  109. * FIXME: Make this so you can have multiple whitespace but not multiple
  110. * parenthesis or something. '(((@n_n@)))' might as well be a smiley.
  111. */
  112. public const BEFORE_MENTIONS = '(?:^|[\s\.\,\:\;\[\(\>]+)';
  113. public const CHECK_LOCAL_USER = 1;
  114. public const CHECK_LOCAL_GROUP = 2;
  115. /**
  116. * Check if a nickname is valid or throw exceptions if it's not.
  117. * Can optionally check if the nickname is currently in use
  118. *
  119. * @throws NicknameEmptyException
  120. * @throws NicknameNotAllowedException
  121. * @throws NicknameTakenException
  122. * @throws NicknameTooLongException
  123. */
  124. public static function validate(string $nickname, bool $check_already_used = false, int $which = self::CHECK_LOCAL_USER, bool $check_is_allowed = true): bool
  125. {
  126. $length = mb_strlen($nickname);
  127. if ($length < 1) {
  128. throw new NicknameEmptyException();
  129. } else {
  130. if ($length > self::MAX_LEN) {
  131. throw new NicknameTooLongException();
  132. } elseif ($check_is_allowed && self::isBlacklisted($nickname)) {
  133. throw new NicknameNotAllowedException();
  134. } elseif ($check_already_used) {
  135. switch ($which) {
  136. case self::CHECK_LOCAL_USER:
  137. try {
  138. $lu = DB::findOneBy('local_user', ['nickname' => $nickname]);
  139. throw new NicknameTakenException($lu->getActor());
  140. } catch (NotFoundException) {
  141. // continue
  142. } catch (DuplicateFoundException) {
  143. Log::critial("Duplicate entry in `local_user` for nickname={$nickname}");
  144. }
  145. break;
  146. // @codeCoverageIgnoreStart
  147. case self::CHECK_LOCAL_GROUP:
  148. throw new NotImplementedException();
  149. break;
  150. default:
  151. throw new InvalidArgumentException();
  152. // @codeCoverageIgnoreEnd
  153. }
  154. }
  155. }
  156. return true;
  157. }
  158. /**
  159. * Normalize input $nickname to its canonical form and validates it.
  160. * The canonical form will be returned, or an exception thrown if invalid.
  161. *
  162. * @throws NicknameEmptyException
  163. * @throws NicknameException (base class)
  164. * @throws NicknameInvalidException
  165. * @throws NicknameNotAllowedException
  166. * @throws NicknameTakenException
  167. * @throws NicknameTooLongException
  168. */
  169. public static function normalize(string $nickname, bool $check_already_used = false, int $which = self::CHECK_LOCAL_USER, bool $check_is_allowed = true): string
  170. {
  171. // Nicknames are lower case and without trailing spaces, it's not offensive to sanitize that to the user
  172. $nickname = trim($nickname);
  173. $nickname = mb_strtolower($nickname);
  174. // Anything else would likely be very confusing
  175. // We could, e.g., do UTF-8 normalization (å to a, etc.) with something like Normalizer::normalize($nickname, Normalizer::FORM_C)
  176. // We won't as it could confuse tremendously the user, he must know what is valid and should fix his own input
  177. if (!self::validate(nickname: $nickname, check_already_used: $check_already_used, which: $which, check_is_allowed: $check_is_allowed) || !self::isCanonical($nickname)) {
  178. throw new NicknameInvalidException();
  179. }
  180. return $nickname;
  181. }
  182. /**
  183. * Nice simple check of whether the given string is a valid input nickname,
  184. * which can be normalized into an internally canonical form.
  185. *
  186. * Note that valid nicknames may be in use or blacklisted.
  187. *
  188. * @return bool True if nickname is valid. False if invalid (or taken if $check_already_used == true).
  189. */
  190. public static function isValid(string $nickname, bool $check_already_used = false, int $which = self::CHECK_LOCAL_USER, bool $check_is_allowed = true): bool
  191. {
  192. try {
  193. self::normalize(nickname: $nickname, check_already_used: $check_already_used, which: $which, check_is_allowed: $check_is_allowed);
  194. } catch (NicknameException) {
  195. return false;
  196. }
  197. return true;
  198. }
  199. /**
  200. * Is the given string a valid canonical nickname form?
  201. */
  202. public static function isCanonical(string $nickname): bool
  203. {
  204. return preg_match('/^(?:' . self::CANONICAL_FMT . ')$/', $nickname) > 0;
  205. }
  206. /**
  207. * Is the given string in our nickname blacklist?
  208. *
  209. * @throws NicknameEmptyException
  210. * @throws NicknameInvalidException
  211. * @throws NicknameNotAllowedException
  212. * @throws NicknameTakenException
  213. * @throws NicknameTooLongException
  214. */
  215. public static function isBlacklisted(string $nickname): bool
  216. {
  217. $reserved = Common::config('nickname', 'blacklist');
  218. if (empty($reserved)) {
  219. return false;
  220. }
  221. return \in_array(
  222. $nickname,
  223. array_merge(
  224. $reserved,
  225. F\map(
  226. $reserved,
  227. fn ($n) => self::normalize($n, check_already_used: false, check_is_allowed: false),
  228. ),
  229. ),
  230. );
  231. }
  232. }