From: Timo Tijhof Date: Mon, 27 Jun 2016 16:08:34 +0000 (+0100) Subject: mediawiki.api.edit: Add edit() and create() methods X-Git-Tag: 1.31.0-rc.0~6515^2 X-Git-Url: http://git.heureux-cyclage.org/?a=commitdiff_plain;h=af9981495e6621343f2a90edd539d39d1cbd13a5;p=lhc%2Fweb%2Fwiklou.git mediawiki.api.edit: Add edit() and create() methods 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 --- diff --git a/resources/src/mediawiki/api/edit.js b/resources/src/mediawiki/api/edit.js index 60276cd0bc..bb3a913f06 100644 --- a/resources/src/mediawiki/api/edit.js +++ b/resources/src/mediawiki/api/edit.js @@ -21,14 +21,149 @@ /** * 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. * diff --git a/tests/qunit/QUnitTestResources.php b/tests/qunit/QUnitTestResources.php index a2d76e01b0..95f28c85b9 100644 --- a/tests/qunit/QUnitTestResources.php +++ b/tests/qunit/QUnitTestResources.php @@ -84,6 +84,7 @@ return [ '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', diff --git a/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.edit.test.js b/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.edit.test.js new file mode 100644 index 0000000000..f83f66cc34 --- /dev/null +++ b/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.edit.test.js @@ -0,0 +1,153 @@ +( 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 ) );