nickname.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. <?php
  2. /*
  3. * StatusNet - the distributed open-source microblogging tool
  4. * Copyright (C) 2008, 2009, StatusNet, Inc.
  5. *
  6. * This program 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. * This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
  18. */
  19. class Nickname
  20. {
  21. public $widgetOpts;
  22. public $scoped;
  23. /**
  24. * Regex fragment for pulling a formated nickname *OR* ID number.
  25. * Suitable for router def of 'id' parameters on API actions.
  26. *
  27. * Not guaranteed to be valid after normalization; run the string through
  28. * Nickname::normalize() to get the canonical form, or Nickname::isValid()
  29. * if you just need to check if it's properly formatted.
  30. *
  31. * This, DISPLAY_FMT, and CANONICAL_FMT should not be enclosed in []s.
  32. *
  33. * @fixme would prefer to define in reference to the other constants
  34. */
  35. const INPUT_FMT = '(?:[0-9]+|[0-9a-zA-Z_]{1,64})';
  36. /**
  37. * Regex fragment for acceptable user-formatted variant of a nickname.
  38. *
  39. * This includes some chars such as underscore which will be removed
  40. * from the normalized canonical form, but still must fit within
  41. * field length limits.
  42. *
  43. * Not guaranteed to be valid after normalization; run the string through
  44. * Nickname::normalize() to get the canonical form, or Nickname::isValid()
  45. * if you just need to check if it's properly formatted.
  46. *
  47. * This, INPUT_FMT and CANONICAL_FMT should not be enclosed in []s.
  48. */
  49. const DISPLAY_FMT = '[0-9a-zA-Z_]{1,64}';
  50. /**
  51. * Simplified regex fragment for acceptable full WebFinger ID of a user
  52. *
  53. * We could probably use an email regex here, but mainly we are interested
  54. * in matching it in our URLs, like https://social.example/user@example.com
  55. */
  56. const WEBFINGER_FMT = '(?:\w+[\w\-\_\.]*)?\w+\@'.URL_REGEX_DOMAIN_NAME;
  57. // old one without support for -_. in nickname part:
  58. // const WEBFINGER_FMT = '[0-9a-zA-Z_]{1,64}\@[0-9a-zA-Z_-.]{3,255}';
  59. /**
  60. * Regex fragment for checking a canonical nickname.
  61. *
  62. * Any non-matching string is not a valid canonical/normalized nickname.
  63. * Matching strings are valid and canonical form, but may still be
  64. * unavailable for registration due to blacklisting et.
  65. *
  66. * Only the canonical forms should be stored as keys in the database;
  67. * there are multiple possible denormalized forms for each valid
  68. * canonical-form name.
  69. *
  70. * This, INPUT_FMT and DISPLAY_FMT should not be enclosed in []s.
  71. */
  72. const CANONICAL_FMT = '[0-9a-z]{1,64}';
  73. /**
  74. * Maximum number of characters in a canonical-form nickname.
  75. */
  76. const MAX_LEN = 64;
  77. /**
  78. * Regex with non-capturing group that matches whitespace and some
  79. * characters which are allowed right before an @ or ! when mentioning
  80. * other users. Like: 'This goes out to:@mmn (@chimo too) (!awwyiss).'
  81. *
  82. * FIXME: Make this so you can have multiple whitespace but not multiple
  83. * parenthesis or something. '(((@n_n@)))' might as well be a smiley.
  84. */
  85. const BEFORE_MENTIONS = '(?:^|[\s\.\,\:\;\[\(]+)';
  86. /**
  87. * Nice simple check of whether the given string is a valid input nickname,
  88. * which can be normalized into an internally canonical form.
  89. *
  90. * Note that valid nicknames may be in use or reserved.
  91. *
  92. * @param string $str The nickname string to test
  93. * @param boolean $checkuse Check if it's in use (return false if it is)
  94. *
  95. * @return boolean True if nickname is valid. False if invalid (or taken if checkuse==true).
  96. */
  97. public static function isValid($str, $checkuse=false)
  98. {
  99. try {
  100. self::normalize($str, $checkuse);
  101. } catch (NicknameException $e) {
  102. return false;
  103. }
  104. return true;
  105. }
  106. /**
  107. * Validate an input nickname string, and normalize it to its canonical form.
  108. * The canonical form will be returned, or an exception thrown if invalid.
  109. *
  110. * @param string $str The nickname string to test
  111. * @param boolean $checkuse Check if it's in use (return false if it is)
  112. * @return string Normalized canonical form of $str
  113. *
  114. * @throws NicknameException (base class)
  115. * @throws NicknameBlacklistedException
  116. * @throws NicknameEmptyException
  117. * @throws NicknameInvalidException
  118. * @throws NicknamePathCollisionException
  119. * @throws NicknameTakenException
  120. * @throws NicknameTooLongException
  121. */
  122. public static function normalize($str, $checkuse=false)
  123. {
  124. if (mb_strlen($str) > self::MAX_LEN) {
  125. // Display forms must also fit!
  126. throw new NicknameTooLongException();
  127. }
  128. // We should also have UTF-8 normalization (å to a etc.)
  129. $str = trim($str);
  130. $str = str_replace('_', '', $str);
  131. $str = mb_strtolower($str);
  132. if (mb_strlen($str) < 1) {
  133. throw new NicknameEmptyException();
  134. } elseif (!self::isCanonical($str) && !filter_var($str, FILTER_VALIDATE_EMAIL)) {
  135. throw new NicknameInvalidException();
  136. } elseif (self::isBlacklisted($str)) {
  137. throw new NicknameBlacklistedException();
  138. } elseif (self::isSystemPath($str)) {
  139. throw new NicknamePathCollisionException();
  140. } elseif ($checkuse) {
  141. $profile = self::isTaken($str);
  142. if ($profile instanceof Profile) {
  143. throw new NicknameTakenException($profile);
  144. }
  145. }
  146. return $str;
  147. }
  148. /**
  149. * Is the given string a valid canonical nickname form?
  150. *
  151. * @param string $str
  152. * @return boolean
  153. */
  154. public static function isCanonical($str)
  155. {
  156. return preg_match('/^(?:' . self::CANONICAL_FMT . ')$/', $str);
  157. }
  158. /**
  159. * Is the given string in our nickname blacklist?
  160. *
  161. * @param string $str
  162. * @return boolean
  163. */
  164. public static function isBlacklisted($str)
  165. {
  166. $blacklist = common_config('nickname', 'blacklist');
  167. if(!$blacklist)
  168. return false;
  169. return in_array($str, $blacklist);
  170. }
  171. /**
  172. * Is the given string identical to a system path or route?
  173. * This could probably be put in some other class, but at
  174. * at the moment, only Nickname requires this functionality.
  175. *
  176. * @param string $str
  177. * @return boolean
  178. */
  179. public static function isSystemPath($str)
  180. {
  181. $paths = [];
  182. // All directory and file names in site root should be blacklisted
  183. $d = dir(PUBLICDIR);
  184. while (false !== ($entry = $d->read())) {
  185. $paths[$entry] = true;
  186. }
  187. $d->close();
  188. // All top level names in the router should be blacklisted
  189. $router = Router::get();
  190. foreach ($router->m->getPaths() as $path) {
  191. if (preg_match('/^([^\/\?]+)[\/\?]/',$path,$matches) && isset($matches[1])) {
  192. $paths[$matches[1]] = true;
  193. }
  194. }
  195. // FIXME: this assumes the 'path' is in the first-level directory, though common it's not certain
  196. foreach (['avatar', 'attachments'] as $cat) {
  197. $paths[basename(common_config($cat, 'path') ?? __DIR__."/file/".$cat)] = true;
  198. }
  199. return in_array($str, array_keys($paths));
  200. }
  201. /**
  202. * Is the nickname already in use locally? Checks the User table.
  203. *
  204. * @param string $str
  205. * @return Profile|null Returns Profile if nickname found, otherwise null
  206. */
  207. public static function isTaken($str)
  208. {
  209. $found = User::getKV('nickname', $str);
  210. if ($found instanceof User) {
  211. return $found->getProfile();
  212. }
  213. $found = Local_group::getKV('nickname', $str);
  214. if ($found instanceof Local_group) {
  215. return $found->getProfile();
  216. }
  217. $found = Group_alias::getKV('alias', $str);
  218. if ($found instanceof Group_alias) {
  219. return $found->getProfile();
  220. }
  221. return null;
  222. }
  223. }
  224. class NicknameException extends ClientException
  225. {
  226. function __construct($msg=null, $code=400)
  227. {
  228. if ($msg === null) {
  229. $msg = $this->defaultMessage();
  230. }
  231. parent::__construct($msg, $code);
  232. }
  233. /**
  234. * Default localized message for this type of exception.
  235. * @return string
  236. */
  237. protected function defaultMessage()
  238. {
  239. return null;
  240. }
  241. }
  242. class NicknameInvalidException extends NicknameException {
  243. /**
  244. * Default localized message for this type of exception.
  245. * @return string
  246. */
  247. protected function defaultMessage()
  248. {
  249. // TRANS: Validation error in form for registration, profile and group settings, etc.
  250. return _('Nickname must have only lowercase letters and numbers and no spaces.');
  251. }
  252. }
  253. class NicknameEmptyException extends NicknameInvalidException
  254. {
  255. /**
  256. * Default localized message for this type of exception.
  257. * @return string
  258. */
  259. protected function defaultMessage()
  260. {
  261. // TRANS: Validation error in form for registration, profile and group settings, etc.
  262. return _('Nickname cannot be empty.');
  263. }
  264. }
  265. class NicknameTooLongException extends NicknameInvalidException
  266. {
  267. /**
  268. * Default localized message for this type of exception.
  269. * @return string
  270. */
  271. protected function defaultMessage()
  272. {
  273. // TRANS: Validation error in form for registration, profile and group settings, etc.
  274. return sprintf(_m('Nickname cannot be more than %d character long.',
  275. 'Nickname cannot be more than %d characters long.',
  276. Nickname::MAX_LEN),
  277. Nickname::MAX_LEN);
  278. }
  279. }
  280. class NicknameBlacklistedException extends NicknameException
  281. {
  282. protected function defaultMessage()
  283. {
  284. // TRANS: Validation error in form for registration, profile and group settings, etc.
  285. return _('Nickname is disallowed through blacklist.');
  286. }
  287. }
  288. class NicknamePathCollisionException extends NicknameException
  289. {
  290. protected function defaultMessage()
  291. {
  292. // TRANS: Validation error in form for registration, profile and group settings, etc.
  293. return _('Nickname is identical to system path names.');
  294. }
  295. }
  296. class NicknameTakenException extends NicknameException
  297. {
  298. public $profile = null; // the Profile which occupies the nickname
  299. public function __construct(Profile $profile, $msg=null, $code=400)
  300. {
  301. $this->profile = $profile;
  302. if ($msg === null) {
  303. $msg = $this->defaultMessage();
  304. }
  305. parent::__construct($msg, $code);
  306. }
  307. protected function defaultMessage()
  308. {
  309. // TRANS: Validation error in form for registration, profile and group settings, etc.
  310. return _('Nickname is already in use on this server.');
  311. }
  312. }