Nickname.php 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. <?php
  2. // {{{ License
  3. // This file is part of GNU social - https://www.gnu.org/software/social
  4. //
  5. // GNU social is free software: you can redistribute it and/or modify
  6. // it under the terms of the GNU Affero General Public License as published by
  7. // the Free Software Foundation, either version 3 of the License, or
  8. // (at your option) any later version.
  9. //
  10. // GNU social is distributed in the hope that it will be useful,
  11. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. // GNU Affero General Public License for more details.
  14. //
  15. // You should have received a copy of the GNU Affero General Public License
  16. // along with GNU social. If not, see <http://www.gnu.org/licenses/>.
  17. // }}}
  18. namespace App\Util;
  19. use App\Core\DB\DB;
  20. use App\Entity\GSActor;
  21. use App\Entity\LocalGroup;
  22. use App\Entity\LocalUser;
  23. use Normalizer;
  24. /**
  25. * Nickname validation
  26. *
  27. * @category Validation
  28. * @package GNUsocial
  29. *
  30. * @author Zach Copley <zach@status.net>
  31. * @copyright 2010 StatusNet Inc.
  32. * @author Brion Vibber <brion@pobox.com>
  33. * @author Mikael Nordfeldth <mmn@hethane.se>
  34. * @author Nym Coy <nymcoy@gmail.com>
  35. * @copyright 2009-2014 Free Software Foundation, Inc http://www.fsf.org
  36. * @auuthor Daniel Supernault <danielsupernault@gmail.com>
  37. * @auuthor Diogo Cordeiro <diogo@fc.up.pt>
  38. *
  39. * @author Hugo Sales <hugo@fc.up.pt>
  40. * @copyright 2018-2020 Free Software Foundation, Inc http://www.fsf.org
  41. * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
  42. */
  43. class Nickname
  44. {
  45. /**
  46. * Regex fragment for pulling a formated nickname *OR* ID number.
  47. * Suitable for router def of 'id' parameters on API actions.
  48. *
  49. * Not guaranteed to be valid after normalization; run the string through
  50. * Nickname::normalize() to get the canonical form, or Nickname::isValid()
  51. * if you just need to check if it's properly formatted.
  52. *
  53. * This, DISPLAY_FMT, and CANONICAL_FMT should not be enclosed in []s.
  54. *
  55. * @fixme would prefer to define in reference to the other constants
  56. */
  57. const INPUT_FMT = '(?:[0-9]+|[0-9a-zA-Z_]{1,64})';
  58. /**
  59. * Regex fragment for acceptable user-formatted variant of a nickname.
  60. *
  61. * This includes some chars such as underscore which will be removed
  62. * from the normalized canonical form, but still must fit within
  63. * field length limits.
  64. *
  65. * Not guaranteed to be valid after normalization; run the string through
  66. * Nickname::normalize() to get the canonical form, or Nickname::isValid()
  67. * if you just need to check if it's properly formatted.
  68. *
  69. * This, INPUT_FMT and CANONICAL_FMT should not be enclosed in []s.
  70. */
  71. const DISPLAY_FMT = '[0-9a-zA-Z_]{1,64}';
  72. /**
  73. * Simplified regex fragment for acceptable full WebFinger ID of a user
  74. *
  75. * We could probably use an email regex here, but mainly we are interested
  76. * in matching it in our URLs, like https://social.example/user@example.com
  77. */
  78. const WEBFINGER_FMT = '(?:\w+[\w\-\_\.]*)?\w+\@' . URL_REGEX_DOMAIN_NAME;
  79. /**
  80. * Regex fragment for checking a canonical nickname.
  81. *
  82. * Any non-matching string is not a valid canonical/normalized nickname.
  83. * Matching strings are valid and canonical form, but may still be
  84. * unavailable for registration due to blacklisting et.
  85. *
  86. * Only the canonical forms should be stored as keys in the database;
  87. * there are multiple possible denormalized forms for each valid
  88. * canonical-form name.
  89. *
  90. * This, INPUT_FMT and DISPLAY_FMT should not be enclosed in []s.
  91. */
  92. const CANONICAL_FMT = '[0-9a-z]{1,64}';
  93. /**
  94. * Maximum number of characters in a canonical-form nickname.
  95. */
  96. const MAX_LEN = 64;
  97. /**
  98. * Regex with non-capturing group that matches whitespace and some
  99. * characters which are allowed right before an @ or ! when mentioning
  100. * other users. Like: 'This goes out to:@mmn (@chimo too) (!awwyiss).'
  101. *
  102. * FIXME: Make this so you can have multiple whitespace but not multiple
  103. * parenthesis or something. '(((@n_n@)))' might as well be a smiley.
  104. */
  105. const BEFORE_MENTIONS = '(?:^|[\s\.\,\:\;\[\(]+)';
  106. const CHECK_USED = true;
  107. const NO_CHECK_USED = false;
  108. /**
  109. * Normalize an input $nickname, and normalize it to its canonical form.
  110. * The canonical form will be returned, or an exception thrown if invalid.
  111. *
  112. * @throws NicknameException (base class)
  113. * @throws NicknameBlacklistedException
  114. * @throws NicknameEmptyException
  115. * @throws NicknameInvalidException
  116. * @throws NicknamePathCollisionException
  117. * @throws NicknameTakenException
  118. * @throws NicknameTooLongException
  119. */
  120. public static function normalize(string $nickname, bool $check_already_used = self::NO_CHECK_USED): string
  121. {
  122. if (mb_strlen($nickname) > self::MAX_LEN) {
  123. // Display forms must also fit!
  124. throw new NicknameTooLongException();
  125. }
  126. $nickname = trim($nickname);
  127. $nickname = str_replace('_', '', $nickname);
  128. $nickname = mb_strtolower($nickname);
  129. $nickname = Normalizer::normalize($nickname, Normalizer::FORM_C);
  130. if (mb_strlen($nickname) < 1) {
  131. throw new NicknameEmptyException();
  132. } elseif (!self::isCanonical($nickname) && !filter_var($nickname, FILTER_VALIDATE_EMAIL)) {
  133. throw new NicknameInvalidException();
  134. } elseif (self::isReserved($nickname) || Common::isSystemPath($nickname)) {
  135. throw new NicknameReservedException();
  136. } elseif ($check_already_used) {
  137. $actor = self::isTaken($nickname);
  138. if ($actor instanceof GSActor) {
  139. throw new NicknameTakenException($actor);
  140. }
  141. }
  142. return $nickname;
  143. }
  144. /**
  145. * Nice simple check of whether the given string is a valid input nickname,
  146. * which can be normalized into an internally canonical form.
  147. *
  148. * Note that valid nicknames may be in use or reserved.
  149. *
  150. * @return bool True if nickname is valid. False if invalid (or taken if $check_already_used == true).
  151. */
  152. public static function isValid(string $nickname, bool $check_already_used = self::CHECK_USED): bool
  153. {
  154. try {
  155. self::normalize($nickname, $check_already_used);
  156. } catch (NicknameException $e) {
  157. return false;
  158. }
  159. return true;
  160. }
  161. /**
  162. * Is the given string a valid canonical nickname form?
  163. */
  164. public static function isCanonical(string $nickname): bool
  165. {
  166. return preg_match('/^(?:' . self::CANONICAL_FMT . ')$/', $nickname);
  167. }
  168. /**
  169. * Is the given string in our nickname blacklist?
  170. */
  171. public static function isReserved(string $nickname): bool
  172. {
  173. $reserved = Common::config('nickname', 'reserved');
  174. if (!$reserved) {
  175. return false;
  176. }
  177. return in_array($nickname, $reserved);
  178. }
  179. /**
  180. * Is the nickname already in use locally? Checks the User table.
  181. *
  182. * @return null|GSActor Returns GSActor if nickname found
  183. */
  184. public static function isTaken(string $nickname): ?GSActor
  185. {
  186. $found = DB::findBy('local_user', ['nickname' => $nickname]);
  187. if ($found instanceof LocalUser) {
  188. return $found->getProfile();
  189. }
  190. $found = DB::findBy('local_group', ['nickname' => $nickname]);
  191. if ($found instanceof LocalGroup) {
  192. return $found->getProfile();
  193. }
  194. return null;
  195. }
  196. }