src/mobile.editor.overlay/EditorGateway.js
const util = require( '../mobile.startup/util' ),
actionParams = require( '../mobile.startup/actionParams' );
/**
* API that helps save and retrieve page content
*
* @param {Object} options Configuration options
* @param {mw.Api} options.api an Api to use.
* @param {string} options.title the title to edit
* @param {string|null} options.sectionId the id of the section to operate edits on.
* @param {number} [options.oldId] revision to operate on. If absent defaults to latest.
* @param {boolean} [options.fromModified] whether the page was loaded in a modified state
* @param {string} [options.preload] the name of a page to preload into the editor
* @param {Array} [options.preloadparams] parameters to prefill into the preload content
* @param {string} [options.editintro] edit intro to add to notices
* @private
*/
function EditorGateway( options ) {
this.api = options.api;
this.title = options.title;
this.sectionId = options.sectionId;
this.oldId = options.oldId;
this.preload = options.preload;
this.preloadparams = options.preloadparams;
this.editintro = options.editintro;
this.content = undefined;
this.fromModified = options.fromModified;
this.hasChanged = options.fromModified;
}
EditorGateway.prototype = {
/**
* Get the block (if there is one) from the result.
*
* @memberof EditorGateway
* @param {Object} pageObj Page object
* @return {Object|null}
*/
getBlockInfo: function ( pageObj ) {
let blockedError;
if ( pageObj.actions &&
pageObj.actions.edit &&
Array.isArray( pageObj.actions.edit )
) {
pageObj.actions.edit.some( function ( error ) {
if ( [ 'blocked', 'autoblocked' ].indexOf( error.code ) !== -1 ) {
blockedError = error;
return true;
}
return false;
} );
if ( blockedError && blockedError.data && blockedError.data.blockinfo ) {
return blockedError.data.blockinfo;
}
}
return null;
},
/**
* Get the content of a page.
*
* @memberof EditorGateway
* @instance
* @return {jQuery.Promise}
*/
getContent: function () {
let options;
const self = this;
function resolve() {
return util.Deferred().resolve( {
text: self.content || '',
blockinfo: self.blockinfo,
notices: self.notices
} );
}
if ( this.content !== undefined ) {
return resolve();
} else {
options = actionParams( {
prop: [ 'revisions', 'info' ],
rvprop: [ 'content', 'timestamp' ],
inprop: [ 'preloadcontent', 'editintro' ],
inpreloadcustom: self.preload,
inpreloadparams: self.preloadparams,
ineditintrocustom: self.editintro,
titles: self.title,
// get block information for this user
intestactions: 'edit',
// …and test whether this edit will auto-create an account
intestactionsautocreate: true,
intestactionsdetail: 'full'
} );
// Load text of old revision if desired
if ( this.oldId ) {
options.rvstartid = this.oldId;
}
// See T52136 - passing rvsection will fail with non wikitext
if ( this.sectionId ) {
options.rvsection = this.sectionId;
}
return this.api.get( options ).then( function ( resp ) {
if ( resp.error ) {
return util.Deferred().reject( resp.error.code );
}
const pageObj = resp.query.pages[0];
// page might not exist and caller might not have known.
if ( pageObj.missing !== undefined ) {
if ( pageObj.preloadcontent ) {
self.content = pageObj.preloadcontent.content;
self.hasChanged = !pageObj.preloadisdefault;
} else {
self.content = '';
}
} else {
const revision = pageObj.revisions[0];
self.content = revision.content;
self.timestamp = revision.timestamp;
}
// save content a second time to be able to check for changes
self.originalContent = self.content;
self.blockinfo = self.getBlockInfo( pageObj );
self.wouldautocreate = pageObj.wouldautocreate && pageObj.wouldautocreate.edit;
self.notices = pageObj.editintro;
return resolve();
} );
}
},
/**
* Mark content as modified and set changes to be submitted when #save
* is invoked.
*
* @memberof EditorGateway
* @instance
* @param {string} content New section content.
*/
setContent: function ( content ) {
if ( this.originalContent !== content || this.fromModified ) {
this.hasChanged = true;
} else {
this.hasChanged = false;
}
this.content = content;
},
/**
* Save the new content of the section, previously set using #setContent.
*
* @memberof EditorGateway
* @instance
* @param {Object} options Configuration options
* @param {string} [options.summary] Optional summary for the edit.
* @param {string} [options.captchaId] If CAPTCHA was requested, ID of the
* captcha.
* @param {string} [options.captchaWord] If CAPTCHA was requested, term
* displayed in the CAPTCHA.
* @return {jQuery.Deferred} On failure callback is passed an object with
* `type` and `details` properties. `type` is a string describing the type
* of error, `details` can be any object (usually error message).
*/
save: function ( options ) {
const self = this,
result = util.Deferred();
options = options || {};
/**
* Save content. Make an API request.
*
* @return {jQuery.Deferred}
*/
function saveContent() {
const apiOptions = {
action: 'edit',
errorformat: 'html',
errorlang: mw.config.get( 'wgUserLanguage' ),
errorsuselocal: 1,
formatversion: 2,
title: self.title,
summary: options.summary,
captchaid: options.captchaId,
captchaword: options.captchaWord,
basetimestamp: self.timestamp,
starttimestamp: self.timestamp
};
if ( self.content !== undefined ) {
apiOptions.text = self.content;
}
if ( self.sectionId ) {
apiOptions.section = self.sectionId;
}
// TODO: When `wouldautocreate` is true, we should also set up:
// - apiOptions.returntofragment to be the URL fragment to link to the section
// (but we don't know what it is; `sectionId` here is the number)
// - apiOptions.returntoquery to be 'redirect=no' if we're saving a redirect
// (but we have can't figure that out, unless we parse the wikitext)
self.api.postWithToken( 'csrf', apiOptions ).then( function ( data ) {
if ( data && data.edit && data.edit.result === 'Success' ) {
self.hasChanged = false;
result.resolve( data.edit.newrevid, data.edit.tempusercreatedredirect,
data.edit.tempusercreated );
} else {
result.reject( data );
}
}, function ( code, data ) {
result.reject( data );
} );
return result;
}
return saveContent();
},
/**
* Abort any pending previews.
*
* @memberof EditorGateway
* @instance
*/
abortPreview: function () {
if ( this._pending ) {
this._pending.abort();
}
},
/**
* Get page preview from the API and abort any existing previews.
*
* @memberof EditorGateway
* @instance
* @param {Object} options API query parameters
* @return {jQuery.Deferred}
*/
getPreview: function ( options ) {
let
sectionLine = '',
sectionId = '';
const self = this;
util.extend( options, {
action: 'parse',
// Enable section preview mode to avoid errors (T51218)
sectionpreview: true,
// Hide section edit links
disableeditsection: true,
// needed for pre-save transform to work (T55692)
pst: true,
// Output mobile HTML (T56243)
mobileformat: true,
useskin: mw.config.get( 'skin' ),
disabletoc: true,
title: this.title,
prop: [ 'text', 'sections' ]
} );
this.abortPreview();
// Acquire a temporary user username before previewing, so that signatures and
// user-related magic words display the temp user instead of IP user in the
// preview. (T331397)
const promise = mw.user.acquireTempUserName().then( function () {
self._pending = self.api.post( options );
return self._pending;
} );
return promise.then( function ( resp ) {
if ( resp && resp.parse && resp.parse.text ) {
// When editing section 0 or the whole page, there is no section name, so skip
if ( self.sectionId && self.sectionId !== '0' &&
resp.parse.sections !== undefined &&
resp.parse.sections[0] !== undefined
) {
if ( resp.parse.sections[0].anchor !== undefined ) {
sectionId = resp.parse.sections[0].anchor;
}
if ( resp.parse.sections[0].line !== undefined ) {
sectionLine = resp.parse.sections[0].line;
}
}
return {
text: resp.parse.text['*'],
id: sectionId,
line: sectionLine
};
} else {
return util.Deferred().reject();
}
} ).promise( {
abort: function () {
self._pending.abort();
}
} );
}
};
module.exports = EditorGateway;