We are no longer offering accounts on this server. Consider https://gitlab.freedesktop.org/ as a place to host projects.

nickname.php 11.1 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
<?php
/*
 * StatusNet - the distributed open-source microblogging tool
 * Copyright (C) 2008, 2009, StatusNet, Inc.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

class Nickname
{
    /**
23 24 25 26 27 28 29
     * Regex fragment for pulling a formated nickname *OR* ID number.
     * Suitable for router def of 'id' parameters on API actions.
     *
     * Not guaranteed to be valid after normalization; run the string through
     * Nickname::normalize() to get the canonical form, or Nickname::isValid()
     * if you just need to check if it's properly formatted.
     *
30
     * This, DISPLAY_FMT, and CANONICAL_FMT should not be enclosed in []s.
31 32 33 34 35 36 37
     *
     * @fixme would prefer to define in reference to the other constants
     */
    const INPUT_FMT = '(?:[0-9]+|[0-9a-zA-Z_]{1,64})';

    /**
     * Regex fragment for acceptable user-formatted variant of a nickname.
38
     *
39 40 41
     * This includes some chars such as underscore which will be removed
     * from the normalized canonical form, but still must fit within
     * 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 46
     * if you just need to check if it's properly formatted.
     *
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 53 54 55 56
    /**
     * Simplified regex fragment for acceptable full WebFinger ID of a user
     *
     * We could probably use an email regex here, but mainly we are interested
     * in matching it in our URLs, like https://social.example/user@example.com
     */
57 58 59 60
    const WEBFINGER_FMT = '(?:\w+[\w\-\_\.]*)?\w+\@'.URL_REGEX_DOMAIN_NAME;

    // old one without support for -_. in nickname part:
    // const WEBFINGER_FMT = '[0-9a-zA-Z_]{1,64}\@[0-9a-zA-Z_-.]{3,255}';
61

62 63 64 65 66 67 68 69 70 71 72
    /**
     * Regex fragment for checking a canonical nickname.
     *
     * Any non-matching string is not a valid canonical/normalized nickname.
     * Matching strings are valid and canonical form, but may still be
     * unavailable for registration due to blacklisting et.
     *
     * Only the canonical forms should be stored as keys in the database;
     * there are multiple possible denormalized forms for each valid
     * canonical-form name.
     *
73
     * This, INPUT_FMT and DISPLAY_FMT should not be enclosed in []s.
74 75 76 77 78 79 80 81
     */
    const CANONICAL_FMT = '[0-9a-z]{1,64}';

    /**
     * Maximum number of characters in a canonical-form nickname.
     */
    const MAX_LEN = 64;

mattl's avatar
mattl committed
82 83 84 85 86 87 88 89 90 91
    /**
     * Regex with non-capturing group that matches whitespace and some
     * characters which are allowed right before an @ or ! when mentioning
     * other users. Like: 'This goes out to:@mmn (@chimo too) (!awwyiss).'
     *
     * FIXME: Make this so you can have multiple whitespace but not multiple
     * parenthesis or something. '(((@n_n@)))' might as well be a smiley.
     */
    const BEFORE_MENTIONS = '(?:^|[\s\.\,\:\;\[\(]+)';

92 93 94 95 96 97
    /**
     * Nice simple check of whether the given string is a valid input nickname,
     * which can be normalized into an internally canonical form.
     *
     * Note that valid nicknames may be in use or reserved.
     *
98 99 100 101
     * @param string    $str       The nickname string to test
     * @param boolean   $checkuse  Check if it's in use (return false if it is)
     *
     * @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 108 109
        } catch (NicknameException $e) {
            return false;
        }
110 111

        return true;
112 113 114 115 116 117
    }

    /**
     * Validate an input nickname string, and normalize it to its canonical form.
     * The canonical form will be returned, or an exception thrown if invalid.
     *
118 119
     * @param string    $str       The nickname string to test
     * @param boolean   $checkuse  Check if it's in use (return false if it is)
120 121 122
     * @return string Normalized canonical form of $str
     *
     * @throws NicknameException (base class)
123
     * @throws   NicknameBlacklistedException
124
     * @throws   NicknameEmptyException
125 126 127
     * @throws   NicknameInvalidException
     * @throws   NicknamePathCollisionException
     * @throws   NicknameTakenException
128 129
     * @throws   NicknameTooLongException
     */
130
    public static function normalize($str, $checkuse=false)
131
    {
132 133 134 135 136
        if (mb_strlen($str) > self::MAX_LEN) {
            // Display forms must also fit!
            throw new NicknameTooLongException();
        }

137
        // We should also have UTF-8 normalization (å to a etc.)
138 139 140 141
        $str = trim($str);
        $str = str_replace('_', '', $str);
        $str = mb_strtolower($str);

142
        if (mb_strlen($str) < 1) {
143
            throw new NicknameEmptyException();
144
        } elseif (!self::isCanonical($str)) {
145
            throw new NicknameInvalidException();
146
        } elseif (self::isBlacklisted($str)) {
147
            throw new NicknameBlacklistedException();
148
        } elseif (self::isSystemPath($str)) {
149
            throw new NicknamePathCollisionException();
150 151 152 153
        } elseif ($checkuse) {
            $profile = self::isTaken($str);
            if ($profile instanceof Profile) {
                throw new NicknameTakenException($profile);
154
            }
155
        }
156 157 158 159 160 161 162 163 164 165 166 167 168 169

        return $str;
    }

    /**
     * Is the given string a valid canonical nickname form?
     *
     * @param string $str
     * @return boolean
     */
    public static function isCanonical($str)
    {
        return preg_match('/^(?:' . self::CANONICAL_FMT . ')$/', $str);
    }
