Doing edits "The Right Way" is non-trivial due there being mulitple strict options that
need to be known and enabled. By default, the API encourages bad behaviour:
* Edit is unexpectedly saved as anon after session becomes invalid.
* Other edits are silently overwritten.
* Accidentally re-creates a deleted page.
* Accidentally creates a new page when an edit was intended (eg. if title was wrong).
Implement abstraction methods for edit and create that handle all this.
Thus guarding JS edits with the same protections as EditPage.
Change-Id: Ic6a35902cbae262971c704b9b8127e54733dac79
/**
* API helper to grab a csrf token.
*
- * @return {jQuery.Promise}
- * @return {Function} return.done
- * @return {string} return.done.token Received token.
+ * @return {jQuery.Promise} Received token.
*/
getEditToken: function () {
return this.getToken( 'csrf' );
},
+ /**
+ * Create a new page.
+ *
+ * Example:
+ *
+ * new mw.Api().create( 'Sandbox',
+ * { summary: 'Load sand particles.' },
+ * 'Sand.'
+ * );
+ *
+ * @since 1.28
+ * @param {mw.Title|string} title Page title
+ * @param {Object} params Edit API parameters
+ * @param {string} params.summary Edit summary
+ * @param {string} content
+ * @return {jQuery.Promise} API response
+ */
+ create: function ( title, params, content ) {
+ return this.postWithEditToken( $.extend( {
+ action: 'edit',
+ title: String( title ),
+ text: content,
+ formatversion: '2',
+
+ // Protect against errors and conflicts
+ assert: mw.user.isAnon() ? undefined : 'user',
+ createonly: true
+ }, params ) ).then( function ( data ) {
+ return data.edit;
+ } );
+ },
+
+ /**
+ * Edit an existing page.
+ *
+ * To create a new page, use #create() instead.
+ *
+ * Simple transformation:
+ *
+ * new mw.Api()
+ * .edit( 'Sandbox', function ( revision ) {
+ * return revision.content.replace( 'foo', 'bar' );
+ * } )
+ * .then( function () {
+ * console.log( 'Saved! ');
+ * } );
+ *
+ * Set save parameters by returning an object instead of a string:
+ *
+ * new mw.Api().edit(
+ * 'Sandbox',
+ * function ( revision ) {
+ * return {
+ * text: revision.content.replace( 'foo', 'bar' ),
+ * summary: 'Replace "foo" with "bar".',
+ * assert: 'bot',
+ * minor: true
+ * };
+ * }
+ * )
+ * .then( function () {
+ * console.log( 'Saved! ');
+ * } );
+ *
+ * Transform asynchronously by returning a promise.
+ *
+ * new mw.Api()
+ * .edit( 'Sandbox', function ( revision ) {
+ * return Spelling
+ * .corrections( revision.content )
+ * .then( function ( report ) {
+ * return {
+ * text: report.output,
+ * summary: report.changelog
+ * };
+ * } );
+ * } )
+ * .then( function () {
+ * console.log( 'Saved! ');
+ * } );
+ *
+ * @since 1.28
+ * @param {mw.Title|string} title Page title
+ * @param {Function} transform Callback that prepares the edit
+ * @param {Object} transform.revision Current revision
+ * @param {string} transform.revision.content Current revision content
+ * @param {string|Object|jQuery.Promise} transform.return New content, object with edit
+ * API parameters, or promise providing one of those.
+ * @return {jQuery.Promise} Edit API response
+ */
+ edit: function ( title, transform ) {
+ var basetimestamp, curtimestamp,
+ api = this;
+ return api.get( {
+ action: 'query',
+ prop: 'revisions',
+ rvprop: [ 'content', 'timestamp' ],
+ titles: String( title ),
+ formatversion: '2',
+ curtimestamp: true
+ } )
+ .then( function ( data ) {
+ var page, revision;
+ if ( !data.query || !data.query.pages ) {
+ return $.Deferred().reject( 'unknown' );
+ }
+ page = data.query.pages[ 0 ];
+ if ( !page || page.missing ) {
+ return $.Deferred().reject( 'nocreate-missing' );
+ }
+ revision = page.revisions[ 0 ];
+ basetimestamp = revision.timestamp;
+ curtimestamp = data.curtimestamp;
+ return transform( {
+ timestamp: revision.timestamp,
+ content: revision.content
+ } );
+ } )
+ .then( function ( params ) {
+ var editParams = typeof params === 'object' ? params : { text: String( params ) };
+ return api.postWithEditToken( $.extend( {
+ action: 'edit',
+ title: title,
+ formatversion: '2',
+
+ // Protect against errors and conflicts
+ assert: mw.user.isAnon() ? undefined : 'user',
+ basetimestamp: basetimestamp,
+ starttimestamp: curtimestamp,
+ nocreate: true
+ }, editParams ) );
+ } )
+ .then( function ( data ) {
+ return data.edit;
+ } );
+ },
+
/**
* Post a new section to the page.
*
'tests/qunit/suites/resources/mediawiki/mediawiki.viewport.test.js',
'tests/qunit/suites/resources/mediawiki.api/mediawiki.api.test.js',
'tests/qunit/suites/resources/mediawiki.api/mediawiki.api.category.test.js',
+ 'tests/qunit/suites/resources/mediawiki.api/mediawiki.api.edit.test.js',
'tests/qunit/suites/resources/mediawiki.api/mediawiki.api.messages.test.js',
'tests/qunit/suites/resources/mediawiki.api/mediawiki.api.options.test.js',
'tests/qunit/suites/resources/mediawiki.api/mediawiki.api.parse.test.js',
--- /dev/null
+( function ( mw, $ ) {
+ QUnit.module( 'mediawiki.api.edit', QUnit.newMwEnvironment( {
+ setup: function () {
+ this.server = this.sandbox.useFakeServer();
+ this.server.respondImmediately = true;
+ }
+ } ) );
+
+ QUnit.test( 'edit( title, transform String )', function ( assert ) {
+ this.server.respond( function ( req ) {
+ if ( /query.+titles=Sandbox/.test( req.url ) ) {
+ req.respond( 200, { 'Content-Type': 'application/json' }, JSON.stringify( {
+ curtimestamp: '2016-01-02T12:00:00Z',
+ query: {
+ pages: [ {
+ pageid: 1,
+ ns: 0,
+ title: 'Sandbox',
+ revisions: [ {
+ timestamp: '2016-01-01T12:00:00Z',
+ contentformat: 'text/x-wiki',
+ contentmodel: 'wikitext',
+ content: 'Sand.'
+ } ]
+ } ]
+ }
+ } ) );
+ }
+ if ( /edit.+basetimestamp=2016-01-01.+starttimestamp=2016-01-02.+text=Box%2E/.test( req.requestBody ) ) {
+ req.respond( 200, { 'Content-Type': 'application/json' }, JSON.stringify( {
+ edit: {
+ result: 'Success',
+ oldrevid: 11,
+ newrevid: 13,
+ newtimestamp: '2016-01-03T12:00:00Z'
+ }
+ } ) );
+ }
+ } );
+
+ return new mw.Api()
+ .edit( 'Sandbox', function ( revision ) {
+ return revision.content.replace( 'Sand', 'Box' );
+ } )
+ .then( function ( edit ) {
+ assert.equal( edit.newrevid, 13 );
+ } );
+ } );
+
+ QUnit.test( 'edit( title, transform Promise )', function ( assert ) {
+ this.server.respond( function ( req ) {
+ if ( /query.+titles=Async/.test( req.url ) ) {
+ req.respond( 200, { 'Content-Type': 'application/json' }, JSON.stringify( {
+ curtimestamp: '2016-02-02T12:00:00Z',
+ query: {
+ pages: [ {
+ pageid: 4,
+ ns: 0,
+ title: 'Async',
+ revisions: [ {
+ timestamp: '2016-02-01T12:00:00Z',
+ contentformat: 'text/x-wiki',
+ contentmodel: 'wikitext',
+ content: 'Async.'
+ } ]
+ } ]
+ }
+ } ) );
+ }
+ if ( /edit.+basetimestamp=2016-02-01.+starttimestamp=2016-02-02.+text=Promise%2E/.test( req.requestBody ) ) {
+ req.respond( 200, { 'Content-Type': 'application/json' }, JSON.stringify( {
+ edit: {
+ result: 'Success',
+ oldrevid: 21,
+ newrevid: 23,
+ newtimestamp: '2016-02-03T12:00:00Z'
+ }
+ } ) );
+ }
+ } );
+
+ return new mw.Api()
+ .edit( 'Async', function ( revision ) {
+ return $.Deferred().resolve( revision.content.replace( 'Async', 'Promise' ) );
+ } )
+ .then( function ( edit ) {
+ assert.equal( edit.newrevid, 23 );
+ } );
+ } );
+
+ QUnit.test( 'edit( title, transform Object )', function ( assert ) {
+ this.server.respond( function ( req ) {
+ if ( /query.+titles=Param/.test( req.url ) ) {
+ req.respond( 200, { 'Content-Type': 'application/json' }, JSON.stringify( {
+ curtimestamp: '2016-03-02T12:00:00Z',
+ query: {
+ pages: [ {
+ pageid: 3,
+ ns: 0,
+ title: 'Param',
+ revisions: [ {
+ timestamp: '2016-03-01T12:00:00Z',
+ contentformat: 'text/x-wiki',
+ contentmodel: 'wikitext',
+ content: '...'
+ } ]
+ } ]
+ }
+ } ) );
+ }
+ if ( /edit.+basetimestamp=2016-03-01.+starttimestamp=2016-03-02.+text=Content&summary=Sum/.test( req.requestBody ) ) {
+ req.respond( 200, { 'Content-Type': 'application/json' }, JSON.stringify( {
+ edit: {
+ result: 'Success',
+ oldrevid: 31,
+ newrevid: 33,
+ newtimestamp: '2016-03-03T12:00:00Z'
+ }
+ } ) );
+ }
+ } );
+
+ return new mw.Api()
+ .edit( 'Param', function () {
+ return { text: 'Content', summary: 'Sum' };
+ } )
+ .then( function ( edit ) {
+ assert.equal( edit.newrevid, 33 );
+ } );
+ } );
+
+ QUnit.test( 'create( title, content )', function ( assert ) {
+ this.server.respond( function ( req ) {
+ if ( /edit.+text=Sand/.test( req.requestBody ) ) {
+ req.respond( 200, { 'Content-Type': 'application/json' }, JSON.stringify( {
+ edit: {
+ 'new': true,
+ result: 'Success',
+ newrevid: 41,
+ newtimestamp: '2016-04-01T12:00:00Z'
+ }
+ } ) );
+ }
+ } );
+
+ return new mw.Api()
+ .create( 'Sandbox', { summary: 'Load sand particles.' }, 'Sand.' )
+ .then( function ( page ) {
+ assert.equal( page.newrevid, 41 );
+ } );
+ } );
+
+}( mediaWiki, jQuery ) );