Use built-in mw.Api 'badtoken' handling, also 'assert'/'assertuser'

When the user is saving their edit, we want to ensure that they
understand how it will be attributed. If the user gets logged out or
logs in in another tab, we want to display a message about it before
saving.

Instead of manually managing tokens and handling the 'badtoken' error
to detect this, use the 'assert'/'assertuser' parameters for the API
to detect it for us. Thanks to this we can rely on automatic retrying
for 'badtoken' errors in mw.Api#postWithToken.

It will be possible to share some of this code with other extensions
that already use ArticleTargetSaver, namely DiscussionTools, now that
it doesn't need to manage tokens for VisualEditor.

Bug: T245327
Depends-On: I485f99e1f5f493262b0c9af22370da01adf1e09c
Change-Id: I37f8e89b6d92c419d1b6569891612256342f8139
This commit is contained in:
Bartosz Dziewoński 2020-02-15 02:22:39 +01:00
parent 6f7ef2fd76
commit 58757d4e3e
3 changed files with 22 additions and 47 deletions

View file

@ -155,7 +155,7 @@ ve.ui.MWExportWikitextDialog.prototype.export = function () {
format: 'text/x-wiki',
model: 'wikitext',
wpTextbox1: wikitext,
wpEditToken: ve.init.target.editToken,
wpEditToken: mw.user.tokens.get( 'csrfToken' ),
// MediaWiki function-verification parameters, mostly relevant to the
// classic editpage, but still required here:
wpUnicodeCheck: 'ℳ𝒲♥𝓊𝓃𝒾𝒸ℴ𝒹ℯ',

View file

@ -694,23 +694,13 @@ ve.init.mw.ArticleTarget.prototype.saveFail = function ( doc, saveData, wasRetry
for ( i = 0; i < data.errors.length; i++ ) {
error = data.errors[ i ];
// Handle token errors
if ( error.code === 'badtoken' ) {
if ( wasRetry ) {
this.saveErrorBadToken( null, true );
return;
}
this.refreshEditToken().done( function ( userChanged ) {
// target.editToken has been refreshed
if ( userChanged ) {
target.saveErrorBadToken( mw.user.isAnon() ? null : mw.user.getName(), false );
} else {
// New session is the same user still; retry
target.emit( 'saveErrorBadToken', true );
target.save( doc, saveData, true );
}
} ).fail( function () {
target.saveErrorBadToken( null, true );
this.saveErrorBadTokenOrNewUser( null, true );
} else if ( error.code === 'assertanonfailed' || error.code === 'assertuserfailed' || error.code === 'assertnameduserfailed' ) {
this.refreshUser().then( function ( username ) {
target.saveErrorBadTokenOrNewUser( username, false );
}, function () {
target.saveErrorUnknown( data );
} );
return;
} else if ( error.code === 'editconflict' ) {
@ -794,14 +784,14 @@ ve.init.mw.ArticleTarget.prototype.saveErrorHookAborted = function ( data ) {
};
/**
* Handle token fetch indicating another user is logged in, and token fetch errors.
* Handle assert error indicating another user is logged in, and token fetch errors.
*
* @param {string|null} username Name of newly logged-in user, or null if anonymous
* @param {boolean} [error=false] Whether there was an error trying to figure out who we're logged in as
* @param {boolean} [error=false] Whether this is a token fetch error
* @fires saveErrorBadToken
* @fires saveErrorNewUser
*/
ve.init.mw.ArticleTarget.prototype.saveErrorBadToken = function ( username, error ) {
ve.init.mw.ArticleTarget.prototype.saveErrorBadTokenOrNewUser = function ( username, error ) {
var $msg = $( document.createTextNode( mw.msg( 'visualeditor-savedialog-error-badtoken' ) + ' ' ) );
if ( error ) {
@ -1480,8 +1470,8 @@ ve.init.mw.ArticleTarget.prototype.save = function ( doc, options, isRetry ) {
basetimestamp: this.baseTimeStamp,
starttimestamp: this.startTimeStamp,
etag: this.etag,
// Pass in token to prevent automatic badtoken retries
token: this.editToken
assert: mw.user.isAnon() ? 'anon' : 'user',
assertuser: mw.user.getName() || undefined
} );
if ( mw.config.get( 'wgVisualEditorConfig' ).useChangeTagging && !data.vetags ) {
@ -1588,7 +1578,7 @@ ve.init.mw.ArticleTarget.prototype.submit = function ( wikitext, fields ) {
wpStarttime: this.startTimeStamp,
wpEdittime: this.baseTimeStamp,
wpTextbox1: wikitext,
wpEditToken: this.editToken,
wpEditToken: mw.user.tokens.get( 'csrfToken' ),
// MediaWiki function-verification parameters, mostly relevant to the
// classic editpage, but still required here:
wpUnicodeCheck: 'ℳ𝒲♥𝓊𝓃𝒾𝒸ℴ𝒹ℯ',

View file

@ -20,7 +20,6 @@ ve.init.mw.Target = function VeInitMwTarget( config ) {
this.active = false;
this.pageName = mw.config.get( 'wgRelevantPageName' );
this.editToken = mw.user.tokens.get( 'csrfToken' );
this.recovered = false;
this.fromEditedState = false;
this.originalHtml = null;
@ -459,26 +458,21 @@ ve.init.mw.Target.prototype.teardown = function () {
* the current user.
*
* @param {ve.dm.Document} [doc] Document to associate with the API request
* @return {jQuery.Promise} Promise resolved with whether we switched users
* @return {jQuery.Promise} Promise resolved with new username, or null if anonymous
*/
ve.init.mw.Target.prototype.refreshEditToken = function ( doc ) {
ve.init.mw.Target.prototype.refreshUser = function ( doc ) {
var api = this.getContentApi( doc ),
deferred = ve.createDeferred(),
target = this;
deferred = ve.createDeferred();
api.get( {
action: 'query',
meta: 'tokens|userinfo',
type: 'csrf'
meta: 'userinfo'
} )
.done( function ( data ) {
var
userInfo = data.query && data.query.userinfo,
editToken = data.query && data.query.tokens && data.query.tokens.csrftoken,
isAnon = mw.user.isAnon();
if ( userInfo && editToken ) {
target.editToken = editToken;
if ( userInfo ) {
if (
( isAnon && userInfo.anon !== undefined ) ||
// Comparing id instead of name to protect against possible
@ -486,7 +480,7 @@ ve.init.mw.Target.prototype.refreshEditToken = function ( doc ) {
mw.config.get( 'wgUserId' ) === userInfo.id
) {
// New session is the same user still
deferred.resolve( false );
deferred.resolve( mw.user.getName() );
} else {
// The now current session is a different user
if ( userInfo.anon !== undefined ) {
@ -502,7 +496,7 @@ ve.init.mw.Target.prototype.refreshEditToken = function ( doc ) {
// New session is a different user
mw.config.set( { wgUserId: userInfo.id, wgUserName: userInfo.name } );
}
deferred.resolve( true );
deferred.resolve( mw.user.getName() );
}
} else {
deferred.reject();
@ -519,15 +513,12 @@ ve.init.mw.Target.prototype.refreshEditToken = function ( doc ) {
*
* @param {ve.dm.Document} doc Document
* @param {boolean} [useRevision=true] Whether to use the revision ID + ETag
* @param {boolean} [isRetry=false] Whether this call is retrying a prior call
* @return {jQuery.Promise} Abortable promise which resolves with a wikitext string
*/
ve.init.mw.Target.prototype.getWikitextFragment = function ( doc, useRevision, isRetry ) {
ve.init.mw.Target.prototype.getWikitextFragment = function ( doc, useRevision ) {
var promise, xhr,
target = this,
params = {
action: 'visualeditoredit',
token: this.editToken,
paction: 'serialize',
html: ve.dm.converter.getDomFromModel( doc ).body.innerHTML,
page: this.getPageName()
@ -543,7 +534,7 @@ ve.init.mw.Target.prototype.getWikitextFragment = function ( doc, useRevision, i
params.etag = this.etag;
}
xhr = this.getContentApi( doc ).post(
xhr = this.getContentApi( doc ).postWithToken( 'csrf',
params,
{ contentType: 'multipart/form-data' }
);
@ -553,12 +544,6 @@ ve.init.mw.Target.prototype.getWikitextFragment = function ( doc, useRevision, i
return response.visualeditoredit.content;
}
return ve.createDeferred().reject();
}, function ( error ) {
if ( error === 'badtoken' && !isRetry ) {
return target.refreshEditToken( doc ).then( function () {
return target.getWikitextFragment( doc, useRevision, true );
} );
}
} );
promise.abort = function () {