]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - lib/nickname.php
Merge branch 'doc-backup-restore-def-vals' into 'nightly'
[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 should not be enclosed in []s.
31      *
32      * @fixme would prefer to define in reference to the other constants
33      */
34     const INPUT_FMT = '(?:[0-9]+|[0-9a-zA-Z_]{1,64})';
35
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     /**
52      * Simplified regex fragment for acceptable full WebFinger ID of a user
53      *
54      * We could probably use an email regex here, but mainly we are interested
55      * in matching it in our URLs, like https://social.example/user@example.com
56      */
57     const WEBFINGER_FMT = '[0-9a-zA-Z_]{1,64}\@[0-9a-zA-Z_-.]{3,255}';
58
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     /**
75      * Maximum number of characters in a canonical-form nickname.
76      */
77     const MAX_LEN = 64;
78
79     /**
80      * Regex with non-capturing group that matches whitespace and some
81      * characters which are allowed right before an @ or ! when mentioning
82      * other users. Like: 'This goes out to:@mmn (@chimo too) (!awwyiss).'
83      *
84      * FIXME: Make this so you can have multiple whitespace but not multiple
85      * parenthesis or something. '(((@n_n@)))' might as well be a smiley.
86      */
87     const BEFORE_MENTIONS = '(?:^|[\s\.\,\:\;\[\(]+)';
88
89     /**
90      * Nice simple check of whether the given string is a valid input nickname,
91      * which can be normalized into an internally canonical form.
92      *
93      * Note that valid nicknames may be in use or reserved.
94      *
95      * @param string    $str       The nickname string to test
96      * @param boolean   $checkuse  Check if it's in use (return false if it is)
97      *
98      * @return boolean  True if nickname is valid. False if invalid (or taken if checkuse==true).
99      */
100     public static function isValid($str, $checkuse=false)
101     {
102         try {
103             self::normalize($str, $checkuse);
104         } catch (NicknameException $e) {
105             return false;
106         }
107
108         return true;
109     }
110
111     /**
112      * Validate an input nickname string, and normalize it to its canonical form.
113      * The canonical form will be returned, or an exception thrown if invalid.
114      *
115      * @param string    $str       The nickname string to test
116      * @param boolean   $checkuse  Check if it's in use (return false if it is)
117      * @return string Normalized canonical form of $str
118      *
119      * @throws NicknameException (base class)
120      * @throws   NicknameBlacklistedException
121      * @throws   NicknameEmptyException
122      * @throws   NicknameInvalidException
123      * @throws   NicknamePathCollisionException
124      * @throws   NicknameTakenException
125      * @throws   NicknameTooLongException
126      */
127     public static function normalize($str, $checkuse=false)
128     {
129         if (mb_strlen($str) > self::MAX_LEN) {
130             // Display forms must also fit!
131             throw new NicknameTooLongException();
132         }
133
134         // We should also have UTF-8 normalization (å to a etc.)
135         $str = trim($str);
136         $str = str_replace('_', '', $str);
137         $str = mb_strtolower($str);
138
139         if (mb_strlen($str) < 1) {
140             throw new NicknameEmptyException();
141         } elseif (!self::isCanonical($str)) {
142             throw new NicknameInvalidException();
143         } elseif (self::isBlacklisted($str)) {
144             throw new NicknameBlacklistedException();
145         } elseif (self::isSystemPath($str)) {
146             throw new NicknamePathCollisionException();
147         } elseif ($checkuse) {
148             $profile = self::isTaken($str);
149             if ($profile instanceof Profile) {
150                 throw new NicknameTakenException($profile);
151             }
152         }
153
154         return $str;
155     }
156
157     /**
158      * Is the given string a valid canonical nickname form?
159      *
160      * @param string $str
161      * @return boolean
162      */
163     public static function isCanonical($str)
164     {
165         return preg_match('/^(?:' . self::CANONICAL_FMT . ')$/', $str);
166     }
167
168     /**
169      * Is the given string in our nickname blacklist?
170      *
171      * @param string $str
172      * @return boolean
173      */
174      public static function isBlacklisted($str)
175      {
176          $blacklist = common_config('nickname', 'blacklist');
177          if(!$blacklist)
178                 return false;
179          return in_array($str, $blacklist);
180      }
181
182     /**
183      * Is the given string identical to a system path or route?
184      * This could probably be put in some other class, but at
185      * at the moment, only Nickname requires this functionality.
186      *
187      * @param string $str
188      * @return boolean
189      */
190      public static function isSystemPath($str)
191      {
192         $paths = array();
193
194         // All directory and file names in site root should be blacklisted
195         $d = dir(INSTALLDIR);
196         while (false !== ($entry = $d->read())) {
197             $paths[$entry] = true;
198         }
199         $d->close();
200
201         // All top level names in the router should be blacklisted
202         $router = Router::get();
203         foreach ($router->m->getPaths() as $path) {
204             if (preg_match('/^([^\/\?]+)[\/\?]/',$path,$matches) && isset($matches[1])) {
205                 $paths[$matches[1]] = true;
206             }
207         }
208
209         // FIXME: this assumes the 'path' is in the first-level directory, though common it's not certain
210         foreach (['avatar', 'attachments'] as $cat) {
211             $paths[basename(common_config($cat, 'path'))] = true;
212         }
213
214         return in_array($str, array_keys($paths));
215     }
216
217     /**
218      * Is the nickname already in use locally? Checks the User table.
219      *
220      * @param   string $str
221      * @return  Profile|null   Returns Profile if nickname found, otherwise null
222      */
223     public static function isTaken($str)
224     {
225         $found = User::getKV('nickname', $str);
226         if ($found instanceof User) {
227             return $found->getProfile();
228         }
229
230         $found = Local_group::getKV('nickname', $str);
231         if ($found instanceof Local_group) {
232             return $found->getProfile();
233         }
234
235         $found = Group_alias::getKV('alias', $str);
236         if ($found instanceof Group_alias) {
237             return $found->getProfile();
238         }
239
240         return null;
241     }
242 }
243
244 class NicknameException extends ClientException
245 {
246     function __construct($msg=null, $code=400)
247     {
248         if ($msg === null) {
249             $msg = $this->defaultMessage();
250         }
251         parent::__construct($msg, $code);
252     }
253
254     /**
255      * Default localized message for this type of exception.
256      * @return string
257      */
258     protected function defaultMessage()
259     {
260         return null;
261     }
262 }
263
264 class NicknameInvalidException extends NicknameException {
265     /**
266      * Default localized message for this type of exception.
267      * @return string
268      */
269     protected function defaultMessage()
270     {
271         // TRANS: Validation error in form for registration, profile and group settings, etc.
272         return _('Nickname must have only lowercase letters and numbers and no spaces.');
273     }
274 }
275
276 class NicknameEmptyException extends NicknameInvalidException
277 {
278     /**
279      * Default localized message for this type of exception.
280      * @return string
281      */
282     protected function defaultMessage()
283     {
284         // TRANS: Validation error in form for registration, profile and group settings, etc.
285         return _('Nickname cannot be empty.');
286     }
287 }
288
289 class NicknameTooLongException extends NicknameInvalidException
290 {
291     /**
292      * Default localized message for this type of exception.
293      * @return string
294      */
295     protected function defaultMessage()
296     {
297         // TRANS: Validation error in form for registration, profile and group settings, etc.
298         return sprintf(_m('Nickname cannot be more than %d character long.',
299                           'Nickname cannot be more than %d characters long.',
300                           Nickname::MAX_LEN),
301                        Nickname::MAX_LEN);
302     }
303 }
304
305 class NicknameBlacklistedException extends NicknameException
306 {
307     protected function defaultMessage()
308     {
309         // TRANS: Validation error in form for registration, profile and group settings, etc.
310         return _('Nickname is disallowed through blacklist.');
311     }
312 }
313
314 class NicknamePathCollisionException extends NicknameException
315 {
316     protected function defaultMessage()
317     {
318         // TRANS: Validation error in form for registration, profile and group settings, etc.
319         return _('Nickname is identical to system path names.');
320     }
321 }
322
323 class NicknameTakenException extends NicknameException
324 {
325     public $profile = null;    // the Profile which occupies the nickname
326
327     public function __construct(Profile $profile, $msg=null, $code=400)
328     {
329         $this->profile = $profile;
330
331         if ($msg === null) {
332             $msg = $this->defaultMessage();
333         }
334
335         parent::__construct($msg, $code);
336     }
337
338     protected function defaultMessage()
339     {
340         // TRANS: Validation error in form for registration, profile and group settings, etc.
341         return _('Nickname is already in use on this server.');
342     }
343 }