170 171 172 173 174 175 176 177 178 179

    /**
     * Is the given string in our nickname blacklist?
     *
     * @param string $str
     * @return boolean
     */
     public static function isBlacklisted($str)
     {
         $blacklist = common_config('nickname', 'blacklist');
180 181
         if(!$blacklist)
         	return false;
182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199
         return in_array($str, $blacklist);
     }

    /**
     * Is the given string identical to a system path or route?
     * This could probably be put in some other class, but at
     * at the moment, only Nickname requires this functionality.
     *
     * @param string $str
     * @return boolean
     */
     public static function isSystemPath($str)
     {
        $paths = array();

        // All directory and file names in site root should be blacklisted
        $d = dir(INSTALLDIR);
        while (false !== ($entry = $d->read())) {
200
            $paths[$entry] = true;
201 202 203 204 205
        }
        $d->close();

        // All top level names in the router should be blacklisted
        $router = Router::get();
206 207 208
        foreach ($router->m->getPaths() as $path) {
            if (preg_match('/^([^\/\?]+)[\/\?]/',$path,$matches) && isset($matches[1])) {
                $paths[$matches[1]] = true;
209 210
            }
        }
211 212 213 214 215 216

        // FIXME: this assumes the 'path' is in the first-level directory, though common it's not certain
        foreach (['avatar', 'attachments'] as $cat) {
            $paths[basename(common_config($cat, 'path'))] = true;
        }

217
        return in_array($str, array_keys($paths));
218 219 220 221 222 223
    }

    /**
     * Is the nickname already in use locally? Checks the User table.
     *
     * @param   string $str
224
     * @return  Profile|null   Returns Profile if nickname found, otherwise null
225 226 227
     */
    public static function isTaken($str)
    {
228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243
        $found = User::getKV('nickname', $str);
        if ($found instanceof User) {
            return $found->getProfile();
        }

        $found = Local_group::getKV('nickname', $str);
        if ($found instanceof Local_group) {
            return $found->getProfile();
        }

        $found = Group_alias::getKV('alias', $str);
        if ($found instanceof Group_alias) {
            return $found->getProfile();
        }

        return null;
244
    }
245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278
}

class NicknameException extends ClientException
{
    function __construct($msg=null, $code=400)
    {
        if ($msg === null) {
            $msg = $this->defaultMessage();
        }
        parent::__construct($msg, $code);
    }

    /**
     * Default localized message for this type of exception.
     * @return string
     */
    protected function defaultMessage()
    {
        return null;
    }
}

class NicknameInvalidException extends NicknameException {
    /**
     * Default localized message for this type of exception.
     * @return string
     */
    protected function defaultMessage()
    {
        // TRANS: Validation error in form for registration, profile and group settings, etc.
        return _('Nickname must have only lowercase letters and numbers and no spaces.');
    }
}

279
class NicknameEmptyException extends NicknameInvalidException
280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306
{
    /**
     * Default localized message for this type of exception.
     * @return string
     */
    protected function defaultMessage()
    {
        // TRANS: Validation error in form for registration, profile and group settings, etc.
        return _('Nickname cannot be empty.');
    }
}

class NicknameTooLongException extends NicknameInvalidException
{
    /**
     * Default localized message for this type of exception.
     * @return string
     */
    protected function defaultMessage()
    {
        // TRANS: Validation error in form for registration, profile and group settings, etc.
        return sprintf(_m('Nickname cannot be more than %d character long.',
                          'Nickname cannot be more than %d characters long.',
                          Nickname::MAX_LEN),
                       Nickname::MAX_LEN);
    }
}
307

308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326
class NicknameBlacklistedException extends NicknameException
{
    protected function defaultMessage()
    {
        // TRANS: Validation error in form for registration, profile and group settings, etc.
        return _('Nickname is disallowed through blacklist.');
    }
}

class NicknamePathCollisionException extends NicknameException
{
    protected function defaultMessage()
    {
        // TRANS: Validation error in form for registration, profile and group settings, etc.
        return _('Nickname is identical to system path names.');
    }
}

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 335 336 337 338 339 340

        if ($msg === null) {
            $msg = $this->defaultMessage();
        }

        parent::__construct($msg, $code);
    }

341 342 343
    protected function defaultMessage()
    {
        // TRANS: Validation error in form for registration, profile and group settings, etc.
344
        return _('Nickname is already in use on this server.');
345 346
    }
}