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