';
} )
);
this.loading = false;
this.edited = this.fromEditedState;
// Parent method
ve.init.mw.ArticleTarget.super.prototype.documentReady.apply( this, arguments );
};
/**
* @inheritdoc
*/
ve.init.mw.ArticleTarget.prototype.surfaceReady = function () {
var name, i, triggers,
accessKeyPrefix = $.fn.updateTooltipAccessKeys.getAccessKeyPrefix().replace( /-/g, '+' ),
accessKeyModifiers = new ve.ui.Trigger( accessKeyPrefix + '-' ).modifiers;
// loadSuccess() may have called setAssumeExistence( true );
ve.init.platform.linkCache.setAssumeExistence( false );
this.getSurface().getModel().connect( this, {
history: 'updateToolbarSaveButtonState'
} );
this.restoreEditSection();
// Iterate over the trigger registry and resolve any access key conflicts
for ( name in ve.ui.triggerRegistry.registry ) {
triggers = ve.ui.triggerRegistry.registry[ name ];
for ( i = 0; i < triggers.length; i++ ) {
if ( ve.compare( triggers[ i ].modifiers, accessKeyModifiers ) ) {
this.disableAccessKey( triggers[ i ].primary );
}
}
}
// Parent method
ve.init.mw.ArticleTarget.super.prototype.surfaceReady.apply( this, arguments );
};
/**
* Disable an access key by removing the attribute from any element containing it
*
* @param {string} key Access key
*/
ve.init.mw.ArticleTarget.prototype.disableAccessKey = function ( key ) {
$( '[accesskey=' + key + ']' ).each( function () {
var $this = $( this );
$this
.attr( 'data-old-accesskey', $this.attr( 'accesskey' ) )
.removeAttr( 'accesskey' );
} );
};
/**
* Re-enable all access keys
*/
ve.init.mw.ArticleTarget.prototype.restoreAccessKeys = function () {
$( '[data-old-accesskey]' ).each( function () {
var $this = $( this );
$this
.attr( 'accesskey', $this.attr( 'data-old-accesskey' ) )
.removeAttr( 'data-old-accesskey' );
} );
};
/**
* Handle an unsuccessful load request.
*
* This method is called within the context of a target instance.
*
* @method
* @param {string} code Error type text from mw.Api
* @param {Object|string} errorDetails Either an object containing xhr, textStatus and exception keys, or a string.
* @fires loadError
*/
ve.init.mw.ArticleTarget.prototype.loadFail = function () {
this.loading = false;
this.emit( 'loadError' );
};
/**
* Handle a successful save request.
*
* This method is called within the context of a target instance.
*
* @method
* @param {HTMLDocument} doc HTML document we tried to save
* @param {Object} saveData Options that were used
* @param {Object} response Response data
* @param {string} status Text status message
*/
ve.init.mw.ArticleTarget.prototype.saveSuccess = function ( doc, saveData, response ) {
var data = response.visualeditoredit;
this.saving = false;
if ( !data ) {
this.saveFail( doc, saveData, false, null, 'Invalid response from server', response );
} else if ( data.result !== 'success' ) {
// Note, this could be any of db failure, hookabort, badtoken or even a captcha
this.saveFail( doc, saveData, false, null, 'Save failure', response );
} else if ( typeof data.content !== 'string' ) {
this.saveFail( doc, saveData, false, null, 'Invalid HTML content in response from server', response );
} else {
this.saveComplete(
data.content,
data.categorieshtml,
data.newrevid,
data.isRedirect,
data.displayTitleHtml,
data.lastModified,
data.contentSub,
data.modules,
data.jsconfigvars
);
}
};
/**
* Handle successful DOM save event.
*
* @method
* @param {string} html Rendered page HTML from server
* @param {string} categoriesHtml Rendered categories HTML from server
* @param {number} newid New revision id, undefined if unchanged
* @param {boolean} isRedirect Whether this page is a redirect or not
* @param {string} displayTitle What HTML to show as the page title
* @param {Object} lastModified Object containing user-formatted date
* and time strings, or undefined if we made no change.
* @param {string} contentSub HTML to show as the content subtitle
* @param {Array} modules The modules to be loaded on the page
* @param {Object} jsconfigvars The mw.config values needed on the page
* @fires save
*/
ve.init.mw.ArticleTarget.prototype.saveComplete = function () {
this.editSummaryValue = null;
this.initialEditSummary = null;
this.saveDeferred.resolve();
this.emit( 'save' );
};
/**
* Handle an unsuccessful save request.
*
* @method
* @param {HTMLDocument} doc HTML document we tried to save
* @param {Object} saveData Options that were used
* @param {boolean} wasRetry Whether this was a retry after a 'badtoken' error
* @param {Object} jqXHR
* @param {string} status Text status message
* @param {Object|null} data API response data
*/
ve.init.mw.ArticleTarget.prototype.saveFail = function ( doc, saveData, wasRetry, jqXHR, status, data ) {
var editApi,
target = this;
this.saving = false;
this.pageDeletedWarning = false;
// Handle empty response
if ( !data ) {
this.saveErrorEmpty();
return;
}
editApi = data && data.visualeditoredit && data.visualeditoredit.edit;
// Handle spam blacklist error (either from core or from Extension:SpamBlacklist)
if ( editApi && editApi.spamblacklist ) {
this.saveErrorSpamBlacklist( editApi );
return;
}
// Handle warnings/errors from Extension:AbuseFilter
// TODO: Move this to a plugin
if ( editApi && editApi.info && editApi.info.indexOf( 'Hit AbuseFilter:' ) === 0 && editApi.warning ) {
this.saveErrorAbuseFilter( editApi );
return;
}
// Handle token errors
if ( data.error && data.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 );
} );
return;
} else if ( data.error && data.error.code === 'editconflict' ) {
this.editConflict();
return;
} else if ( data.error && data.error.code === 'pagedeleted' ) {
this.saveErrorPageDeleted();
return;
} else if ( data.error && data.error.code === 'titleblacklist-forbidden' ) {
this.saveErrorTitleBlacklist();
return;
} else if ( data.error && data.error.code === 'hookaborted' ) {
this.saveErrorHookAborted();
return;
} else if ( data.error && data.error.code === 'readonly' ) {
this.saveErrorReadOnly();
return;
}
// Handle captcha
// Captcha "errors" usually aren't errors. We simply don't know about them ahead of time,
// so we save once, then (if required) we get an error with a captcha back and try again after
// the user solved the captcha.
// TODO: ConfirmEdit API is horrible, there is no reliable way to know whether it is a "math",
// "question" or "fancy" type of captcha. They all expose differently named properties in the
// API for different things in the UI. At this point we only support the SimpleCaptcha and FancyCaptcha
// which we very intuitively detect by the presence of a "url" property.
if ( editApi && editApi.captcha && (
editApi.captcha.url ||
editApi.captcha.type === 'simple' ||
editApi.captcha.type === 'math' ||
editApi.captcha.type === 'question'
) ) {
this.saveErrorCaptcha( editApi );
return;
}
// Handle (other) unknown and/or unrecoverable errors
this.saveErrorUnknown( editApi, data );
};
/**
* Show an save process error message
*
* @method
* @param {string|jQuery|Node[]} msg Message content (string of HTML, jQuery object or array of
* Node objects)
* @param {boolean} [allowReapply=true] Whether or not to allow the user to reapply.
* Reset when swapping panels. Assumed to be true unless explicitly set to false.
* @param {boolean} [warning=false] Whether or not this is a warning.
*/
ve.init.mw.ArticleTarget.prototype.showSaveError = function ( msg, allowReapply, warning ) {
this.saveDeferred.reject( [ new OO.ui.Error( msg, { recoverable: allowReapply, warning: warning } ) ] );
};
/**
* Handle general save error
*
* @method
* @fires saveErrorEmpty
*/
ve.init.mw.ArticleTarget.prototype.saveErrorEmpty = function () {
this.showSaveError( ve.msg( 'visualeditor-saveerror', 'Empty server response' ), false /* prevents reapply */ );
this.emit( 'saveErrorEmpty' );
};
/**
* Handle spam blacklist error
*
* @method
* @param {Object} editApi
* @fires saveErrorSpamBlacklist
*/
ve.init.mw.ArticleTarget.prototype.saveErrorSpamBlacklist = function ( editApi ) {
this.showSaveError(
$( $.parseHTML( editApi.sberrorparsed ) ),
false // prevents reapply
);
this.emit( 'saveErrorSpamBlacklist' );
};
/**
* Handel abuse filter error
*
* @method
* @param {Object} editApi
* @fires saveErrorAbuseFilter
*/
ve.init.mw.ArticleTarget.prototype.saveErrorAbuseFilter = function ( editApi ) {
this.showSaveError( $( $.parseHTML( editApi.warning ) ) );
// Don't disable the save button. If the action is not disallowed the user may save the
// edit by pressing Save again. The AbuseFilter API currently has no way to distinguish
// between filter triggers that are and aren't disallowing the action.
this.emit( 'saveErrorAbuseFilter' );
};
/**
* Handle title blacklist save error
*
* @method
* @fires saveErrorTitleBlacklist
*/
ve.init.mw.ArticleTarget.prototype.saveErrorTitleBlacklist = function () {
this.showSaveError( mw.msg( 'visualeditor-saveerror-titleblacklist' ) );
this.emit( 'saveErrorTitleBlacklist' );
};
/**
* Handle hook abort save error
*
* @method
* @fires saveErrorHookAborted
*/
ve.init.mw.ArticleTarget.prototype.saveErrorHookAborted = function () {
this.showSaveError( mw.msg( 'visualeditor-saveerror-hookaborted' ) );
this.emit( 'saveErrorHookAborted' );
};
/**
* Handle token fetch indicating another user is logged in, and token fetch errors.
*
* @method
* @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
* @fires saveErrorBadToken
* @fires saveErrorNewUser
*/
ve.init.mw.ArticleTarget.prototype.saveErrorBadToken = function ( username, error ) {
var userMsg,
$msg = $( document.createTextNode( mw.msg( 'visualeditor-savedialog-error-badtoken' ) + ' ' ) );
if ( error ) {
this.emit( 'saveErrorBadToken', false );
$msg = $msg.add( document.createTextNode( mw.msg( 'visualeditor-savedialog-identify-trylogin' ) ) );
} else {
this.emit( 'saveErrorNewUser' );
if ( username === null ) {
userMsg = 'visualeditor-savedialog-identify-anon';
} else {
userMsg = 'visualeditor-savedialog-identify-user';
}
$msg = $msg.add( mw.message( userMsg, username ).parseDom() );
}
this.showSaveError( $msg );
};
/**
* Handle unknown save error
*
* @method
* @param {Object} editApi
* @param {Object|null} data API response data
* @fires saveErrorUnknown
*/
ve.init.mw.ArticleTarget.prototype.saveErrorUnknown = function ( editApi, data ) {
var errorMsg = ( editApi && editApi.info ) || ( data && data.error && data.error.info ),
errorCode = ( editApi && editApi.code ) || ( data && data.error && data.error.code ),
unknown = 'Unknown error';
if ( data.xhr && data.xhr.status !== 200 ) {
unknown += ', HTTP status ' + data.xhr.status;
}
this.showSaveError(
$( document.createTextNode( errorMsg || errorCode || unknown ) ),
false // prevents reapply
);
this.emit( 'saveErrorUnknown', errorCode || errorMsg || unknown );
};
/**
* Handle captcha error
*
* @method
* @param {Object} editApi
* @fires saveErrorCaptcha
*/
ve.init.mw.ArticleTarget.prototype.saveErrorCaptcha = function ( editApi ) {
var $captchaImg, msg, question,
captchaData = editApi.captcha,
captchaInput = new OO.ui.TextInputWidget( { classes: [ 've-ui-saveDialog-captchaInput' ] } ),
$captchaDiv = $( '