nickname.php 9.9 KB

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