Update account creation form validation
Use the cancreateerror returned from list=users&usprop=cancreate for username validation. Use the new action=validatepassword to validate entered passwords. This also injects the resulting errors in the style of HTMLForm's field validation rather than at the top of the form. Change-Id: Ie8c1270eb605367556fe36b0b2080eb3f957dc54
This commit is contained in:
parent
1e4fdfb825
commit
4d59edf138
5 changed files with 249 additions and 77 deletions
|
|
@ -1145,6 +1145,9 @@ abstract class HTMLFormField {
|
|||
* @since 1.18
|
||||
*/
|
||||
protected static function formatErrors( $errors ) {
|
||||
// Note: If you change the logic in this method, change
|
||||
// htmlform.Checker.js to match.
|
||||
|
||||
if ( is_array( $errors ) && count( $errors ) === 1 ) {
|
||||
$errors = array_shift( $errors );
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,8 @@
|
|||
"mw.plugin.*",
|
||||
"mw.cookie",
|
||||
"mw.experiments",
|
||||
"mw.viewport"
|
||||
"mw.viewport",
|
||||
"mw.htmlform.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1092,6 +1092,12 @@ return [
|
|||
],
|
||||
'targets' => [ 'desktop', 'mobile' ],
|
||||
],
|
||||
'mediawiki.htmlform.checker' => [
|
||||
'scripts' => [
|
||||
'resources/src/mediawiki/htmlform/htmlform.Checker.js',
|
||||
],
|
||||
'targets' => [ 'desktop', 'mobile' ],
|
||||
],
|
||||
'mediawiki.htmlform.ooui' => [
|
||||
'scripts' => [
|
||||
'resources/src/mediawiki/htmlform/htmlform.Element.js',
|
||||
|
|
@ -2070,7 +2076,6 @@ return [
|
|||
'mediawiki.special.userlogin.signup.js' => [
|
||||
'scripts' => 'resources/src/mediawiki.special/mediawiki.special.userlogin.signup.js',
|
||||
'messages' => [
|
||||
'createacct-error',
|
||||
'createacct-emailrequired',
|
||||
'noname',
|
||||
'userexists',
|
||||
|
|
@ -2079,6 +2084,7 @@ return [
|
|||
'mediawiki.api',
|
||||
'mediawiki.jqueryMsg',
|
||||
'jquery.throttle-debounce',
|
||||
'mediawiki.htmlform.checker',
|
||||
],
|
||||
],
|
||||
'mediawiki.special.unwatchedPages' => [
|
||||
|
|
|
|||
|
|
@ -31,29 +31,14 @@
|
|||
} );
|
||||
|
||||
// Check if the username is invalid or already taken
|
||||
$( function () {
|
||||
var
|
||||
// We need to hook to all of these events to be sure we are notified of all changes to the
|
||||
// value of an <input type=text> field.
|
||||
events = 'keyup keydown change mouseup cut paste focus blur',
|
||||
$input = $( '#wpName2' ),
|
||||
$statusContainer = $( '#mw-createacct-status-area' ),
|
||||
mw.hook( 'htmlform.enhance' ).add( function ( $root ) {
|
||||
var $usernameInput = $root.find( '#wpName2' ),
|
||||
$passwordInput = $root.find( '#wpPassword2' ),
|
||||
$emailInput = $root.find( '#wpEmail' ),
|
||||
$realNameInput = $root.find( '#wpRealName' ),
|
||||
api = new mw.Api(),
|
||||
currentRequest;
|
||||
usernameChecker, passwordChecker;
|
||||
|
||||
// Hide any present status messages.
|
||||
function clearStatus() {
|
||||
$statusContainer.slideUp( function () {
|
||||
$statusContainer
|
||||
.removeAttr( 'class' )
|
||||
.empty();
|
||||
} );
|
||||
}
|
||||
|
||||
// Returns a promise receiving a { state:, username: } object, where:
|
||||
// * 'state' is one of 'invalid', 'taken', 'ok'
|
||||
// * 'username' is the validated username if 'state' is 'ok', null otherwise (if it's not
|
||||
// possible to register such an account)
|
||||
function checkUsername( username ) {
|
||||
// We could just use .then() if we didn't have to pass on .abort()…
|
||||
var d, apiPromise;
|
||||
|
|
@ -62,20 +47,29 @@
|
|||
apiPromise = api.get( {
|
||||
action: 'query',
|
||||
list: 'users',
|
||||
ususers: username // '|' in usernames is handled below
|
||||
ususers: username,
|
||||
usprop: 'cancreate',
|
||||
formatversion: 2,
|
||||
errorformat: 'html',
|
||||
errorsuselocal: true,
|
||||
uselang: mw.config.get( 'wgUserLanguage' )
|
||||
} )
|
||||
.done( function ( resp ) {
|
||||
var userinfo = resp.query.users[ 0 ];
|
||||
|
||||
if ( resp.query.users.length !== 1 ) {
|
||||
// Happens if the user types '|' into the field
|
||||
d.resolve( { state: 'invalid', username: null } );
|
||||
} else if ( userinfo.invalid !== undefined ) {
|
||||
d.resolve( { state: 'invalid', username: null } );
|
||||
if ( resp.query.users.length !== 1 || userinfo.invalid ) {
|
||||
d.resolve( { valid: false, messages: [ mw.message( 'noname' ).parseDom() ] } );
|
||||
} else if ( userinfo.userid !== undefined ) {
|
||||
d.resolve( { state: 'taken', username: null } );
|
||||
d.resolve( { valid: false, messages: [ mw.message( 'userexists' ).parseDom() ] } );
|
||||
} else if ( !userinfo.cancreate ) {
|
||||
d.resolve( {
|
||||
valid: false,
|
||||
messages: userinfo.cancreateerror ? userinfo.cancreateerror.map( function ( m ) {
|
||||
return m.html;
|
||||
} ) : []
|
||||
} );
|
||||
} else {
|
||||
d.resolve( { state: 'ok', username: username } );
|
||||
d.resolve( { valid: true, messages: [] } );
|
||||
}
|
||||
} )
|
||||
.fail( d.reject );
|
||||
|
|
@ -83,58 +77,46 @@
|
|||
return d.promise( { abort: apiPromise.abort } );
|
||||
}
|
||||
|
||||
function updateUsernameStatus() {
|
||||
var
|
||||
username = $.trim( $input.val() ),
|
||||
currentRequestInternal;
|
||||
function checkPassword() {
|
||||
// We could just use .then() if we didn't have to pass on .abort()…
|
||||
var apiPromise,
|
||||
d = $.Deferred();
|
||||
|
||||
// Abort any pending requests.
|
||||
if ( currentRequest ) {
|
||||
currentRequest.abort();
|
||||
if ( $.trim( $usernameInput.val() ) === '' ) {
|
||||
d.resolve( { valid: true, messages: [] } );
|
||||
return d.promise();
|
||||
}
|
||||
|
||||
if ( username === '' ) {
|
||||
clearStatus();
|
||||
return;
|
||||
}
|
||||
apiPromise = api.post( {
|
||||
action: 'validatepassword',
|
||||
user: $usernameInput.val(),
|
||||
password: $passwordInput.val(),
|
||||
email: $emailInput.val() || '',
|
||||
realname: $realNameInput.val() || '',
|
||||
formatversion: 2,
|
||||
errorformat: 'html',
|
||||
errorsuselocal: true,
|
||||
uselang: mw.config.get( 'wgUserLanguage' )
|
||||
} )
|
||||
.done( function ( resp ) {
|
||||
var pwinfo = resp.validatepassword || {};
|
||||
|
||||
currentRequest = currentRequestInternal = checkUsername( username ).done( function ( info ) {
|
||||
var message;
|
||||
d.resolve( {
|
||||
valid: pwinfo.validity === 'Good',
|
||||
messages: pwinfo.validitymessages ? pwinfo.validitymessages.map( function ( m ) {
|
||||
return m.html;
|
||||
} ) : []
|
||||
} );
|
||||
} )
|
||||
.fail( d.reject );
|
||||
|
||||
// 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;
|
||||
}
|
||||
// If we're here, then the current request has finished, avoid calling .abort() needlessly.
|
||||
currentRequest = undefined;
|
||||
|
||||
if ( info.state === 'ok' ) {
|
||||
clearStatus();
|
||||
} else {
|
||||
if ( info.state === 'invalid' ) {
|
||||
message = mw.message( 'noname' ).text();
|
||||
} else if ( info.state === 'taken' ) {
|
||||
message = mw.message( 'userexists' ).text();
|
||||
}
|
||||
|
||||
$statusContainer
|
||||
.attr( 'class', 'errorbox' )
|
||||
.empty()
|
||||
.append(
|
||||
// Ugh…
|
||||
// TODO Change the HTML structure in includes/templates/Usercreate.php
|
||||
$( '<strong>' ).text( mw.message( 'createacct-error' ).text() ),
|
||||
$( '<br>' ),
|
||||
document.createTextNode( message )
|
||||
)
|
||||
.slideDown();
|
||||
}
|
||||
} ).fail( function () {
|
||||
clearStatus();
|
||||
} );
|
||||
return d.promise( { abort: apiPromise.abort } );
|
||||
}
|
||||
|
||||
$input.on( events, $.debounce( 1000, updateUsernameStatus ) );
|
||||
usernameChecker = new mw.htmlform.Checker( $usernameInput, checkUsername );
|
||||
usernameChecker.attach();
|
||||
|
||||
passwordChecker = new mw.htmlform.Checker( $passwordInput, checkPassword );
|
||||
passwordChecker.attach( $usernameInput.add( $emailInput ).add( $realNameInput ) );
|
||||
} );
|
||||
}( mediaWiki, jQuery ) );
|
||||
|
|
|
|||
180
resources/src/mediawiki/htmlform/htmlform.Checker.js
Normal file
180
resources/src/mediawiki/htmlform/htmlform.Checker.js
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
( function ( mw, $ ) {
|
||||
|
||||
mw.htmlform = {};
|
||||
|
||||
/**
|
||||
* @class mw.htmlform.Checker
|
||||
*/
|
||||
|
||||
/**
|
||||
* A helper class to add validation to non-OOUI HtmlForm fields.
|
||||
*
|
||||
* @constructor
|
||||
* @param {jQuery} $element Form field generated by HTMLForm
|
||||
* @param {Function} validator Validation callback
|
||||
* @param {string} validator.value Value of the form field to be validated
|
||||
* @param {jQuery.Promise} validator.return The promise should be resolved
|
||||
* with an object with two properties: Boolean 'valid' to indicate success
|
||||
* or failure of validation, and an array 'messages' to be passed to
|
||||
* setErrors() on failure.
|
||||
*/
|
||||
mw.htmlform.Checker = function ( $element, validator ) {
|
||||
this.validator = validator;
|
||||
this.$element = $element;
|
||||
|
||||
this.$errorBox = $element.next( '.error' );
|
||||
if ( !this.$errorBox.length ) {
|
||||
this.$errorBox = $( '<span>' );
|
||||
this.$errorBox.hide();
|
||||
$element.after( this.$errorBox );
|
||||
}
|
||||
|
||||
this.currentValue = this.$element.val();
|
||||
};
|
||||
|
||||
/**
|
||||
* Attach validation events to the form element
|
||||
*
|
||||
* @param {jQuery} [$extraElements] Additional elements to listen for change
|
||||
* events on.
|
||||
* @return {mw.htmlform.Checker}
|
||||
* @chainable
|
||||
*/
|
||||
mw.htmlform.Checker.prototype.attach = function ( $extraElements ) {
|
||||
var $e,
|
||||
// We need to hook to all of these events to be sure we are
|
||||
// notified of all changes to the value of an <input type=text>
|
||||
// field.
|
||||
events = 'keyup keydown change mouseup cut paste focus blur';
|
||||
|
||||
$e = this.$element;
|
||||
if ( $extraElements ) {
|
||||
$e = $e.add( $extraElements );
|
||||
}
|
||||
$e.on( events, $.debounce( 1000, this.validate.bind( this ) ) );
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate the form element
|
||||
* @return {jQuery.Promise}
|
||||
*/
|
||||
mw.htmlform.Checker.prototype.validate = function () {
|
||||
var currentRequestInternal,
|
||||
that = this,
|
||||
value = this.$element.val();
|
||||
|
||||
// Abort any pending requests.
|
||||
if ( this.currentRequest && this.currentRequest.abort ) {
|
||||
this.currentRequest.abort();
|
||||
}
|
||||
|
||||
if ( value === '' ) {
|
||||
this.currentValue = value;
|
||||
this.setErrors( [] );
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentRequest = currentRequestInternal = this.validator( value )
|
||||
.done( function ( info ) {
|
||||
var forceReplacement = value !== that.currentValue;
|
||||
|
||||
// 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 ( that.currentRequest !== currentRequestInternal ) {
|
||||
return;
|
||||
}
|
||||
// If we're here, then the current request has finished, avoid calling .abort() needlessly.
|
||||
that.currentRequest = undefined;
|
||||
|
||||
that.currentValue = value;
|
||||
|
||||
if ( info.valid ) {
|
||||
that.setErrors( [], forceReplacement );
|
||||
} else {
|
||||
that.setErrors( info.messages, forceReplacement );
|
||||
}
|
||||
} ).fail( function () {
|
||||
that.currentValue = null;
|
||||
that.setErrors( [] );
|
||||
} );
|
||||
|
||||
return currentRequestInternal;
|
||||
};
|
||||
|
||||
/**
|
||||
* Display errors associated with the form element
|
||||
* @param {Array} errors Error messages. Each error message will be appended to a
|
||||
* `<span>` or `<li>`, as with jQuery.append().
|
||||
* @param {boolean} [forceReplacement] Set true to force a visual replacement even
|
||||
* if the errors are the same. Ignored if errors are empty.
|
||||
* @return {mw.htmlform.Checker}
|
||||
* @chainable
|
||||
*/
|
||||
mw.htmlform.Checker.prototype.setErrors = function ( errors, forceReplacement ) {
|
||||
var $oldErrorBox, tagName, showFunc, text, replace,
|
||||
$errorBox = this.$errorBox;
|
||||
|
||||
if ( errors.length === 0 ) {
|
||||
$errorBox.slideUp( function () {
|
||||
$errorBox
|
||||
.removeAttr( 'class' )
|
||||
.empty();
|
||||
} );
|
||||
} else {
|
||||
// Match behavior of HTMLFormField::formatErrors(), <span> or <ul>
|
||||
// depending on the count.
|
||||
tagName = errors.length === 1 ? 'span' : 'ul';
|
||||
|
||||
// We have to animate the replacement if we're changing the tag. We
|
||||
// also want to if told to by the caller (i.e. to make it visually
|
||||
// obvious that the changed field value gives the same error) or if
|
||||
// the error text changes (because it makes more sense than
|
||||
// changing the text with no animation).
|
||||
replace = (
|
||||
forceReplacement || $errorBox.length > 1 ||
|
||||
$errorBox[ 0 ].tagName.toLowerCase() !== tagName
|
||||
);
|
||||
if ( !replace ) {
|
||||
text = $( '<' + tagName + '>' )
|
||||
.append( errors.map( function ( e ) {
|
||||
return errors.length === 1 ? e : $( '<li>' ).append( e );
|
||||
} ) );
|
||||
if ( text.text() !== $errorBox.text() ) {
|
||||
replace = true;
|
||||
}
|
||||
}
|
||||
|
||||
$oldErrorBox = $errorBox;
|
||||
if ( replace ) {
|
||||
this.$errorBox = $errorBox = $( '<' + tagName + '>' );
|
||||
$errorBox.hide();
|
||||
$oldErrorBox.after( this.$errorBox );
|
||||
}
|
||||
|
||||
showFunc = function () {
|
||||
if ( $oldErrorBox !== $errorBox ) {
|
||||
$oldErrorBox
|
||||
.removeAttr( 'class' )
|
||||
.detach();
|
||||
}
|
||||
$errorBox
|
||||
.attr( 'class', 'error' )
|
||||
.empty()
|
||||
.append( errors.map( function ( e ) {
|
||||
return errors.length === 1 ? e : $( '<li>' ).append( e );
|
||||
} ) )
|
||||
.slideDown();
|
||||
};
|
||||
if ( $oldErrorBox !== $errorBox && $oldErrorBox.hasClass( 'error' ) ) {
|
||||
$oldErrorBox.slideUp( showFunc );
|
||||
} else {
|
||||
showFunc();
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
}( mediaWiki, jQuery ) );
|
||||
Loading…
Reference in a new issue