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