]> git.mxchange.org Git - quix0rs-gnu-social.git/commitdiff
Merge branch 'namecase' into 0.9.x
authorBrion Vibber <brion@pobox.com>
Tue, 30 Nov 2010 00:47:51 +0000 (16:47 -0800)
committerBrion Vibber <brion@pobox.com>
Tue, 30 Nov 2010 00:47:51 +0000 (16:47 -0800)
18 files changed:
actions/apigroupcreate.php
actions/editgroup.php
actions/newgroup.php
actions/profilesettings.php
actions/register.php
classes/User.php
lib/command.php
lib/common.php
lib/nickname.php [new file with mode: 0644]
lib/router.php
lib/util.php
plugins/Facebook/FBConnectAuth.php
plugins/FacebookBridge/actions/facebookfinishlogin.php
plugins/Mapstraction/MapstractionPlugin.php
plugins/Mapstraction/map.php
plugins/OpenID/finishopenidlogin.php
plugins/TwitterBridge/twitterauthorization.php
tests/NicknameTest.php [new file with mode: 0644]

index 54875a7188fd77c68a6e2de900db81e5d462cd30..d01504bc8087065bf81dbe54af609295c2dd689d 100644 (file)
@@ -73,7 +73,7 @@ class ApiGroupCreateAction extends ApiAuthAction
 
         $this->user  = $this->auth_user;
 
-        $this->nickname    = $this->arg('nickname');
+        $this->nickname    = Nickname::normalize($this->arg('nickname'));
         $this->fullname    = $this->arg('full_name');
         $this->homepage    = $this->arg('homepage');
         $this->description = $this->arg('description');
@@ -150,26 +150,7 @@ class ApiGroupCreateAction extends ApiAuthAction
      */
     function validateParams()
     {
-        $valid = Validate::string(
-            $this->nickname, array(
-                'min_length' => 1,
-                'max_length' => 64,
-                'format' => NICKNAME_FMT
-            )
-        );
-
-        if (!$valid) {
-            $this->clientError(
-                // TRANS: Validation error in form for group creation.
-                _(
-                    'Nickname must have only lowercase letters ' .
-                    'and numbers and no spaces.'
-                ),
-                403,
-                $this->format
-            );
-            return false;
-        } elseif ($this->groupNicknameExists($this->nickname)) {
+        if ($this->groupNicknameExists($this->nickname)) {
             $this->clientError(
                 // TRANS: Client error trying to create a group with a nickname this is already in use.
                 _('Nickname already in use. Try another one.'),
@@ -265,15 +246,7 @@ class ApiGroupCreateAction extends ApiAuthAction
 
         foreach ($this->aliases as $alias) {
 
-            $valid = Validate::string(
-                $alias, array(
-                    'min_length' => 1,
-                    'max_length' => 64,
-                    'format' => NICKNAME_FMT
-                )
-            );
-
-            if (!$valid) {
+            if (!Nickname::isValid($alias)) {
                 $this->clientError(
                     // TRANS: Client error shown when providing an invalid alias during group creation.
                     // TRANS: %s is the invalid alias.
index 4d3af34c7b9e4c225fa50b8e6f70a6e143346f96..ab4dbb28360e30278d8515a06a3c61b7fef85f09 100644 (file)
@@ -177,21 +177,14 @@ class EditgroupAction extends GroupDesignAction
             return;
         }
 
-        $nickname    = common_canonical_nickname($this->trimmed('nickname'));
+        $nickname    = Nickname::normalize($this->trimmed('nickname'));
         $fullname    = $this->trimmed('fullname');
         $homepage    = $this->trimmed('homepage');
         $description = $this->trimmed('description');
         $location    = $this->trimmed('location');
         $aliasstring = $this->trimmed('aliases');
 
-        if (!Validate::string($nickname, array('min_length' => 1,
-                                               'max_length' => 64,
-                                               'format' => NICKNAME_FMT))) {
-            // TRANS: Group edit form validation error.
-            $this->showForm(_('Nickname must have only lowercase letters '.
-                              'and numbers and no spaces.'));
-            return;
-        } else if ($this->nicknameExists($nickname)) {
+        if ($this->nicknameExists($nickname)) {
             // TRANS: Group edit form validation error.
             $this->showForm(_('Nickname already in use. Try another one.'));
             return;
@@ -241,9 +234,7 @@ class EditgroupAction extends GroupDesignAction
         }
 
         foreach ($aliases as $alias) {
-            if (!Validate::string($alias, array('min_length' => 1,
-                                                'max_length' => 64,
-                                                'format' => NICKNAME_FMT))) {
+            if (!Nickname::isValid($alias)) {
                 // TRANS: Group edit form validation error.
                 $this->showForm(sprintf(_('Invalid alias: "%s"'), $alias));
                 return;
index e0e7978c32ebb430d07972e96b00cfa204e30128..95af6415e507b303272226511e737c0900dfb283 100644 (file)
@@ -113,21 +113,18 @@ class NewgroupAction extends Action
 
     function trySave()
     {
-        $nickname    = $this->trimmed('nickname');
+        try {
+            $nickname = Nickname::normalize($this->trimmed('nickname'));
+        } catch (NicknameException $e) {
+            $this->showForm($e->getMessage());
+        }
         $fullname    = $this->trimmed('fullname');
         $homepage    = $this->trimmed('homepage');
         $description = $this->trimmed('description');
         $location    = $this->trimmed('location');
         $aliasstring = $this->trimmed('aliases');
 
-        if (!Validate::string($nickname, array('min_length' => 1,
-                                               'max_length' => 64,
-                                               'format' => NICKNAME_FMT))) {
-            // TRANS: Group create form validation error.
-            $this->showForm(_('Nickname must have only lowercase letters '.
-                              'and numbers and no spaces.'));
-            return;
-        } else if ($this->nicknameExists($nickname)) {
+        if ($this->nicknameExists($nickname)) {
             // TRANS: Group create form validation error.
             $this->showForm(_('Nickname already in use. Try another one.'));
             return;
@@ -177,9 +174,7 @@ class NewgroupAction extends Action
         }
 
         foreach ($aliases as $alias) {
-            if (!Validate::string($alias, array('min_length' => 1,
-                                                'max_length' => 64,
-                                                'format' => NICKNAME_FMT))) {
+            if (!Nickname::isValid($alias)) {
                 // TRANS: Group create form validation error.
                 $this->showForm(sprintf(_('Invalid alias: "%s"'), $alias));
                 return;
index e1a0f8b6d09edf032bdc0f2075a8aaea13543153..28b1d20f34125fab3b39b60281de0b08458dfeb2 100644 (file)
@@ -225,7 +225,13 @@ class ProfilesettingsAction extends AccountSettingsAction
 
         if (Event::handle('StartProfileSaveForm', array($this))) {
 
-            $nickname = $this->trimmed('nickname');
+            try {
+                $nickname = Nickname::normalize($this->trimmed('nickname'));
+            } catch (NicknameException $e) {
+                $this->showForm($e->getMessage());
+                return;
+            }
+
             $fullname = $this->trimmed('fullname');
             $homepage = $this->trimmed('homepage');
             $bio = $this->trimmed('bio');
@@ -236,13 +242,7 @@ class ProfilesettingsAction extends AccountSettingsAction
             $tagstring = $this->trimmed('tags');
 
             // Some validation
-            if (!Validate::string($nickname, array('min_length' => 1,
-                                                   'max_length' => 64,
-                                                   'format' => NICKNAME_FMT))) {
-                // TRANS: Validation error in form for profile settings.
-                $this->showForm(_('Nickname must have only lowercase letters and numbers and no spaces.'));
-                return;
-            } else if (!User::allowed_nickname($nickname)) {
+            if (!User::allowed_nickname($nickname)) {
                 // TRANS: Validation error in form for profile settings.
                 $this->showForm(_('Not a valid nickname.'));
                 return;
index 3ae3f56f60efd4f7eedb724bd4b1054ba0cf63b2..5d91aef70e857b622ce0420aaf35e131ae288a11 100644 (file)
@@ -198,7 +198,11 @@ class RegisterAction extends Action
             }
 
             // Input scrubbing
-            $nickname = common_canonical_nickname($nickname);
+            try {
+                $nickname = Nickname::normalize($nickname);
+            } catch (NicknameException $e) {
+                $this->showForm($e->getMessage());
+            }
             $email    = common_canonical_email($email);
 
             if (!$this->boolean('license')) {
@@ -206,11 +210,6 @@ class RegisterAction extends Action
                                   'agree to the license.'));
             } else if ($email && !Validate::email($email, common_config('email', 'check_domain'))) {
                 $this->showForm(_('Not a valid email address.'));
-            } else if (!Validate::string($nickname, array('min_length' => 1,
-                                                          'max_length' => 64,
-                                                          'format' => NICKNAME_FMT))) {
-                $this->showForm(_('Nickname must have only lowercase letters '.
-                                  'and numbers and no spaces.'));
             } else if ($this->nicknameExists($nickname)) {
                 $this->showForm(_('Nickname already in use. Try another one.'));
             } else if (!User::allowed_nickname($nickname)) {
index 964bc3e7f3b4f0ea48764f09a499ad69d3b4c037..92180a9fbc4d747b8809588cfdcd4040d4172d97 100644 (file)
@@ -116,6 +116,16 @@ class User extends Memcached_DataObject
         return $result;
     }
 
+    /**
+     * Check whether the given nickname is potentially usable, or if it's
+     * excluded by any blacklists on this system.
+     *
+     * WARNING: INPUT IS NOT VALIDATED OR NORMALIZED. NON-NORMALIZED INPUT
+     * OR INVALID INPUT MAY LEAD TO FALSE RESULTS.
+     *
+     * @param string $nickname
+     * @return boolean true if clear, false if blacklisted
+     */
     static function allowed_nickname($nickname)
     {
         // XXX: should already be validated for size, content, etc.
index ae69f04a1366b80d990110344374db868c40ef09..2a8075e7bacc11aa9d4f52b0b0b04f9f6ed1263e 100644 (file)
@@ -139,7 +139,7 @@ class Command
     {
         $user = null;
         if (Event::handle('StartCommandGetUser', array($this, $arg, &$user))) {
-            $user = User::staticGet('nickname', $arg);
+            $user = User::staticGet('nickname', Nickname::normalize($arg));
         }
         Event::handle('EndCommandGetUser', array($this, $arg, &$user));
         if (!$user){
index cd4fbfb15a9455db889c3c52920b1fd17897ddf1..d891807185fa24de3d8e0c3ece6f1ed5c45335f8 100644 (file)
@@ -117,6 +117,17 @@ require_once 'markdown.php';
 
 // XXX: other formats here
 
+/**
+ * Avoid the NICKNAME_FMT constant; use the Nickname class instead.
+ *
+ * Nickname::DISPLAY_FMT is more suitable for inserting into regexes;
+ * note that it includes the [] and repeating bits, so should be wrapped
+ * directly in a capture paren usually.
+ *
+ * For validation, use Nickname::validate() etc.
+ *
+ * @deprecated
+ */
 define('NICKNAME_FMT', VALIDATE_NUM.VALIDATE_ALPHA_LOWER);
 
 require_once INSTALLDIR.'/lib/util.php';
diff --git a/lib/nickname.php b/lib/nickname.php
new file mode 100644 (file)
index 0000000..48269f3
--- /dev/null
@@ -0,0 +1,176 @@
+<?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
+{
+    /**
+     * Regex fragment for pulling an arbitrarily-formated nickname.
+     *
+     * Not guaranteed to be valid after normalization; run the string through
+     * Nickname::normalize() to get the canonical form, or Nickname::validate()
+     * if you just need to check if it's properly formatted.
+     *
+     * This and CANONICAL_FMT replace the old NICKNAME_FMT, but be aware
+     * that these should not be enclosed in []s.
+     */
+    const DISPLAY_FMT = '[0-9a-zA-Z_]+';
+
+    /**
+     * 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.
+     *
+     * This and DISPLAY_FMT replace the old NICKNAME_FMT, but be aware
+     * that these should not be enclosed in []s.
+     */
+    const CANONICAL_FMT = '[0-9a-z]{1,64}';
+
+    /**
+     * Maximum number of characters in a canonical-form nickname.
+     */
+    const MAX_LEN = 64;
+
+    /**
+     * 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.
+     *
+     * @param string $str
+     * @return boolean
+     */
+    public static function validate($str)
+    {
+        try {
+            self::normalize($str);
+            return true;
+        } catch (NicknameException $e) {
+            return false;
+        }
+    }
+
+    /**
+     * Validate an input nickname string, and normalize it to its canonical form.
+     * The canonical form will be returned, or an exception thrown if invalid.
+     *
+     * @param string $str
+     * @return string Normalized canonical form of $str
+     *
+     * @throws NicknameException (base class)
+     * @throws   NicknameInvalidException
+     * @throws   NicknameEmptyException
+     * @throws   NicknameTooLongException
+     */
+    public static function normalize($str)
+    {
+        $str = trim($str);
+        $str = str_replace('_', '', $str);
+        $str = mb_strtolower($str);
+
+        $len = mb_strlen($str);
+        if ($len < 1) {
+            throw new NicknameEmptyException();
+        } else if ($len > self::MAX_LEN) {
+            throw new NicknameTooLongException();
+        }
+        if (!self::isCanonical($str)) {
+            throw new NicknameInvalidException();
+        }
+
+        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);
+    }
+}
+
+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.');
+    }
+}
+
+class NicknameEmptyException 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 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);
+    }
+}
index 47357ca0856282e516724d12ab9885b5c0fcc8de..7d675bb5471713dc7b0733672613593b810c5608 100644 (file)
@@ -223,10 +223,10 @@ class Router
             $m->connect('notice/new', array('action' => 'newnotice'));
             $m->connect('notice/new?replyto=:replyto',
                         array('action' => 'newnotice'),
-                        array('replyto' => '[A-Za-z0-9_-]+'));
+                        array('replyto' => Nickname::DISPLAY_FMT));
             $m->connect('notice/new?replyto=:replyto&inreplyto=:inreplyto',
                         array('action' => 'newnotice'),
-                        array('replyto' => '[A-Za-z0-9_-]+'),
+                        array('replyto' => Nickname::DISPLAY_FMT),
                         array('inreplyto' => '[0-9]+'));
 
             $m->connect('notice/:notice/file',
@@ -250,7 +250,7 @@ class Router
                         array('id' => '[0-9]+'));
 
             $m->connect('message/new', array('action' => 'newmessage'));
-            $m->connect('message/new?to=:to', array('action' => 'newmessage'), array('to' => '[A-Za-z0-9_-]+'));
+            $m->connect('message/new?to=:to', array('action' => 'newmessage'), array('to' => Nickname::DISPLAY_FMT));
             $m->connect('message/:message',
                         array('action' => 'showmessage'),
                         array('message' => '[0-9]+'));
@@ -281,7 +281,7 @@ class Router
             foreach (array('edit', 'join', 'leave', 'delete') as $v) {
                 $m->connect('group/:nickname/'.$v,
                             array('action' => $v.'group'),
-                            array('nickname' => '[a-zA-Z0-9]+'));
+                            array('nickname' => Nickname::DISPLAY_FMT));
                 $m->connect('group/:id/id/'.$v,
                             array('action' => $v.'group'),
                             array('id' => '[0-9]+'));
@@ -290,20 +290,20 @@ class Router
             foreach (array('members', 'logo', 'rss', 'designsettings') as $n) {
                 $m->connect('group/:nickname/'.$n,
                             array('action' => 'group'.$n),
-                            array('nickname' => '[a-zA-Z0-9]+'));
+                            array('nickname' => Nickname::DISPLAY_FMT));
             }
 
             $m->connect('group/:nickname/foaf',
                         array('action' => 'foafgroup'),
-                        array('nickname' => '[a-zA-Z0-9]+'));
+                        array('nickname' => Nickname::DISPLAY_FMT));
 
             $m->connect('group/:nickname/blocked',
                         array('action' => 'blockedfromgroup'),
-                        array('nickname' => '[a-zA-Z0-9]+'));
+                        array('nickname' => Nickname::DISPLAY_FMT));
 
             $m->connect('group/:nickname/makeadmin',
                         array('action' => 'makeadmin'),
-                        array('nickname' => '[a-zA-Z0-9]+'));
+                        array('nickname' => Nickname::DISPLAY_FMT));
 
             $m->connect('group/:id/id',
                         array('action' => 'groupbyid'),
@@ -311,7 +311,7 @@ class Router
 
             $m->connect('group/:nickname',
                         array('action' => 'showgroup'),
-                        array('nickname' => '[a-zA-Z0-9]+'));
+                        array('nickname' => Nickname::DISPLAY_FMT));
 
             $m->connect('group/', array('action' => 'groups'));
             $m->connect('group', array('action' => 'groups'));
@@ -332,7 +332,7 @@ class Router
 
             $m->connect('api/statuses/friends_timeline/:id.:format',
                         array('action' => 'ApiTimelineFriends',
-                              'id' => '[a-zA-Z0-9]+',
+                              'id' => Nickname::DISPLAY_FMT,
                               'format' => '(xml|json|rss|atom)'));
 
             $m->connect('api/statuses/home_timeline.:format',
@@ -341,7 +341,7 @@ class Router
 
             $m->connect('api/statuses/home_timeline/:id.:format',
                         array('action' => 'ApiTimelineHome',
-                              'id' => '[a-zA-Z0-9]+',
+                              'id' => Nickname::DISPLAY_FMT,
                               'format' => '(xml|json|rss|atom)'));
 
             $m->connect('api/statuses/user_timeline.:format',
@@ -350,7 +350,7 @@ class Router
 
             $m->connect('api/statuses/user_timeline/:id.:format',
                         array('action' => 'ApiTimelineUser',
-                              'id' => '[a-zA-Z0-9]+',
+                              'id' => Nickname::DISPLAY_FMT,
                               'format' => '(xml|json|rss|atom)'));
 
             $m->connect('api/statuses/mentions.:format',
@@ -359,7 +359,7 @@ class Router
 
             $m->connect('api/statuses/mentions/:id.:format',
                         array('action' => 'ApiTimelineMentions',
-                              'id' => '[a-zA-Z0-9]+',
+                              'id' => Nickname::DISPLAY_FMT,
                               'format' => '(xml|json|rss|atom)'));
 
             $m->connect('api/statuses/replies.:format',
@@ -368,7 +368,7 @@ class Router
 
             $m->connect('api/statuses/replies/:id.:format',
                         array('action' => 'ApiTimelineMentions',
-                              'id' => '[a-zA-Z0-9]+',
+                              'id' => Nickname::DISPLAY_FMT,
                               'format' => '(xml|json|rss|atom)'));
 
             $m->connect('api/statuses/retweeted_by_me.:format',
@@ -389,7 +389,7 @@ class Router
 
             $m->connect('api/statuses/friends/:id.:format',
                         array('action' => 'ApiUserFriends',
-                              'id' => '[a-zA-Z0-9]+',
+                              'id' => Nickname::DISPLAY_FMT,
                               'format' => '(xml|json)'));
 
             $m->connect('api/statuses/followers.:format',
@@ -398,7 +398,7 @@ class Router
 
             $m->connect('api/statuses/followers/:id.:format',
                         array('action' => 'ApiUserFollowers',
-                              'id' => '[a-zA-Z0-9]+',
+                              'id' => Nickname::DISPLAY_FMT,
                               'format' => '(xml|json)'));
 
             $m->connect('api/statuses/show.:format',
@@ -441,7 +441,7 @@ class Router
 
             $m->connect('api/users/show/:id.:format',
                         array('action' => 'ApiUserShow',
-                              'id' => '[a-zA-Z0-9]+',
+                              'id' => Nickname::DISPLAY_FMT,
                               'format' => '(xml|json)'));
 
             // direct messages
@@ -479,12 +479,12 @@ class Router
 
             $m->connect('api/friendships/create/:id.:format',
                         array('action' => 'ApiFriendshipsCreate',
-                              'id' => '[a-zA-Z0-9]+',
+                              'id' => Nickname::DISPLAY_FMT,
                               'format' => '(xml|json)'));
 
             $m->connect('api/friendships/destroy/:id.:format',
                         array('action' => 'ApiFriendshipsDestroy',
-                              'id' => '[a-zA-Z0-9]+',
+                              'id' => Nickname::DISPLAY_FMT,
                               'format' => '(xml|json)'));
 
             // Social graph
@@ -541,17 +541,17 @@ class Router
 
             $m->connect('api/favorites/:id.:format',
                         array('action' => 'ApiTimelineFavorites',
-                              'id' => '[a-zA-Z0-9]+',
+                              'id' => Nickname::DISPLAY_FMT,
                               'format' => '(xml|json|rss|atom)'));
 
             $m->connect('api/favorites/create/:id.:format',
                         array('action' => 'ApiFavoriteCreate',
-                              'id' => '[a-zA-Z0-9]+',
+                              'id' => Nickname::DISPLAY_FMT,
                               'format' => '(xml|json)'));
 
             $m->connect('api/favorites/destroy/:id.:format',
                         array('action' => 'ApiFavoriteDestroy',
-                              'id' => '[a-zA-Z0-9]+',
+                              'id' => Nickname::DISPLAY_FMT,
                               'format' => '(xml|json)'));
             // blocks
 
@@ -561,7 +561,7 @@ class Router
 
             $m->connect('api/blocks/create/:id.:format',
                         array('action' => 'ApiBlockCreate',
-                              'id' => '[a-zA-Z0-9]+',
+                              'id' => Nickname::DISPLAY_FMT,
                               'format' => '(xml|json)'));
 
             $m->connect('api/blocks/destroy.:format',
@@ -570,7 +570,7 @@ class Router
 
             $m->connect('api/blocks/destroy/:id.:format',
                         array('action' => 'ApiBlockDestroy',
-                              'id' => '[a-zA-Z0-9]+',
+                              'id' => Nickname::DISPLAY_FMT,
                               'format' => '(xml|json)'));
             // help
 
@@ -606,7 +606,7 @@ class Router
 
             $m->connect('api/statusnet/groups/timeline/:id.:format',
                         array('action' => 'ApiTimelineGroup',
-                              'id' => '[a-zA-Z0-9]+',
+                              'id' => Nickname::DISPLAY_FMT,
                               'format' => '(xml|json|rss|atom)'));
 
             $m->connect('api/statusnet/groups/show.:format',
@@ -615,12 +615,12 @@ class Router
 
             $m->connect('api/statusnet/groups/show/:id.:format',
                         array('action' => 'ApiGroupShow',
-                              'id' => '[a-zA-Z0-9]+',
+                              'id' => Nickname::DISPLAY_FMT,
                               'format' => '(xml|json)'));
 
             $m->connect('api/statusnet/groups/join.:format',
                         array('action' => 'ApiGroupJoin',
-                              'id' => '[a-zA-Z0-9]+',
+                              'id' => Nickname::DISPLAY_FMT,
                               'format' => '(xml|json)'));
 
             $m->connect('api/statusnet/groups/join/:id.:format',
@@ -629,7 +629,7 @@ class Router
 
             $m->connect('api/statusnet/groups/leave.:format',
                         array('action' => 'ApiGroupLeave',
-                              'id' => '[a-zA-Z0-9]+',
+                              'id' => Nickname::DISPLAY_FMT,
                               'format' => '(xml|json)'));
 
             $m->connect('api/statusnet/groups/leave/:id.:format',
@@ -646,7 +646,7 @@ class Router
 
             $m->connect('api/statusnet/groups/list/:id.:format',
                         array('action' => 'ApiGroupList',
-                              'id' => '[a-zA-Z0-9]+',
+                              'id' => Nickname::DISPLAY_FMT,
                               'format' => '(xml|json|rss|atom)'));
 
             $m->connect('api/statusnet/groups/list_all.:format',
@@ -659,7 +659,7 @@ class Router
 
             $m->connect('api/statusnet/groups/membership/:id.:format',
                         array('action' => 'ApiGroupMembership',
-                              'id' => '[a-zA-Z0-9]+',
+                              'id' => Nickname::DISPLAY_FMT,
                               'format' => '(xml|json)'));
 
             $m->connect('api/statusnet/groups/create.:format',
@@ -692,7 +692,7 @@ class Router
 
             $m->connect('api/statusnet/app/service/:id.xml',
                         array('action' => 'ApiAtomService',
-                              'id' => '[a-zA-Z0-9]+'));
+                              'id' => Nickname::DISPLAY_FMT));
 
             $m->connect('api/statusnet/app/service.xml',
                         array('action' => 'ApiAtomService'));
@@ -789,54 +789,54 @@ class Router
                                'replies', 'inbox', 'outbox', 'microsummary', 'hcard') as $a) {
                     $m->connect(':nickname/'.$a,
                                 array('action' => $a),
-                                array('nickname' => '[a-zA-Z0-9]{1,64}'));
+                                array('nickname' => Nickname::DISPLAY_FMT));
                 }
 
                 foreach (array('subscriptions', 'subscribers') as $a) {
                     $m->connect(':nickname/'.$a.'/:tag',
                                 array('action' => $a),
                                 array('tag' => '[a-zA-Z0-9]+',
-                                      'nickname' => '[a-zA-Z0-9]{1,64}'));
+                                      'nickname' => Nickname::DISPLAY_FMT));
                 }
 
                 foreach (array('rss', 'groups') as $a) {
                     $m->connect(':nickname/'.$a,
                                 array('action' => 'user'.$a),
-                                array('nickname' => '[a-zA-Z0-9]{1,64}'));
+                                array('nickname' => Nickname::DISPLAY_FMT));
                 }
 
                 foreach (array('all', 'replies', 'favorites') as $a) {
                     $m->connect(':nickname/'.$a.'/rss',
                                 array('action' => $a.'rss'),
-                                array('nickname' => '[a-zA-Z0-9]{1,64}'));
+                                array('nickname' => Nickname::DISPLAY_FMT));
                 }
 
                 $m->connect(':nickname/favorites',
                             array('action' => 'showfavorites'),
-                            array('nickname' => '[a-zA-Z0-9]{1,64}'));
+                            array('nickname' => Nickname::DISPLAY_FMT));
 
                 $m->connect(':nickname/avatar/:size',
                             array('action' => 'avatarbynickname'),
                             array('size' => '(original|96|48|24)',
-                                  'nickname' => '[a-zA-Z0-9]{1,64}'));
+                                  'nickname' => Nickname::DISPLAY_FMT));
 
                 $m->connect(':nickname/tag/:tag/rss',
                             array('action' => 'userrss'),
-                            array('nickname' => '[a-zA-Z0-9]{1,64}'),
+                            array('nickname' => Nickname::DISPLAY_FMT),
                             array('tag' => '[\pL\pN_\-\.]{1,64}'));
 
                 $m->connect(':nickname/tag/:tag',
                             array('action' => 'showstream'),
-                            array('nickname' => '[a-zA-Z0-9]{1,64}'),
+                            array('nickname' => Nickname::DISPLAY_FMT),
                             array('tag' => '[\pL\pN_\-\.]{1,64}'));
 
                 $m->connect(':nickname/rsd.xml',
                             array('action' => 'rsd'),
-                            array('nickname' => '[a-zA-Z0-9]{1,64}'));
+                            array('nickname' => Nickname::DISPLAY_FMT));
 
                 $m->connect(':nickname',
                             array('action' => 'showstream'),
-                            array('nickname' => '[a-zA-Z0-9]{1,64}'));
+                            array('nickname' => Nickname::DISPLAY_FMT));
             }
 
             // user stuff
index ce5da1cd8134fa050087511392d0c9f2aed06a24..42762b22fb0f846e1afbae94816150d444a59d6f 100644 (file)
@@ -517,14 +517,29 @@ function common_user_cache_hash($user=false)
     }
 }
 
-// get canonical version of nickname for comparison
+/**
+ * get canonical version of nickname for comparison
+ *
+ * @param string $nickname
+ * @return string
+ *
+ * @throws NicknameException on invalid input
+ * @deprecated call Nickname::normalize() directly.
+ */
 function common_canonical_nickname($nickname)
 {
-    // XXX: UTF-8 canonicalization (like combining chars)
-    return strtolower($nickname);
+    return Nickname::normalize($nickname);
 }
 
-// get canonical version of email for comparison
+/**
+ * get canonical version of email for comparison
+ *
+ * @fixme actually normalize
+ * @fixme reject invalid input
+ *
+ * @param string $email
+ * @return string
+ */
 function common_canonical_email($email)
 {
     // XXX: canonicalize UTF-8
@@ -532,15 +547,33 @@ function common_canonical_email($email)
     return $email;
 }
 
+/**
+ * Partial notice markup rendering step: build links to !group references.
+ *
+ * @param string $text partially rendered HTML
+ * @param Notice $notice in whose context we're working
+ * @return string partially rendered HTML
+ */
 function common_render_content($text, $notice)
 {
     $r = common_render_text($text);
     $id = $notice->profile_id;
     $r = common_linkify_mentions($r, $notice);
-    $r = preg_replace('/(^|[\s\.\,\:\;]+)!([A-Za-z0-9]{1,64})/e', "'\\1!'.common_group_link($id, '\\2')", $r);
+    $r = preg_replace('/(^|[\s\.\,\:\;]+)!(' . Nickname::DISPLAY_FMT . ')/e',
+                      "'\\1!'.common_group_link($id, '\\2')", $r);
     return $r;
 }
 
+/**
+ * Finds @-mentions within the partially-rendered text section and
+ * turns them into live links.
+ *
+ * Should generally not be called except from common_render_content().
+ *
+ * @param string $text partially-rendered HTML
+ * @param Notice $notice in-progress or complete Notice object for context
+ * @return string partially-rendered HTML
+ */
 function common_linkify_mentions($text, $notice)
 {
     $mentions = common_find_mentions($text, $notice);
@@ -597,6 +630,21 @@ function common_linkify_mention($mention)
     return $output;
 }
 
+/**
+ * Find @-mentions in the given text, using the given notice object as context.
+ * References will be resolved with common_relative_profile() against the user
+ * who posted the notice.
+ *
+ * Note the return data format is internal, to be used for building links and
+ * such. Should not be used directly; rather, call common_linkify_mentions().
+ *
+ * @param string $text
+ * @param Notice $notice notice in whose context we're building links
+ *
+ * @return array
+ *
+ * @access private
+ */
 function common_find_mentions($text, $notice)
 {
     $mentions = array();
@@ -631,20 +679,15 @@ function common_find_mentions($text, $notice)
             }
         }
 
-        preg_match_all('/^T ([A-Z0-9]{1,64}) /',
-                       $text,
-                       $tmatches,
-                       PREG_OFFSET_CAPTURE);
-
-        preg_match_all('/(?:^|\s+)@(['.NICKNAME_FMT.']{1,64})/',
-                       $text,
-                       $atmatches,
-                       PREG_OFFSET_CAPTURE);
-
-        $matches = array_merge($tmatches[1], $atmatches[1]);
+        $matches = common_find_mentions_raw($text);
 
         foreach ($matches as $match) {
-            $nickname = common_canonical_nickname($match[0]);
+            try {
+                $nickname = Nickname::normalize($match[0]);
+            } catch (NicknameException $e) {
+                // Bogus match? Drop it.
+                continue;
+            }
 
             // Try to get a profile for this nickname.
             // Start with conversation context, then go to
@@ -710,6 +753,31 @@ function common_find_mentions($text, $notice)
     return $mentions;
 }
 
+/**
+ * Does the actual regex pulls to find @-mentions in text.
+ * Should generally not be called directly; for use in common_find_mentions.
+ *
+ * @param string $text
+ * @return array of PCRE match arrays
+ */
+function common_find_mentions_raw($text)
+{
+    $tmatches = array();
+    preg_match_all('/^T (' . Nickname::DISPLAY_FMT . ') /',
+                   $text,
+                   $tmatches,
+                   PREG_OFFSET_CAPTURE);
+
+    $atmatches = array();
+    preg_match_all('/(?:^|\s+)@(' . Nickname::DISPLAY_FMT . ')\b/',
+                   $text,
+                   $atmatches,
+                   PREG_OFFSET_CAPTURE);
+
+    $matches = array_merge($tmatches[1], $atmatches[1]);
+    return $matches;
+}
+
 function common_render_text($text)
 {
     $r = htmlspecialchars($text);
@@ -1004,6 +1072,13 @@ function common_valid_profile_tag($str)
     return preg_match('/^[A-Za-z0-9_\-\.]{1,64}$/', $str);
 }
 
+/**
+ *
+ * @param <type> $sender_id
+ * @param <type> $nickname
+ * @return <type>
+ * @access private
+ */
 function common_group_link($sender_id, $nickname)
 {
     $sender = Profile::staticGet($sender_id);
@@ -1026,13 +1101,37 @@ function common_group_link($sender_id, $nickname)
     }
 }
 
+/**
+ * Resolve an ambiguous profile nickname reference, checking in following order:
+ * - profiles that $sender subscribes to
+ * - profiles that subscribe to $sender
+ * - local user profiles
+ *
+ * WARNING: does not validate or normalize $nickname -- MUST BE PRE-VALIDATED
+ * OR THERE MAY BE A RISK OF SQL INJECTION ATTACKS. THIS FUNCTION DOES NOT
+ * ESCAPE SQL.
+ *
+ * @fixme validate input
+ * @fixme escape SQL
+ * @fixme fix or remove mystery third parameter
+ * @fixme is $sender a User or Profile?
+ *
+ * @param <type> $sender the user or profile in whose context we're looking
+ * @param string $nickname validated nickname of
+ * @param <type> $dt unused mystery parameter; in Notice reply-to handling a timestamp is passed.
+ *
+ * @return Profile or null
+ */
 function common_relative_profile($sender, $nickname, $dt=null)
 {
+    // Will throw exception on invalid input.
+    $nickname = Nickname::normalize($nickname);
+
     // Try to find profiles this profile is subscribed to that have this nickname
     $recipient = new Profile();
     // XXX: use a join instead of a subquery
-    $recipient->whereAdd('EXISTS (SELECT subscribed from subscription where subscriber = '.$sender->id.' and subscribed = id)', 'AND');
-    $recipient->whereAdd("nickname = '" . trim($nickname) . "'", 'AND');
+    $recipient->whereAdd('EXISTS (SELECT subscribed from subscription where subscriber = '.intval($sender->id).' and subscribed = id)', 'AND');
+    $recipient->whereAdd("nickname = '" . $recipient->escape($nickname) . "'", 'AND');
     if ($recipient->find(true)) {
         // XXX: should probably differentiate between profiles with
         // the same name by date of most recent update
@@ -1041,8 +1140,8 @@ function common_relative_profile($sender, $nickname, $dt=null)
     // Try to find profiles that listen to this profile and that have this nickname
     $recipient = new Profile();
     // XXX: use a join instead of a subquery
-    $recipient->whereAdd('EXISTS (SELECT subscriber from subscription where subscribed = '.$sender->id.' and subscriber = id)', 'AND');
-    $recipient->whereAdd("nickname = '" . trim($nickname) . "'", 'AND');
+    $recipient->whereAdd('EXISTS (SELECT subscriber from subscription where subscribed = '.intval($sender->id).' and subscriber = id)', 'AND');
+    $recipient->whereAdd("nickname = '" . $recipient->escape($nickname) . "'", 'AND');
     if ($recipient->find(true)) {
         // XXX: should probably differentiate between profiles with
         // the same name by date of most recent update
index d6d3786261e6ebc6f2ea91224e3966ca66df3423..84d51578f12198dd32c72f343c543e627227755f 100644 (file)
@@ -257,13 +257,10 @@ class FBConnectauthAction extends Action
             }
         }
 
-        $nickname = $this->trimmed('newname');
-
-        if (!Validate::string($nickname, array('min_length' => 1,
-                                               'max_length' => 64,
-                                               'format' => NICKNAME_FMT))) {
-            $this->showForm(_m('Nickname must have only lowercase letters and numbers and no spaces.'));
-            return;
+        try {
+            $nickname = Nickname::normalize($this->trimmed('newname'));
+        } catch (NicknameException $e) {
+            $this->showForm($e->getMessage());
         }
 
         if (!User::allowed_nickname($nickname)) {
@@ -447,9 +444,7 @@ class FBConnectauthAction extends Action
 
     function isNewNickname($str)
     {
-        if (!Validate::string($str, array('min_length' => 1,
-                                          'max_length' => 64,
-                                          'format' => NICKNAME_FMT))) {
+        if (!Nickname::isValid($str)) {
             return false;
         }
         if (!User::allowed_nickname($str)) {
index 2174c5ad4a020fea2ba508885195791f1d073c93..349acd7e2298630e8839c13724c07ce907449e9f 100644 (file)
@@ -324,12 +324,10 @@ class FacebookfinishloginAction extends Action
             }
         }
 
-        $nickname = $this->trimmed('newname');
-
-        if (!Validate::string($nickname, array('min_length' => 1,
-                                               'max_length' => 64,
-                                               'format' => NICKNAME_FMT))) {
-            $this->showForm(_m('Nickname must have only lowercase letters and numbers and no spaces.'));
+        try {
+            $nickname = Nickname::normalize($this->trimmed('newname'));
+        } catch (NicknameException $e) {
+            $this->showForm($e->getMessage());
             return;
         }
 
@@ -639,16 +637,7 @@ class FacebookfinishloginAction extends Action
       */
      function isNewNickname($str)
      {
-        if (
-            !Validate::string(
-                $str,
-                array(
-                    'min_length' => 1,
-                    'max_length' => 64,
-                    'format' => NICKNAME_FMT
-                )
-            )
-        ) {
+        if (!Nickname::isValid($str)) {
             return false;
         }
 
index d5261d8bc7764f833d524462353d0918512f117f..5ad25763e75981fef47defb5a16f71a1297c0fc4 100644 (file)
@@ -67,10 +67,10 @@ class MapstractionPlugin extends Plugin
     {
         $m->connect(':nickname/all/map',
                     array('action' => 'allmap'),
-                    array('nickname' => '['.NICKNAME_FMT.']{1,64}'));
+                    array('nickname' => Nickname::DISPLAY_FMT));
         $m->connect(':nickname/map',
                     array('action' => 'usermap'),
-                    array('nickname' => '['.NICKNAME_FMT.']{1,64}'));
+                    array('nickname' => Nickname::DISPLAY_FMT));
         return true;
     }
 
index 50ff82b67ef2b806f754e6ff08c8ee6803587ece..dbba4edd0cbd7a11c904c60e4ba307d1bf4f5e3b 100644 (file)
@@ -53,7 +53,7 @@ class MapAction extends OwnerDesignAction
         parent::prepare($args);
 
         $nickname_arg = $this->arg('nickname');
-        $nickname     = common_canonical_nickname($nickname_arg);
+        $nickname     = Nickname::normalize($nickname_arg);
 
         // Permanent redirect on non-canonical nickname
 
index 01dd61edb12002b756c6b9e177147f2a30dcbb2a..86dd1c669b95ed30a4334d52c49ca588307e849e 100644 (file)
@@ -272,13 +272,10 @@ class FinishopenidloginAction extends Action
             }
         }
 
-        $nickname = $this->trimmed('newname');
-
-        if (!Validate::string($nickname, array('min_length' => 1,
-                                               'max_length' => 64,
-                                               'format' => NICKNAME_FMT))) {
-            // TRANS: OpenID plugin message. The entered new user name did not conform to the requirements.
-            $this->showForm(_m('Nickname must have only lowercase letters and numbers and no spaces.'));
+        try {
+            $nickname = Nickname::validate($this->trimmed('newname'));
+        } catch (NicknameException $e) {
+            $this->showForm($e->getMessage());
             return;
         }
 
@@ -463,9 +460,7 @@ class FinishopenidloginAction extends Action
 
     function isNewNickname($str)
     {
-        if (!Validate::string($str, array('min_length' => 1,
-                                          'max_length' => 64,
-                                          'format' => NICKNAME_FMT))) {
+        if (!Nickname::isValid($str)) {
             return false;
         }
         if (!User::allowed_nickname($str)) {
index 931a037230b2c26289fbd1cc1ed7fd3ed87647e4..bbe41bd438f181303c033bc59181958300d5f182 100644 (file)
@@ -441,12 +441,10 @@ class TwitterauthorizationAction extends Action
             }
         }
 
-        $nickname = $this->trimmed('newname');
-
-        if (!Validate::string($nickname, array('min_length' => 1,
-                                               'max_length' => 64,
-                                               'format' => NICKNAME_FMT))) {
-            $this->showForm(_m('Nickname must have only lowercase letters and numbers and no spaces.'));
+        try {
+            $nickname = Nickname::normalize($this->trimmed('newname'));
+        } catch (NicknameException $e) {
+            $this->showForm($e->getMessage());
             return;
         }
 
@@ -619,9 +617,7 @@ class TwitterauthorizationAction extends Action
 
     function isNewNickname($str)
     {
-        if (!Validate::string($str, array('min_length' => 1,
-                                          'max_length' => 64,
-                                          'format' => NICKNAME_FMT))) {
+        if (!Nickname::isValid($str)) {
             return false;
         }
         if (!User::allowed_nickname($str)) {
diff --git a/tests/NicknameTest.php b/tests/NicknameTest.php
new file mode 100644 (file)
index 0000000..f49aeba
--- /dev/null
@@ -0,0 +1,111 @@
+<?php
+
+if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) {
+    print "This script must be run from the command line\n";
+    exit();
+}
+
+define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
+define('STATUSNET', true);
+define('LACONICA', true);
+
+require_once INSTALLDIR . '/lib/common.php';
+
+/**
+ * Test cases for nickname validity and normalization.
+ */
+class NicknameTest extends PHPUnit_Framework_TestCase
+{
+    /**
+     * Basic test using Nickname::normalize()
+     *
+     * @dataProvider provider
+     */
+    public function testBasic($input, $expected, $expectedException=null)
+    {
+        $exception = null;
+        $normalized = false;
+        try {
+            $normalized = Nickname::normalize($input);
+        } catch (NicknameException $e) {
+            $exception = $e;
+        }
+
+        if ($expected === false) {
+            if ($expectedException) {
+                $this->assertTrue($exception && $exception instanceof $expectedException,
+                        "invalid input '$input' expected to fail with $expectedException, " .
+                        "got " . get_class($exception) . ': ' . $exception->getMessage());
+            } else {
+                $this->assertTrue($normalized == false,
+                        "invalid input '$input' expected to fail");
+            }
+        } else {
+            $msg = "normalized input nickname '$input' expected to normalize to '$expected', got ";
+            if ($exception) {
+                $msg .= get_class($exception) . ': ' . $exception->getMessage();
+            } else {
+                $msg .= "'$normalized'";
+            }
+            $this->assertEquals($expected, $normalized, $msg);
+        }
+    }
+
+    /**
+     * Test on the regex matching used in common_find_mentions
+     * (testing on the full notice rendering is difficult as it needs
+     * to be able to pull from global state)
+     *
+     * @dataProvider provider
+     */
+    public function testAtReply($input, $expected, $expectedException=null)
+    {
+        if ($expected == false) {
+            // nothing to do
+        } else {
+            $text = "@{$input} awesome! :)";
+            $matches = common_find_mentions_raw($text);
+            $this->assertEquals(1, count($matches));
+            $this->assertEquals($expected, Nickname::normalize($matches[0][0]));
+        }
+    }
+
+    static public function provider()
+    {
+        return array(
+                     array('evan', 'evan'),
+
+                     // Case and underscore variants
+                     array('Evan', 'evan'),
+                     array('EVAN', 'evan'),
+                     array('ev_an', 'evan'),
+                     array('E__V_an', 'evan'),
+                     array('evan1', 'evan1'),
+                     array('evan_1', 'evan1'),
+                     array('0x20', '0x20'),
+                     array('1234', '1234'), // should this be allowed though? :)
+                     array('12__34', '1234'),
+
+                     // Some (currently) invalid chars...
+                     array('^#@&^#@', false, 'NicknameInvalidException'), // all invalid :D
+                     array('ev.an', false, 'NicknameInvalidException'),
+                     array('ev/an', false, 'NicknameInvalidException'),
+                     array('ev an', false, 'NicknameInvalidException'),
+                     array('ev-an', false, 'NicknameInvalidException'),
+
+                     // Non-ASCII letters; currently not allowed, in future
+                     // we'll add them at least with conversion to ASCII.
+                     // Not much use until we have storage of display names,
+                     // though.
+                     array('évan', false, 'NicknameInvalidException'), // so far...
+                     array('Évan', false, 'NicknameInvalidException'), // so far...
+
+                     // Length checks
+                     array('', false, 'NicknameEmptyException'),
+                     array('___', false, 'NicknameEmptyException'),
+                     array('eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', 'eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'), // 64 chars
+                     array('eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee_', 'eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'), // the _ will be trimmed off, remaining valid
+                     array('eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', false, 'NicknameTooLongException'), // 65 chars -- too long
+                     );
+    }
+}