From: Mark Holmquist Date: Wed, 24 Jun 2015 19:00:19 +0000 (-0500) Subject: Add frontend API for uploading via iframe X-Git-Tag: 1.31.0-rc.0~10791^2 X-Git-Url: http://git.heureux-cyclage.org/?a=commitdiff_plain;h=c642d2c1a15ec5991efb9b25bbcedd8ecd0df219;p=lhc%2Fweb%2Fwiklou.git Add frontend API for uploading via iframe Coming next: File API support, stash support Bug: T64513 Change-Id: I06fa61e7155efe8126ba12cda9376c37f1c45e8e --- diff --git a/jsduck.json b/jsduck.json index 58d1ee65e3..5dd4977715 100644 --- a/jsduck.json +++ b/jsduck.json @@ -7,7 +7,7 @@ "--builtin-classes": true, "--processes": "0", "--warnings-exit-nonzero": true, - "--external": "HTMLElement,HTMLDocument,Window,File,MouseEvent,KeyboardEvent", + "--external": "HTMLElement,HTMLDocument,Window,File,MouseEvent,KeyboardEvent,HTMLIframeElement,HTMLInputElement,XMLDocument", "--output": "docs/js", "--": [ "maintenance/jsduck/external.js", diff --git a/resources/Resources.php b/resources/Resources.php index 5b6d52f629..c6f25ac827 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -855,6 +855,10 @@ return array( 'dependencies' => 'mediawiki.api', 'targets' => array( 'desktop', 'mobile' ), ), + 'mediawiki.api.upload' => array( + 'scripts' => 'resources/src/mediawiki.api/mediawiki.api.upload.js', + 'dependencies' => array( 'mediawiki.api', 'mediawiki.api.edit', 'json' ), + ), 'mediawiki.api.watch' => array( 'scripts' => 'resources/src/mediawiki.api/mediawiki.api.watch.js', 'dependencies' => array( diff --git a/resources/src/mediawiki.api/mediawiki.api.upload.js b/resources/src/mediawiki.api/mediawiki.api.upload.js new file mode 100644 index 0000000000..86cfff21a0 --- /dev/null +++ b/resources/src/mediawiki.api/mediawiki.api.upload.js @@ -0,0 +1,201 @@ +/** + * Provides an interface for uploading files to MediaWiki. + * @class mw.Api.plugin.upload + * @singleton + */ +( function ( mw, $ ) { + var nonce = 0, + fieldsAllowed = { + filename: true, + comment: true, + text: true, + watchlist: true, + ignorewarnings: true + }; + + /** + * @private + * Get nonce for iframe IDs on the page. + * @return {number} + */ + function getNonce() { + return nonce++; + } + + /** + * @private + * Get new iframe object for an upload. + * @return {HTMLIframeElement} + */ + function getNewIframe( id ) { + var frame = document.createElement( 'iframe' ); + frame.id = id; + frame.name = id; + return frame; + } + + /** + * @private + * Shortcut for getting hidden inputs + * @return {jQuery} + */ + function getHiddenInput( name, val ) { + return $( '') + .attr( 'name', name ) + .val( val ); + } + + /** + * Process the result of the form submission, returned to an iframe. + * This is the iframe's onload event. + * + * @param {HTMLIframeElement} iframe Iframe to extract result from + * @return {Object} Response from the server. The return value may or may + * not be an XMLDocument, this code was copied from elsewhere, so if you + * see an unexpected return type, please file a bug. + */ + function processIframeResult( iframe ) { + var json, + doc = iframe.contentDocument || frames[iframe.id].document; + + if ( doc.XMLDocument ) { + // The response is a document property in IE + return doc.XMLDocument; + } + + if ( doc.body ) { + // Get the json string + // We're actually searching through an HTML doc here -- + // according to mdale we need to do this + // because IE does not load JSON properly in an iframe + json = $( doc.body ).find( 'pre' ).text(); + + return JSON.parse( json ); + } + + // Response is a xml document + return doc; + } + + $.extend( mw.Api.prototype, { + /** + * Upload a file to MediaWiki. + * @param {HTMLInputElement} file HTML input type=file element with a file already inside of it. + * @param {Object} data Other upload options, see action=upload API docs for more + * @return {jQuery.Promise} + */ + upload: function ( file, data ) { + if ( !file ) { + return $.Deferred().reject( 'No file' ); + } + + if ( !file.nodeType || file.nodeType !== file.ELEMENT_NODE ) { + return $.Deferred().reject( 'Unsupported argument type passed to mw.Api.upload' ); + } + + return this.uploadWithIframe( file, data ); + }, + + /** + * Upload a file to MediaWiki with an iframe and a form. + * + * This method is necessary for browsers without the File/FormData + * APIs, and continues to work in browsers with those APIs. + * + * The rough sketch of how this method works is as follows: + * * An iframe is loaded with no content. + * * A form is submitted with the passed-in file input and some extras. + * * The MediaWiki API receives that form data, and sends back a response. + * * The response is sent to the iframe, because we set target=(iframe id) + * * The response is parsed out of the iframe's document, and passed back + * through the promise. + * @param {HTMLInputElement} file The file input with a file in it. + * @param {Object} data Other upload options, see action=upload API docs for more + * @return {jQuery.Promise} + */ + uploadWithIframe: function ( file, data ) { + var tokenPromise, + api = this, + filenameFound = false, + deferred = $.Deferred(), + nonce = getNonce(), + id = 'uploadframe-' + nonce, + $form = $( '
' ), + iframe = getNewIframe( id ), + $iframe = $( iframe ); + + $form.addClass( 'mw-api-upload-form' ); + + $form.append( + getHiddenInput( 'action', 'upload' ), + getHiddenInput( 'format', 'json' ), + file + ); + + $form.css( 'display', 'none' ) + .attr( { + action: api.defaults.ajax.url, + method: 'POST', + target: id, + enctype: 'multipart/form-data' + } ); + + $iframe.one( 'load', function () { + $iframe.one( 'load', function () { + var result = processIframeResult( iframe ); + + if ( !result ) { + deferred.reject( 'No response from API on upload attempt.' ); + } else if ( result.error || result.warnings ) { + if ( result.error && result.error.code === 'badtoken' ) { + api.badToken( 'edit' ); + } + + deferred.reject( result.error || result.warnings ); + } else { + deferred.notify( 1 ); + deferred.resolve( result ); + } + } ); + tokenPromise.done( function () { + $form.submit(); + } ); + } ); + + $iframe.error( function ( error ) { + deferred.reject( 'iframe failed to load: ' + error ); + } ); + + $iframe.prop( 'src', 'about:blank' ).hide(); + + file.name = 'file'; + + $.each( data, function ( key, val ) { + if ( key === 'filename' ) { + filenameFound = true; + } + + if ( fieldsAllowed[key] === true ) { + $form.append( getHiddenInput( key, val ) ); + } + } ); + + if ( !filenameFound ) { + return $.Deferred().reject( 'Filename not included in file data.' ); + } + + tokenPromise = this.getEditToken().then( function ( token ) { + $form.append( getHiddenInput( 'token', token ) ); + } ); + + $( 'body' ).append( $form, $iframe ); + + return deferred.promise(); + } + } ); + + /** + * @class mw.Api + * @mixins mw.Api.plugin.upload + */ +}( mediaWiki, jQuery ) ); diff --git a/tests/qunit/QUnitTestResources.php b/tests/qunit/QUnitTestResources.php index 97eddeefaf..345b7ef38e 100644 --- a/tests/qunit/QUnitTestResources.php +++ b/tests/qunit/QUnitTestResources.php @@ -79,6 +79,7 @@ return array( 'tests/qunit/suites/resources/mediawiki.api/mediawiki.api.category.test.js', 'tests/qunit/suites/resources/mediawiki.api/mediawiki.api.options.test.js', 'tests/qunit/suites/resources/mediawiki.api/mediawiki.api.parse.test.js', + 'tests/qunit/suites/resources/mediawiki.api/mediawiki.api.upload.test.js', 'tests/qunit/suites/resources/mediawiki.api/mediawiki.api.watch.test.js', 'tests/qunit/suites/resources/mediawiki.special/mediawiki.special.recentchanges.test.js', 'tests/qunit/suites/resources/mediawiki/mediawiki.language.test.js', @@ -106,6 +107,7 @@ return array( 'mediawiki.api.category', 'mediawiki.api.options', 'mediawiki.api.parse', + 'mediawiki.api.upload', 'mediawiki.api.watch', 'mediawiki.jqueryMsg', 'mediawiki.messagePoster', diff --git a/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.upload.test.js b/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.upload.test.js new file mode 100644 index 0000000000..1afbd35e85 --- /dev/null +++ b/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.upload.test.js @@ -0,0 +1,34 @@ +( function ( mw, $ ) { + QUnit.module( 'mediawiki.api.upload', QUnit.newMwEnvironment( {} ) ); + + QUnit.test( 'Basic functionality', function ( assert ) { + QUnit.expect( 2 ); + var api = new mw.Api(); + assert.ok( api.upload ); + // The below will return a rejected deferred, but that's OK. + assert.ok( api.upload() ); + } ); + + QUnit.test( 'Set up iframe upload', function ( assert ) { + QUnit.expect( 5 ); + var $iframe, $form, $input, + api = new mw.Api(); + + this.sandbox.stub( api, 'getEditToken', function () { + return $.Deferred().promise(); + } ); + + api.uploadWithIframe( $( '' )[0], { filename: 'Testing API upload.jpg' } ); + + $iframe = $( 'iframe' ); + $form = $( 'form.mw-api-upload-form' ); + $input = $form.find( 'input[name=filename]' ); + + assert.ok( $form.length > 0 ); + assert.ok( $input.length > 0 ); + assert.ok( $iframe.length > 0 ); + assert.strictEqual( $form.prop( 'target' ), $iframe.prop( 'id' ) ); + assert.strictEqual( $input.val(), 'Testing API upload.jpg' ); + } ); + +}( mediaWiki, jQuery ) );