Warn on account creation when username is adjusted
authorBartosz Dziewoński <matma.rex@gmail.com>
Thu, 20 Feb 2014 01:30:58 +0000 (02:30 +0100)
committerBartosz Dziewoński <matma.rex@gmail.com>
Wed, 5 Mar 2014 21:20:58 +0000 (22:20 +0100)
The user name can be adjusted due to various technical restrictions:
the first letter is capitalized, underscores are changed to spaces,
numerous other less visible changes happen. Some of these tweaks can
be unwanted by some users.

Generate a warning if that happens. If the user has JavaScript
enabled, the check happens entirely client-side – a little warning box
is shown, the user doesn't have to do anything. Otherwise the check
happens server-side and the user has to resubmit the form.

The way this is done makes it trivial to also check if the username
is invalid or already taken, so let's do that. It also means that we
can't check for all error conditions, e.g. these enforced by
extensions – that is still handled server-side. (Client-side we
intentionally never say that whatever the user typed in is valid – we
only warn when we know it's not.)

API behavior is unchanged.

Co-Authored-By: umherirrender <umherirrender_de.wp@web.de>
Co-Authored-By: Bartosz Dziewoński <matma.rex@gmail.com>
Bug: 34447
Bug: 61416
Change-Id: Ic461a5e597ad71b854dc65bbf8a395c0f55d1fc3

RELEASE-NOTES-1.23
includes/specials/SpecialUserlogin.php
includes/templates/Usercreate.php
languages/messages/MessagesEn.php
languages/messages/MessagesQqq.php
maintenance/language/messages.inc
resources/Resources.php
resources/mediawiki.special/mediawiki.special.userlogin.signup.js

index 2b91f88..798d59c 100644 (file)
@@ -116,6 +116,10 @@ production.
   creations, similar to the topOnly option.
 * Add mediawiki.ui.button styling to all pages so wiki content can use styled
   buttons.
+* Special:UserLogin/signup now does AJAX checks for invalid and taken usernames,
+  displaying the error live.
+* Special:UserLogin/signup now warns the user if their chosen username has to be
+  normalized.
 
 === Bug fixes in 1.23 ===
 * (bug 41759) The "updated since last visit" markers (on history pages, recent
index bbe56ec..67e33b3 100644 (file)
@@ -223,7 +223,7 @@ class LoginForm extends SpecialPage {
                $status = $this->addNewAccountInternal();
                if ( !$status->isGood() ) {
                        $error = $status->getMessage();
-                       $this->mainLoginForm( $error->toString() );
+                       $this->mainLoginForm( $error->toString(), $status->isOK() ? 'warning' : 'error' );
                        return;
                }
 
@@ -259,7 +259,7 @@ class LoginForm extends SpecialPage {
                $status = $this->addNewAccountInternal();
                if ( !$status->isGood() ) {
                        $error = $status->getMessage();
-                       $this->mainLoginForm( $error->toString() );
+                       $this->mainLoginForm( $error->toString(), $status->isOK() ? 'warning' : 'error' );
                        return false;
                }
 
@@ -401,6 +401,10 @@ class LoginForm extends SpecialPage {
                        return Status::newFatal( 'sorbs_create_account_reason' );
                }
 
+               // Leading/trailing/multiple whitespace characters are never accepted in usernames and users
+               // know that, don't warn if someone accidentally types it. We do warn about underscores.
+               $name = trim( preg_replace( '/\s+/', ' ', $this->mUsername ) );
+
                // Normalize the name so that silly things don't cause "invalid username" errors.
                // User::newFromName does some rather strict checking, rejecting e.g. leading/trailing/multiple spaces.
                $title = Title::makeTitleSafe( NS_USER, $this->mUsername );
@@ -408,12 +412,23 @@ class LoginForm extends SpecialPage {
                        return Status::newFatal( 'noname' );
                }
 
-               # Now create a dummy user ($u) and check if it is valid
+               // Now create a dummy user ($u) and check if it is valid.
                $u = User::newFromName( $title->getText(), 'creatable' );
+
                if ( !is_object( $u ) ) {
                        return Status::newFatal( 'noname' );
                } elseif ( 0 != $u->idForName() ) {
                        return Status::newFatal( 'userexists' );
+               } elseif ( $name !== $u->getName() ) {
+                       // User name was adjusted due to technical restrictions (e.g. first letter capitalized).
+                       // This is normally handled by a client-side check, but users with JavaScript disabled get here.
+                       $status = Status::newGood();
+                       $status->warning( 'createacct-normalization', $name, $u->getName() );
+
+                       // Set the form field to the correct name, so the user can just hit the button again.
+                       $this->mUsername = $u->getName();
+
+                       return $status;
                }
 
                if ( $this->mCreateaccountMail ) {
index 0cb83d5..aba0d27 100644 (file)
@@ -58,15 +58,23 @@ class UsercreateTemplate extends BaseTemplate {
                        <section class="mw-form-header">
                                <?php $this->html( 'header' ); /* extensions such as ConfirmEdit add form HTML here */ ?>
                        </section>
+                       <!-- This element is used by the mediawiki.special.userlogin.signup.js module. -->
+                       <div
+                               id="mw-createacct-status-area"
+                               <?php if ( $this->data['message'] ) { ?>
+                                       class="<?php echo $this->data['messagetype']; ?>box"
+                               <?php } else { ?>
+                                       style="display: none;"
+                               <?php } ?>
+                       >
                        <?php if ( $this->data['message'] ) { ?>
-                               <div class="<?php $this->text( 'messagetype' ); ?>box">
                                        <?php if ( $this->data['messagetype'] == 'error' ) { ?>
                                                <strong><?php $this->msg( 'createacct-error' ); ?></strong>
                                                <br />
                                        <?php } ?>
                                        <?php $this->html( 'message' ); ?>
-                               </div>
                        <?php } ?>
+                       </div>
 
                        <div>
                                <label for='wpName2'>
index 4210c0c..f16784a 100644 (file)
@@ -1161,6 +1161,7 @@ Use the form below to log in as another user.',
 'badretype'                       => 'The passwords you entered do not match.',
 'userexists'                      => 'Username entered already in use.
 Please choose a different name.',
+'createacct-normalization'        => 'Your username will be adjusted to "$2" due to technical restrictions.',
 'loginerror'                      => 'Login error',
 'createacct-error'                => 'Account creation error',
 'createaccounterror'              => 'Could not create account: $1',
index 8aad59b..56fafd0 100644 (file)
@@ -1436,6 +1436,9 @@ Parameters:
 * $1 - number of contributors (users)',
 'badretype' => 'Used as error message when the new password and its retype do not match.',
 'userexists' => 'Used as error message in creating a user account.',
+'createacct-normalization' => 'Used as warning message on account creation when user name is adjusted silently due to technical restrictions (e.g. first letter capitalized, underscores converted to spaces).
+* $1 - the old username
+* $2 - the new username',
 'loginerror' => 'Used as title of error message.
 {{Identical|Login error}}',
 'createacct-error' => 'Used as heading for the error message.',
index d97a4cd..dda68a8 100644 (file)
@@ -503,6 +503,7 @@ $wgMessageStructure = array(
                'createacct-benefit-body3',
                'badretype',
                'userexists',
+               'createacct-normalization',
                'loginerror',
                'createacct-error',
                'createaccounterror',
index 5ceda32..a2958eb 100644 (file)
@@ -1249,9 +1249,17 @@ return array(
        'mediawiki.special.userlogin.signup.js' => array(
                'scripts' => 'resources/mediawiki.special/mediawiki.special.userlogin.signup.js',
                'messages' => array(
+                       'createacct-error',
                        'createacct-emailrequired',
+                       'createacct-normalization',
+                       'noname',
+                       'userexists',
+               ),
+               'dependencies' => array(
+                       'mediawiki.api',
+                       'mediawiki.jqueryMsg',
+                       'jquery.throttle-debounce',
                ),
-               'dependencies' => 'mediawiki.jqueryMsg',
        ),
        'mediawiki.special.javaScriptTest' => array(
                'scripts' => 'resources/mediawiki.special/mediawiki.special.javaScriptTest.js',
index c293f65..4fafd6d 100644 (file)
@@ -3,7 +3,7 @@
  */
 ( function ( mw, $ ) {
        // When sending password by email, hide the password input fields.
-       function hidePasswordOnEmail() {
+       $( function () {
                // Always required if checked, otherwise it depends, so we use the original
                var $emailLabel = $( 'label[for="wpEmail"]' ),
                        originalText = $emailLabel.text(),
 
                $createByMailCheckbox.on( 'change', updateForCheckbox );
                updateForCheckbox();
-       }
+       } );
 
-       $( hidePasswordOnEmail );
+       // Show username normalisation warning
+       $( function () {
+               var
+                       // All of these are apparently required to be sure we detect any changes.
+                       events = 'keyup keydown change mouseup cut paste focus blur',
+                       $input = $( '#wpName2' ),
+                       $warningContainer = $( '#mw-createacct-status-area' ),
+                       api = new mw.Api(),
+                       currentRequest,
+                       tweakedUsername;
+
+               // Hide any warnings / errors.
+               function cleanup() {
+                       $warningContainer.slideUp( function () {
+                               $warningContainer
+                                       .removeAttr( 'class' )
+                                       .empty();
+                       } );
+               }
+
+               function updateUsernameStatus() {
+                       var
+                               // Leading/trailing/multiple whitespace characters are never accepted in usernames and users
+                               // know that, don't warn if someone accidentally types it. We do warn about underscores.
+                               username = $.trim( $input.val().replace( /\s+/g, ' ' ) ),
+                               currentRequestInternal;
+
+                       // Abort any pending requests.
+                       if ( currentRequest ) {
+                               currentRequest.abort();
+                       }
+
+                       if ( username === '' ) {
+                               cleanup();
+                               return;
+                       }
+
+                       currentRequest = currentRequestInternal = api.get( {
+                               action: 'query',
+                               list: 'users',
+                               ususers: username // '|' in usernames is handled below
+                       } ).done( function ( resp ) {
+                               var userinfo, state;
+
+                               // Another request was fired in the meantime, the result we got here is no longer current.
+                               // This shouldn't happen as we abort pending requests, but you never know.
+                               if ( currentRequest !== currentRequestInternal ) {
+                                       return;
+                               }
+
+                               tweakedUsername = undefined;
+
+                               userinfo = resp.query.users[0];
+
+                               if ( resp.query.users.length !== 1 ) {
+                                       // Happens if the user types '|' into the field
+                                       state = 'invalid';
+                               } else if ( userinfo.invalid !== undefined ) {
+                                       state = 'invalid';
+                               } else if ( userinfo.userid !== undefined ) {
+                                       state = 'taken';
+                               } else if ( username !== userinfo.name ) {
+                                       state = 'tweaked';
+                               } else {
+                                       state = 'ok';
+                               }
+
+                               if ( state === 'ok' ) {
+                                       cleanup();
+                               } else if ( state === 'tweaked' ) {
+                                       $warningContainer
+                                               .attr( 'class', 'warningbox' )
+                                               .text( mw.message( 'createacct-normalization', username, userinfo.name ).text() )
+                                               .slideDown();
+
+                                       tweakedUsername = userinfo.name;
+                               } else {
+                                       $warningContainer
+                                               .attr( 'class', 'errorbox' )
+                                               .empty()
+                                               .append(
+                                                       $( '<strong>' ).text( mw.message( 'createacct-error' ).text() ),
+                                                       $( '<br>' ) // Ugh
+                                               );
+
+                                       if ( state === 'invalid' ) {
+                                               $warningContainer
+                                                       .attr( 'class', 'errorbox' )
+                                                       .append( document.createTextNode( mw.message( 'noname' ).text() ) )
+                                                       .slideDown();
+                                       } else if ( state === 'taken' ) {
+                                               $warningContainer
+                                                       .attr( 'class', 'errorbox' )
+                                                       .append( document.createTextNode( mw.message( 'userexists' ).text() ) )
+                                                       .slideDown();
+                                       }
+
+                                       $warningContainer.slideDown();
+                               }
+                       } ).fail( function () {
+                               cleanup();
+                       } );
+               }
+
+               $input.on( events, $.debounce( 250, updateUsernameStatus ) );
+
+               $input.closest( 'form' ).on( 'submit', function () {
+                       // If the username has to be adjusted before it's accepted, server-side check will force the
+                       // form to be resubmitted. Let's prevent that.
+                       if ( tweakedUsername !== undefined ) {
+                               $input.val( tweakedUsername );
+                       }
+               } );
+       } );
 }( mediaWiki, jQuery ) );