From 58757d4e3e9a15288b180e04b9dca4770afd7a77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20Dziewo=C5=84ski?= Date: Sat, 15 Feb 2020 02:22:39 +0100 Subject: [PATCH] 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 --- .../ve.ui.MWExportWikitextDialog.js | 2 +- .../init/targets/ve.init.mw.ArticleTarget.js | 34 +++++++------------ .../ve-mw/init/targets/ve.init.mw.Target.js | 33 +++++------------- 3 files changed, 22 insertions(+), 47 deletions(-) diff --git a/modules/ve-mw-collab/ve.ui.MWExportWikitextDialog.js b/modules/ve-mw-collab/ve.ui.MWExportWikitextDialog.js index 5f29e8151d..427be8a8b5 100644 --- a/modules/ve-mw-collab/ve.ui.MWExportWikitextDialog.js +++ b/modules/ve-mw-collab/ve.ui.MWExportWikitextDialog.js @@ -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: 'ℳ𝒲β™₯π“Šπ“ƒπ’Ύπ’Έβ„΄π’Ήβ„―', diff --git a/modules/ve-mw/init/targets/ve.init.mw.ArticleTarget.js b/modules/ve-mw/init/targets/ve.init.mw.ArticleTarget.js index 24be8e6800..3929f88c5d 100644 --- a/modules/ve-mw/init/targets/ve.init.mw.ArticleTarget.js +++ b/modules/ve-mw/init/targets/ve.init.mw.ArticleTarget.js @@ -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: 'ℳ𝒲β™₯π“Šπ“ƒπ’Ύπ’Έβ„΄π’Ήβ„―', diff --git a/modules/ve-mw/init/targets/ve.init.mw.Target.js b/modules/ve-mw/init/targets/ve.init.mw.Target.js index d38739a0f7..fc55c3fd38 100644 --- a/modules/ve-mw/init/targets/ve.init.mw.Target.js +++ b/modules/ve-mw/init/targets/ve.init.mw.Target.js @@ -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 () {