jenkins-bot has submitted this change and it was merged.
Change subject: Replace FDT callbacks with OOJS events
......................................................................
Replace FDT callbacks with OOJS events
I think this is much cleaner.
Bug: T77124
Change-Id: I8513b7360271300efd478db8e98292623d306aa8
---
M UploadWizard.php
M resources/mw.ApiUploadFormDataHandler.js
M resources/mw.FormDataTransport.js
3 files changed, 329 insertions(+), 309 deletions(-)
Approvals:
Gilles: Looks good to me, approved
jenkins-bot: Verified
diff --git a/UploadWizard.php b/UploadWizard.php
index 0b49595..61d1e0d 100644
--- a/UploadWizard.php
+++ b/UploadWizard.php
@@ -99,6 +99,10 @@
$wgResourceModules['ext.uploadWizard.formDataTransport'] = array(
'scripts' => 'mw.FormDataTransport.js',
+
+ 'dependencies' => array(
+ 'oojs',
+ ),
) + $uploadWizardModuleInfo;
$wgResourceModules['ext.uploadWizard.iFrameTransport'] = array(
diff --git a/resources/mw.ApiUploadFormDataHandler.js
b/resources/mw.ApiUploadFormDataHandler.js
index 22a7aa5..eb2ff09 100644
--- a/resources/mw.ApiUploadFormDataHandler.js
+++ b/resources/mw.ApiUploadFormDataHandler.js
@@ -20,15 +20,12 @@
this.transport = new mw.FormDataTransport(
this.$form[0].action,
this.formData,
- this.upload,
- function ( fraction ) {
- handler.upload.setTransportProgress( fraction );
- },
- function ( result ) {
- handler.upload.setTransported( result );
- }
- );
-
+ this.upload
+ ).on( 'progress', function ( fraction ) {
+ handler.upload.setTransportProgress( fraction );
+ } ).on( 'transported', function ( result ) {
+ handler.upload.setTransported( result );
+ } );
};
mw.ApiUploadFormDataHandler.prototype = {
diff --git a/resources/mw.FormDataTransport.js
b/resources/mw.FormDataTransport.js
index d1121be..92c00a2 100644
--- a/resources/mw.FormDataTransport.js
+++ b/resources/mw.FormDataTransport.js
@@ -1,19 +1,22 @@
-( function ( mw, $ ) {
- /**
- * Represents a "transport" for files to upload; using html5 FormData.
- *
- * @param form jQuery selector for HTML form with selected
file
- * @param formData object with additinal form fields required
for upload api call
- * @param progressCb callback to execute when we've started.
- * @param transportedCb callback to execute when we've finished the
upload
- */
+( function ( mw, $, oo ) {
+ var FDTP;
- mw.FormDataTransport = function ( postUrl, formData, uploadObject,
progressCb, transportedCb ) {
+ /**
+ * @class mw.FormDataTransport
+ * Represents a "transport" for files to upload; using HTML5 FormData.
+ * @mixins oo.EventEmitter
+ *
+ * @constructor
+ * @param {string} postUrl URL to post to.
+ * @param {Object} formData Additional form fields required for upload
api call
+ * @param {mw.UploadWizardUpload} uploadObject Stupidly included object
that is controlling the upload details
+ */
+ mw.FormDataTransport = function ( postUrl, formData, uploadObject ) {
var profile = $.client.profile();
+ oo.EventEmitter.call( this );
+
this.formData = formData;
- this.progressCb = progressCb;
- this.transportedCb = transportedCb;
this.uploadObject = uploadObject;
this.postUrl = postUrl;
@@ -36,318 +39,334 @@
this.insufficientFormDataSupport = profile.name === 'firefox'
&& profile.versionNumber < 7;
};
- mw.FormDataTransport.prototype = {
- upload: function () {
- var formData,
- transport = this,
- file = this.uploadObject.file;
+ oo.mixinClass( mw.FormDataTransport, oo.EventEmitter );
- // use timestamp + filename to avoid conflicts on server
- this.tempname = ( new Date() ).getTime().toString() +
file.name;
- // remove unicode characters, tempname is only used
during upload
- this.tempname = this.tempname.split('').map(function
(c) {
- return c.charCodeAt(0) > 128 ? '_' : c;
- }).join('');
+ FDTP = mw.FormDataTransport.prototype;
- if ( mw.UploadWizard.config.enableChunked && file.size
> this.chunkSize ) {
- this.uploadChunk(0);
- } else {
- this.xhr = new XMLHttpRequest();
- this.xhr.addEventListener('load', function
(evt) {
- transport.parseResponse(evt,
transport.transportedCb);
- }, false);
- this.xhr.addEventListener('error', function
(evt) {
- transport.parseResponse(evt,
transport.transportedCb);
- }, false);
- this.xhr.upload.addEventListener('progress',
function (evt) {
- if ( transport.uploadObject.state ===
'aborted' ) {
- transport.xhr.abort();
- return;
- }
- if (evt.lengthComputable) {
- var progress =
parseFloat(evt.loaded / evt.total );
- transport.progressCb(progress);
- }
- }, false);
- this.xhr.addEventListener('abort', function
(evt) {
- transport.parseResponse(evt,
transport.transportedCb);
- }, false);
+ FDTP.upload = function () {
+ var formData,
+ transport = this,
+ file = this.uploadObject.file;
- formData = new FormData();
+ // use timestamp + filename to avoid conflicts on server
+ this.tempname = ( new Date() ).getTime().toString() + file.name;
+ // remove unicode characters, tempname is only used during
upload
+ this.tempname = this.tempname.split('').map(function (c) {
+ return c.charCodeAt(0) > 128 ? '_' : c;
+ }).join('');
- $.each(this.formData, function (key, value) {
- formData.append(key, value);
- });
- formData.append('filename', this.tempname);
- formData.append('file', file);
-
- // ignorewarnings is turned on, since warnings
are presented in a later step and this
- // transport doesn't know how to deal with
them. Also, it's important to allow people to
- // upload files with (for example) blacklisted
names, and then rename them later in the
- // wizard.
- formData.append( 'ignorewarnings', true );
-
- this.xhr.open('POST', this.postUrl, true);
- this.xhr.send(formData);
- }
- },
- uploadChunk: function (offset) {
- var formData,
- transport = this,
- file = this.uploadObject.file,
- bytesAvailable = file.size,
- chunk;
- if ( this.uploadObject.state === 'aborted' ) {
- if ( this.xhr ) {
- this.xhr.abort();
- }
- return;
- }
- //Slice API was changed and has vendor prefix for now
- //new version now require start/end and not start/length
- if (file.mozSlice) {
- chunk = file.mozSlice(offset, offset +
this.chunkSize, file.type);
- } else if (file.webkitSlice) {
- chunk = file.webkitSlice(offset, offset +
this.chunkSize, file.type);
- } else {
- chunk = file.slice(offset, offset +
this.chunkSize, file.type);
- }
-
+ if ( mw.UploadWizard.config.enableChunked && file.size >
this.chunkSize ) {
+ this.uploadChunk(0);
+ } else {
this.xhr = new XMLHttpRequest();
this.xhr.addEventListener('load', function (evt) {
- transport.responseText =
evt.target.responseText;
- transport.parseResponse(evt, function
(response) {
- if (response.upload &&
response.upload.filekey) {
- transport.filekey =
response.upload.filekey;
- }
- if (response.upload &&
response.upload.result === 'Success') {
- //upload finished and can be
unstashed later
-
transport.transportedCb(response);
- } else if (response.upload &&
response.upload.result === 'Poll') {
- //Server not ready, wait for 3
seconds
- setTimeout(function () {
- transport.checkStatus();
- }, 3000);
- } else if (response.upload &&
response.upload.result === 'Continue') {
- //reset retry counter
- transport.retries = 0;
- //start uploading next chunk
-
transport.uploadChunk(response.upload.offset);
- } else {
- if (
transport.uploadObject.state === 'aborted' ) {
- return;
- }
- //failed to upload, try again
in 3 seconds
- transport.retries++;
- if (transport.maxRetries > 0 &&
transport.retries >= transport.maxRetries) {
- mw.log.warn( 'Max
retries exceeded on unknown response' );
- //upload failed, raise
response
-
transport.transportedCb(response);
- } else {
- mw.log( 'Retry #' +
transport.retries + ' on unknown response' );
- setTimeout(function () {
-
transport.uploadChunk(offset);
- }, 3000);
- }
- }
- });
+ transport.emitParsedResponse( evt );
}, false);
this.xhr.addEventListener('error', function (evt) {
- if ( transport.uploadObject.state === 'aborted'
) {
- return;
- }
- //failed to upload, try again in 3 second
- transport.retries++;
- if (transport.maxRetries > 0 &&
transport.retries >= transport.maxRetries) {
- mw.log.warn( 'Max retries exceeded on
error event' );
- transport.parseResponse(evt,
transport.transportedCb);
- } else {
- mw.log( 'Retry #' + transport.retries +
' on error event' );
- setTimeout(function () {
- transport.uploadChunk(offset);
- }, 3000);
- }
+ transport.emitParsedResponse( evt );
}, false);
- this.xhr.upload.addEventListener('progress', function
(evt) {
+
+ this.xhr.upload.addEventListener( 'progress', function
( evt ) {
if ( transport.uploadObject.state === 'aborted'
) {
transport.xhr.abort();
+ return;
}
- if (evt.lengthComputable) {
- var progress = parseFloat( offset +
evt.loaded ) / bytesAvailable;
- transport.progressCb(progress);
+
+ if ( evt.lengthComputable ) {
+ var progress = parseFloat(evt.loaded /
evt.total );
+ transport.emit( 'progress', progress );
}
}, false);
this.xhr.addEventListener('abort', function (evt) {
- transport.parseResponse(evt,
transport.transportedCb);
+ transport.emitParsedResponse( evt );
}, false);
- if (this.insufficientFormDataSupport) {
- formData = this.geckoFormData();
- } else {
- formData = new FormData();
- }
+ formData = new FormData();
+
$.each(this.formData, function (key, value) {
formData.append(key, value);
});
- formData.append('offset', offset);
formData.append('filename', this.tempname);
+ formData.append('file', file);
- // ignorewarnings is turned on intentionally, see the
above comment to the same effect.
+ // ignorewarnings is turned on, since warnings are
presented in a later step and this
+ // transport doesn't know how to deal with them. Also,
it's important to allow people to
+ // upload files with (for example) blacklisted names,
and then rename them later in the
+ // wizard.
formData.append( 'ignorewarnings', true );
- // only enable async if file is larger 10Mb
- if ( bytesAvailable > 10 * 1024 * 1024 ) {
- formData.append( 'async', true );
- }
- if (this.filekey) {
- formData.append('filekey', this.filekey);
- }
- formData.append('filesize', bytesAvailable);
- if (this.insufficientFormDataSupport) {
- formData.appendBlob('chunk', chunk,
'chunk.bin');
- } else {
- formData.append('chunk', chunk);
- }
this.xhr.open('POST', this.postUrl, true);
- if (this.insufficientFormDataSupport) {
- formData.send(this.xhr);
- } else {
- this.xhr.send(formData);
- }
- },
- checkStatus: function () {
- var transport = this,
- api = new mw.Api(),
- params = {};
- if ( this.uploadObject.state === 'aborted' ) {
- return;
- }
- if (!this.firstPoll) {
- this.firstPoll = ( new Date() ).getTime();
- }
- $.each(this.formData, function (key, value) {
- params[key] = value;
- });
- params.checkstatus = true;
- params.filekey = this.filekey;
- api.post( params )
- .done( function (response) {
- if (response.upload &&
response.upload.result === 'Poll') {
- //If concatenation takes longer
than 10 minutes give up
- if ( ( ( new Date() ).getTime()
- transport.firstPoll ) > 10 * 60 * 1000 ) {
-
transport.transportedCb({
- code:
'server-error',
- info: 'unknown
server error'
- });
- //Server not ready, wait for 3
more seconds
- } else {
- if (
response.upload.stage === undefined && window.console ) {
-
window.console.log( 'Unable to check file\'s status' );
- } else {
- //Statuses that
can be returned:
- //
*mwe-upwiz-queued
- //
*mwe-upwiz-publish
- //
*mwe-upwiz-assembling
-
transport.uploadObject.ui.setStatus( 'mwe-upwiz-' + response.upload.stage );
-
setTimeout(function () {
-
transport.checkStatus();
- }, 3000);
- }
- }
- } else {
-
transport.transportedCb(response);
- }
- } )
- .fail( function (status, response) {
- transport.transportedCb(response);
- } );
- },
- parseResponse: function (evt, callback) {
- var response;
- try {
- response = $.parseJSON(evt.target.responseText);
- } catch ( e ) {
- response = {
- error: {
- code: evt.target.code,
- info: evt.target.responseText
- }
- };
- }
- callback(response);
- },
- geckoFormData: function () {
- var formData, onload,
- boundary = '------XX' + Math.random(),
- dashdash = '--',
- crlf = '\r\n',
- builder = '', // Build RFC2388 string.
- chunksRemaining = 0;
-
- builder += dashdash + boundary + crlf;
-
- formData = {
- append: function (name, data) {
- // Generate headers.
- builder += 'Content-Disposition:
form-data; name="' + name + '"';
- builder += crlf;
- builder += crlf;
-
- // Write data.
- builder += data;
- builder += crlf;
-
- // Write boundary.
- builder += dashdash + boundary + crlf;
- },
- appendFile: function (name, data, type,
filename) {
- builder += 'Content-Disposition:
form-data; name="' + name + '"';
- builder += '; filename="' + filename +
'"';
- builder += crlf;
- builder += 'Content-Type: ' + type;
- builder += crlf;
- builder += crlf;
-
- // Write binary data.
- builder += data;
- builder += crlf;
-
- // Write boundary.
- builder += dashdash + boundary + crlf;
- },
- appendBlob: function (name, blob, filename) {
- chunksRemaining++;
- var reader = new FileReader();
- reader.onload = function (e) {
- formData.appendFile(name,
e.target.result,
-
blob.type, filename);
- // Call onload after last Blob
- chunksRemaining--;
- if (!chunksRemaining &&
formData.xhr) {
- onload();
- }
- };
- reader.readAsBinaryString(blob);
- },
- send: function (xhr) {
- formData.xhr = xhr;
- if (!chunksRemaining) {
- onload();
- }
- }
- };
- onload = function () {
- // Mark end of the request.
- builder += dashdash + boundary + dashdash +
crlf;
-
- // Send to server
- formData.xhr.setRequestHeader(
- 'Content-type',
- 'multipart/form-data; boundary=' +
boundary
- );
- formData.xhr.sendAsBinary(builder);
- };
- return formData;
+ this.xhr.send(formData);
}
};
-}( mediaWiki, jQuery ) );
+
+ FDTP.uploadChunk = function ( offset ) {
+ var formData,
+ transport = this,
+ file = this.uploadObject.file,
+ bytesAvailable = file.size,
+ chunk;
+ if ( this.uploadObject.state === 'aborted' ) {
+ if ( this.xhr ) {
+ this.xhr.abort();
+ }
+ return;
+ }
+ //Slice API was changed and has vendor prefix for now
+ //new version now require start/end and not start/length
+ if (file.mozSlice) {
+ chunk = file.mozSlice(offset, offset + this.chunkSize,
file.type);
+ } else if (file.webkitSlice) {
+ chunk = file.webkitSlice(offset, offset +
this.chunkSize, file.type);
+ } else {
+ chunk = file.slice(offset, offset + this.chunkSize,
file.type);
+ }
+
+ this.xhr = new XMLHttpRequest();
+ this.xhr.addEventListener('load', function (evt) {
+ transport.responseText = evt.target.responseText;
+ transport.parseResponse(evt, function (response) {
+ if (response.upload && response.upload.filekey)
{
+ transport.filekey =
response.upload.filekey;
+ }
+ if (response.upload && response.upload.result
=== 'Success') {
+ //upload finished and can be unstashed
later
+ transport.emit( 'transported', response
);
+ } else if (response.upload &&
response.upload.result === 'Poll') {
+ //Server not ready, wait for 3 seconds
+ setTimeout(function () {
+ transport.checkStatus();
+ }, 3000);
+ } else if (response.upload &&
response.upload.result === 'Continue') {
+ //reset retry counter
+ transport.retries = 0;
+ //start uploading next chunk
+
transport.uploadChunk(response.upload.offset);
+ } else {
+ //failed to upload, try again in 3
seconds
+ transport.retries++;
+ if (transport.maxRetries > 0 &&
transport.retries >= transport.maxRetries) {
+ mw.log.warn( 'Max retries
exceeded on unknown response' );
+ //upload failed, raise response
+ transport.emit( 'transported',
response );
+ } else {
+ mw.log( 'Retry #' +
transport.retries + ' on unknown response' );
+ setTimeout(function () {
+
transport.uploadChunk(offset);
+ }, 3000);
+ }
+ }
+ });
+ }, false);
+ this.xhr.addEventListener('error', function (evt) {
+ //failed to upload, try again in 3 second
+ transport.retries++;
+ if (transport.maxRetries > 0 && transport.retries >=
transport.maxRetries) {
+ mw.log.warn( 'Max retries exceeded on error
event' );
+ transport.emitParsedResponse( evt );
+ } else {
+ mw.log( 'Retry #' + transport.retries + ' on
error event' );
+ setTimeout(function () {
+ transport.uploadChunk(offset);
+ }, 3000);
+ }
+ }, false);
+ this.xhr.upload.addEventListener( 'progress', function ( evt ) {
+ if ( transport.uploadObject.state === 'aborted' ) {
+ transport.xhr.abort();
+ }
+ if ( evt.lengthComputable ) {
+ var progress = parseFloat( offset + evt.loaded
) / bytesAvailable;
+ transport.emit( 'progress', progress );
+ }
+ }, false );
+ this.xhr.addEventListener('abort', function (evt) {
+ transport.emitParsedResponse( evt );
+ }, false);
+
+ if (this.insufficientFormDataSupport) {
+ formData = this.geckoFormData();
+ } else {
+ formData = new FormData();
+ }
+ $.each(this.formData, function (key, value) {
+ formData.append(key, value);
+ });
+ formData.append('offset', offset);
+ formData.append('filename', this.tempname);
+
+ // ignorewarnings is turned on intentionally, see the above
comment to the same effect.
+ formData.append( 'ignorewarnings', true );
+ // only enable async if file is larger 10Mb
+ if ( bytesAvailable > 10 * 1024 * 1024 ) {
+ formData.append( 'async', true );
+ }
+
+ if (this.filekey) {
+ formData.append('filekey', this.filekey);
+ }
+ formData.append('filesize', bytesAvailable);
+ if (this.insufficientFormDataSupport) {
+ formData.appendBlob('chunk', chunk, 'chunk.bin');
+ } else {
+ formData.append('chunk', chunk);
+ }
+ this.xhr.open('POST', this.postUrl, true);
+ if (this.insufficientFormDataSupport) {
+ formData.send(this.xhr);
+ } else {
+ this.xhr.send(formData);
+ }
+ };
+
+ FDTP.checkStatus = function () {
+ var transport = this,
+ api = new mw.Api(),
+ params = {};
+ if ( this.uploadObject.state === 'aborted' ) {
+ return;
+ }
+ if (!this.firstPoll) {
+ this.firstPoll = ( new Date() ).getTime();
+ }
+ $.each(this.formData, function (key, value) {
+ params[key] = value;
+ });
+ params.checkstatus = true;
+ params.filekey = this.filekey;
+ api.post( params )
+ .done( function (response) {
+ if (response.upload && response.upload.result
=== 'Poll') {
+ //If concatenation takes longer than 10
minutes give up
+ if ( ( ( new Date() ).getTime() -
transport.firstPoll ) > 10 * 60 * 1000 ) {
+ transport.emit( 'transported', {
+ code: 'server-error',
+ info: 'unknown server
error'
+ } );
+ //Server not ready, wait for 3 more
seconds
+ } else {
+ if ( response.upload.stage ===
undefined && window.console ) {
+ window.console.log(
'Unable to check file\'s status' );
+ } else {
+ //Statuses that can be
returned:
+ // *mwe-upwiz-queued
+ // *mwe-upwiz-publish
+ // *mwe-upwiz-assembling
+
transport.uploadObject.ui.setStatus( 'mwe-upwiz-' + response.upload.stage );
+ setTimeout(function () {
+
transport.checkStatus();
+ }, 3000);
+ }
+ }
+ } else {
+ transport.emit( 'transported', response
);
+ }
+ } )
+ .fail( function (status, response) {
+ transport.emit( 'transported', response );
+ } );
+ };
+
+ FDTP.parseResponse = function ( evt, cb ) {
+ var response;
+ try {
+ response = $.parseJSON(evt.target.responseText);
+ } catch ( e ) {
+ response = {
+ error: {
+ code: evt.target.code,
+ info: evt.target.responseText
+ }
+ };
+ }
+
+ cb( response );
+ };
+
+ /**
+ * Emits a 'transported' event with an object indicating status,
+ * parsed from JSON in the event body.
+ * @param {Event} evt The response event.
+ */
+ FDTP.emitParsedResponse = function ( evt ) {
+ var transport = this;
+
+ this.parseResponse( evt, function ( response ) {
+ transport.emit( 'transported', response );
+ } );
+ };
+
+ FDTP.geckoFormData = function () {
+ var formData, onload,
+ boundary = '------XX' + Math.random(),
+ dashdash = '--',
+ crlf = '\r\n',
+ builder = '', // Build RFC2388 string.
+ chunksRemaining = 0;
+
+ builder += dashdash + boundary + crlf;
+
+ formData = {
+ append: function (name, data) {
+ // Generate headers.
+ builder += 'Content-Disposition: form-data;
name="' + name + '"';
+ builder += crlf;
+ builder += crlf;
+
+ // Write data.
+ builder += data;
+ builder += crlf;
+
+ // Write boundary.
+ builder += dashdash + boundary + crlf;
+ },
+ appendFile: function (name, data, type, filename) {
+ builder += 'Content-Disposition: form-data;
name="' + name + '"';
+ builder += '; filename="' + filename + '"';
+ builder += crlf;
+ builder += 'Content-Type: ' + type;
+ builder += crlf;
+ builder += crlf;
+
+ // Write binary data.
+ builder += data;
+ builder += crlf;
+
+ // Write boundary.
+ builder += dashdash + boundary + crlf;
+ },
+ appendBlob: function (name, blob, filename) {
+ chunksRemaining++;
+ var reader = new FileReader();
+ reader.onload = function (e) {
+ formData.appendFile(name,
e.target.result,
+
blob.type, filename);
+ // Call onload after last Blob
+ chunksRemaining--;
+ if (!chunksRemaining && formData.xhr) {
+ onload();
+ }
+ };
+ reader.readAsBinaryString(blob);
+ },
+ send: function (xhr) {
+ formData.xhr = xhr;
+ if (!chunksRemaining) {
+ onload();
+ }
+ }
+ };
+ onload = function () {
+ // Mark end of the request.
+ builder += dashdash + boundary + dashdash + crlf;
+
+ // Send to server
+ formData.xhr.setRequestHeader(
+ 'Content-type',
+ 'multipart/form-data; boundary=' + boundary
+ );
+ formData.xhr.sendAsBinary(builder);
+ };
+ return formData;
+ };
+}( mediaWiki, jQuery, OO ) );
--
To view, visit https://gerrit.wikimedia.org/r/178207
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: I8513b7360271300efd478db8e98292623d306aa8
Gerrit-PatchSet: 4
Gerrit-Project: mediawiki/extensions/UploadWizard
Gerrit-Branch: master
Gerrit-Owner: MarkTraceur <[email protected]>
Gerrit-Reviewer: Gilles <[email protected]>
Gerrit-Reviewer: jenkins-bot <>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits