Bartosz Dziewoński has uploaded a new change for review.
https://gerrit.wikimedia.org/r/244512
Change subject: mw.Upload: Refactor error handling for the umpteenth time
......................................................................
mw.Upload: Refactor error handling for the umpteenth time
When I started, I just wanted mw.Upload.BookletLayout to be able to
display more information about errors than just the default message
(like it can do for warnings). And down the rabbit hole I went...
mediawiki/api/upload.js:
* Simply throw errors when our methods are called with bad parameters,
rather than return a rejected promise.
* Always call .notify( 1 ) when upload is complete, regardless of
whether it succeeded or failed.
* Reject promises with error code and error details, for consistency
with api.js. Previous behavior meant that we did not let callers
know the details in some cases. It was also problematic when we
passed-through promises rejected in api.js (which had different
parameters given).
* Made some effort to return sane codes when something intricate
fails in iframe upload, but no guarantee that this works well. The
codes are inspired by what api.js returns in similar circumstances.
* When rejecting because of warnings, use the first warning's key as
error code.
* Always ignore the warnings when uploading to stash and 'filekey'
is present in response, never ignore when uploading directly.
* When the upload succeeds, never check for 'result.upload.error'
(which just isn't a thing) nor for 'result.error' (which api.js
detects and rejects the promise before we get to it). We only need
to check for 'result.upload.warnings'.
mediawiki.Upload.js:
* Update for the above changes in mediawiki/api/upload.js.
* More reliably distinguish warnings from errors in all cases, not
only when finishing a stash upload.
* Store machine-readable error codes, not mw.Message objects. This
lets callers do something sensible when we encounter an unknown
error (especially one that has no corresponding message).
* Store full result as state details for warnings, as well as errors.
mediawiki.Upload.BookletLayout.js:
* Update for the above changes in mediawiki.Upload.js.
* Give errors/warnings generated during upload to stash the same
loving treatment as errors/warnings during publishing.
* Extract the code to a new method getErrorMessageForStateDetails().
* Handle 'stashfailed' warning (which is really an error).
* Handle unknown errors, now that mw.Upload lets us do something
sensible with them. (See, this is the thing I set out to do.)
Bug: T114940
Change-Id: I4c0f619a4e483cca296c2fa2907ed1f81a99fdd6
---
M resources/Resources.php
M resources/src/mediawiki/api/upload.js
M resources/src/mediawiki/mediawiki.Upload.BookletLayout.js
M resources/src/mediawiki/mediawiki.Upload.js
4 files changed, 223 insertions(+), 172 deletions(-)
git pull ssh://gerrit.wikimedia.org:29418/mediawiki/core
refs/changes/12/244512/1
diff --git a/resources/Resources.php b/resources/Resources.php
index c635f0e..4d1b5ef 100644
--- a/resources/Resources.php
+++ b/resources/Resources.php
@@ -1135,54 +1135,6 @@
'dom-level2-shim',
'mediawiki.api.upload',
),
- 'messages' => array(
- 'api-error-badaccess-groups',
- 'api-error-badtoken',
- 'api-error-copyuploaddisabled',
- 'api-error-duplicate',
- 'api-error-duplicate-archive',
- 'api-error-empty-file',
- 'api-error-emptypage',
- 'api-error-fetchfileerror',
- 'api-error-fileexists-forbidden',
- 'api-error-fileexists-shared-forbidden',
- 'api-error-file-too-large',
- 'api-error-filename-tooshort',
- 'api-error-filetype-banned',
- 'api-error-filetype-banned-type',
- 'api-error-filetype-missing',
- 'api-error-hookaborted',
- 'api-error-http',
- 'api-error-illegal-filename',
- 'api-error-internal-error',
- 'api-error-invalid-file-key',
- 'api-error-missingparam',
- 'api-error-missingresult',
- 'api-error-mustbeloggedin',
- 'api-error-mustbeposted',
- 'api-error-noimageinfo',
- 'api-error-nomodule',
- 'api-error-ok-but-empty',
- 'api-error-overwrite',
- 'api-error-stashfailed',
- 'api-error-publishfailed',
- 'api-error-stasherror',
- 'api-error-stashedfilenotfound',
- 'api-error-stashpathinvalid',
- 'api-error-stashfilestorage',
- 'api-error-stashzerolength',
- 'api-error-stashnotloggedin',
- 'api-error-stashwrongowner',
- 'api-error-stashnosuchfilekey',
- 'api-error-timeout',
- 'api-error-unclassified',
- 'api-error-unknown-code',
- 'api-error-unknown-error',
- 'api-error-unknown-warning',
- 'api-error-unknownerror',
- 'api-error-uploaddisabled',
- 'api-error-verification-error',
- ),
),
'mediawiki.ForeignUpload' => array(
'scripts' =>
'resources/src/mediawiki/mediawiki.ForeignUpload.js',
@@ -1231,6 +1183,52 @@
'upload-form-label-infoform-description',
'upload-form-label-usage-title',
'upload-form-label-usage-filename',
+ 'api-error-unknownerror',
+ 'api-error-unknown-warning',
+ 'api-error-badaccess-groups',
+ 'api-error-badtoken',
+ 'api-error-copyuploaddisabled',
+ 'api-error-duplicate',
+ 'api-error-duplicate-archive',
+ 'api-error-empty-file',
+ 'api-error-emptypage',
+ 'api-error-fetchfileerror',
+ 'api-error-fileexists-forbidden',
+ 'api-error-fileexists-shared-forbidden',
+ 'api-error-file-too-large',
+ 'api-error-filename-tooshort',
+ 'api-error-filetype-banned',
+ 'api-error-filetype-banned-type',
+ 'api-error-filetype-missing',
+ 'api-error-hookaborted',
+ 'api-error-http',
+ 'api-error-illegal-filename',
+ 'api-error-internal-error',
+ 'api-error-invalid-file-key',
+ 'api-error-missingparam',
+ 'api-error-missingresult',
+ 'api-error-mustbeloggedin',
+ 'api-error-mustbeposted',
+ 'api-error-noimageinfo',
+ 'api-error-nomodule',
+ 'api-error-ok-but-empty',
+ 'api-error-overwrite',
+ 'api-error-stashfailed',
+ 'api-error-publishfailed',
+ 'api-error-stasherror',
+ 'api-error-stashedfilenotfound',
+ 'api-error-stashpathinvalid',
+ 'api-error-stashfilestorage',
+ 'api-error-stashzerolength',
+ 'api-error-stashnotloggedin',
+ 'api-error-stashwrongowner',
+ 'api-error-stashnosuchfilekey',
+ 'api-error-timeout',
+ 'api-error-unclassified',
+ 'api-error-unknown-code',
+ 'api-error-unknown-error',
+ 'api-error-uploaddisabled',
+ 'api-error-verification-error',
'fileexists',
'filepageexists',
'filename-bad-prefix',
diff --git a/resources/src/mediawiki/api/upload.js
b/resources/src/mediawiki/api/upload.js
index 4d6b34c..614c001 100644
--- a/resources/src/mediawiki/api/upload.js
+++ b/resources/src/mediawiki/api/upload.js
@@ -28,6 +28,21 @@
/**
* @private
+ * Given a non-empty object, return one of its keys.
+ *
+ * @param {Object} obj
+ * @return {string}
+ */
+ function getFirstKey( obj ) {
+ for ( var key in obj ) {
+ if ( obj.hasOwnProperty( key ) ) {
+ return key;
+ }
+ }
+ }
+
+ /**
+ * @private
* Get new iframe object for an upload.
*
* @return {HTMLIframeElement}
@@ -116,13 +131,13 @@
}
if ( !file ) {
- return $.Deferred().reject( 'No file' );
+ throw new Error( 'No file' );
}
canUseFormData = formDataAvailable() && file instanceof
window.File;
if ( !isFileInput && !canUseFormData ) {
- return $.Deferred().reject( 'Unsupported
argument type passed to mw.Api.upload' );
+ throw new Error( 'Unsupported argument type
passed to mw.Api.upload' );
}
if ( canUseFormData ) {
@@ -182,17 +197,19 @@
$iframe.one( 'load', function () {
$iframe.one( 'load', function () {
var result = processIframeResult(
iframe );
+ deferred.notify( 1 );
if ( !result ) {
- deferred.reject( 'No response
from API on upload attempt.' );
- } else if ( result.error ||
result.warnings ) {
- if ( result.error &&
result.error.code === 'badtoken' ) {
+ deferred.reject(
'ok-but-empty', 'No response from API on upload attempt.' );
+ } else if ( result.error ) {
+ if ( result.error.code ===
'badtoken' ) {
api.badToken( 'edit' );
}
- deferred.reject( result.error
|| result.warnings );
+ deferred.reject(
result.error.code, result );
+ } else if ( result.upload &&
result.upload.warnings ) {
+ deferred.reject( getFirstKey(
result.upload.warnings ), result );
} else {
- deferred.notify( 1 );
deferred.resolve( result );
}
} );
@@ -202,7 +219,7 @@
} );
$iframe.error( function ( error ) {
- deferred.reject( 'iframe failed to load: ' +
error );
+ deferred.reject( 'http', error );
} );
$iframe.prop( 'src', 'about:blank' ).hide();
@@ -214,7 +231,7 @@
} );
if ( !data.filename && !data.stash ) {
- return $.Deferred().reject( 'Filename not
included in file data.' );
+ throw new Error( 'Filename not included in file
data.' );
}
if ( this.needToken() ) {
@@ -258,7 +275,7 @@
data.file = file;
if ( !data.filename && !data.stash ) {
- return $.Deferred().reject( 'Filename not
included in file data.' );
+ throw new Error( 'Filename not included in file
data.' );
}
// Use this.postWithEditToken() or this.post()
@@ -281,15 +298,16 @@
}
} )
.done( function ( result ) {
- if ( result.error || result.warnings ) {
- deferred.reject( result.error
|| result.warnings );
+ deferred.notify( 1 );
+ if ( result.upload &&
result.upload.warnings ) {
+ deferred.reject( getFirstKey(
result.upload.warnings ), result );
} else {
- deferred.notify( 1 );
deferred.resolve( result );
}
} )
- .fail( function ( result ) {
- deferred.reject( result );
+ .fail( function ( errorCode, result ) {
+ deferred.notify( 1 );
+ deferred.reject( errorCode, result );
} );
return deferred.promise();
@@ -324,7 +342,7 @@
api = this;
if ( !data.filename ) {
- return $.Deferred().reject( 'Filename not
included in file data.' );
+ throw new Error( 'Filename not included in file
data.' );
}
function finishUpload( moreData ) {
@@ -334,26 +352,31 @@
data.format = 'json';
if ( !data.filename ) {
- return $.Deferred().reject( 'Filename
not included in file data.' );
+ throw new Error( 'Filename not included
in file data.' );
}
return api.postWithEditToken( data ).then(
function ( result ) {
- if ( result.upload && (
result.upload.error || result.upload.warnings ) ) {
- return $.Deferred().reject(
result.upload.error || result.upload.warnings ).promise();
+ if ( result.upload &&
result.upload.warnings ) {
+ return $.Deferred().reject(
getFirstKey( result.upload.warnings ), result ).promise();
}
return result;
} );
}
- return this.upload( file, { stash: true, filename:
data.filename } ).then( function ( result ) {
- if ( result && result.upload &&
result.upload.filekey ) {
+ return this.upload( file, { stash: true, filename:
data.filename } ).then(
+ function ( result ) {
filekey = result.upload.filekey;
- } else if ( result && ( result.error ||
result.warning ) ) {
- return $.Deferred().reject( result );
+ return finishUpload;
+ },
+ function ( errorCode, result ) {
+ if ( result && result.upload &&
result.upload.filekey ) {
+ // Ignore any warnings if
'filekey' was returned, that's all we care about
+ filekey = result.upload.filekey;
+ return $.Deferred().resolve(
finishUpload );
+ }
+ return $.Deferred().reject( errorCode,
result );
}
-
- return finishUpload;
- } );
+ );
},
needToken: function () {
diff --git a/resources/src/mediawiki/mediawiki.Upload.BookletLayout.js
b/resources/src/mediawiki/mediawiki.Upload.BookletLayout.js
index c4b8c09..1a748da 100644
--- a/resources/src/mediawiki/mediawiki.Upload.BookletLayout.js
+++ b/resources/src/mediawiki/mediawiki.Upload.BookletLayout.js
@@ -191,19 +191,15 @@
deferred.resolve();
layout.emit( 'fileUploaded' );
}, function () {
- // These errors will be thrown while the user is on the
info page
- if ( layout.upload.getState() === mw.Upload.State.ERROR
) {
- deferred.reject( new OO.ui.Error(
layout.upload.getStateDetails(), {
- recoverable: false
- } ) );
- return false;
- }
- if ( layout.upload.getState() ===
mw.Upload.State.WARNING ) {
- deferred.reject( new OO.ui.Error(
layout.upload.getStateDetails(), {
- recoverable: false
- } ) );
- return false;
- }
+ var
+ state = layout.upload.getState(),
+ stateDetails = layout.upload.getStateDetails(),
+ errorMessage =
layout.getErrorMessageForStateDetails( state, stateDetails );
+
+ // These errors will be thrown while the user is on the
info page.
+ // Pretty sure it's impossible to get a warning other
than 'stashfailed' here, which should
+ // really be an error...
+ deferred.reject( errorMessage );
} );
// If there is an error in uploading, come back to the upload
page
@@ -244,80 +240,112 @@
deferred.resolve();
layout.emit( 'fileSaved' );
}, function () {
- var stateDetails =
layout.upload.getStateDetails();
+ var
+ state = layout.upload.getState(),
+ stateDetails =
layout.upload.getStateDetails(),
+ errorMessage =
layout.getErrorMessageForStateDetails( state, stateDetails );
- if ( layout.upload.getState() ===
mw.Upload.State.ERROR ) {
- deferred.reject( new OO.ui.Error(
stateDetails, {
- recoverable: false
- } ) );
- return false;
- }
-
- if ( layout.upload.getState() ===
mw.Upload.State.WARNING ) {
- // We could get more than one of these
errors, these are in order
- // of importance. For example fixing
the thumbnail like file name
- // won't help the fact that the file
already exists.
- if ( stateDetails.exists !== undefined
) {
- deferred.reject( new
OO.ui.Error(
- $( '<p>' ).html(
- mw.message(
'filepageexists', stateDetails.exists ).parse()
- ),
- { recoverable: false }
- ) );
- } else if ( stateDetails.duplicate !==
undefined ) {
- deferred.reject( new
OO.ui.Error(
- $( '<p>' ).html(
- mw.message(
'fileexists', stateDetails.duplicate[ 0 ] ).parse()
- ),
- { recoverable: false }
- ) );
- } else if ( stateDetails[ 'thumb-name'
] !== undefined ) {
- deferred.reject( new
OO.ui.Error(
- $( '<p>' ).html(
- mw.message(
'filename-thumb-name' ).parse()
- ),
- { recoverable: false }
- ) );
- } else if ( stateDetails[ 'bad-prefix'
] !== undefined ) {
- deferred.reject( new
OO.ui.Error(
- $( '<p>' ).html(
- mw.message(
'filename-bad-prefix', stateDetails[ 'bad-prefix' ] ).parse()
- ),
- { recoverable: false }
- ) );
- } else if ( stateDetails[
'duplicate-archive' ] !== undefined ) {
- deferred.reject( new
OO.ui.Error(
- $( '<p>' ).html(
- mw.message(
'api-error-duplicate-archive', stateDetails[ 'duplicate-archive' ] ).parse()
- ),
- { recoverable: false }
- ) );
- } else if ( stateDetails.badfilename
!== undefined ) {
- // Change the name if the
current name isn't acceptable
- layout.filenameWidget.setValue(
stateDetails.badfilename );
- deferred.reject( new
OO.ui.Error(
- $( '<p>' ).html(
- mw.message(
'badfilename', stateDetails.badfilename ).parse()
- )
- ) );
- } else {
- deferred.reject( new
OO.ui.Error(
- $( '<p>' ).html(
- // Let's get
all the help we can if we can't pin point the error
- mw.message(
'api-error-unknown-warning', JSON.stringify( stateDetails ) ).parse()
- ),
- { recoverable: false }
- ) );
- }
-
- return false;
- }
+ deferred.reject( errorMessage );
} );
} );
return deferred.promise();
};
+ /**
+ * Get an error message (as OO.ui.Error object) that should be
displayed to the user for the given
+ * state and state details.
+ *
+ * @protected
+ * @param {mw.Upload.State} state mw.Upload.State.ERROR or
mw.Upload.State.WARNING
+ * @param {Object} stateDetails
+ * @returns {OO.ui.Error} Error to display for given state and details.
+ */
+ mw.Upload.BookletLayout.prototype.getErrorMessageForStateDetails =
function ( state, stateDetails ) {
+ var message,
+ error = stateDetails.error,
+ warnings = stateDetails.upload &&
stateDetails.upload.warnings;
+
+ if ( state === mw.Upload.State.ERROR ) {
+ message = mw.message( 'api-error-' + error.code );
+ if ( !message.exists() ) {
+ message = mw.message( 'api-error-unknownerror',
JSON.stringify( stateDetails ) );
+ }
+ return new OO.ui.Error(
+ $( '<p>' ).html(
+ message.parse()
+ ),
+ { recoverable: false }
+ );
+ }
+
+ if ( state === mw.Upload.State.WARNING ) {
+ // We could get more than one of these errors, these
are in order
+ // of importance. For example fixing the thumbnail like
file name
+ // won't help the fact that the file already exists.
+ if ( warnings.stashfailed !== undefined ) {
+ return new OO.ui.Error(
+ $( '<p>' ).html(
+ mw.message(
'api-error-stashfailed' ).parse()
+ ),
+ { recoverable: false }
+ );
+ } else if ( warnings.exists !== undefined ) {
+ return new OO.ui.Error(
+ $( '<p>' ).html(
+ mw.message( 'filepageexists',
warnings.exists ).parse()
+ ),
+ { recoverable: false }
+ );
+ } else if ( warnings.duplicate !== undefined ) {
+ return new OO.ui.Error(
+ $( '<p>' ).html(
+ mw.message( 'fileexists',
warnings.duplicate[ 0 ] ).parse()
+ ),
+ { recoverable: false }
+ );
+ } else if ( warnings[ 'thumb-name' ] !== undefined ) {
+ return new OO.ui.Error(
+ $( '<p>' ).html(
+ mw.message(
'filename-thumb-name' ).parse()
+ ),
+ { recoverable: false }
+ );
+ } else if ( warnings[ 'bad-prefix' ] !== undefined ) {
+ return new OO.ui.Error(
+ $( '<p>' ).html(
+ mw.message(
'filename-bad-prefix', warnings[ 'bad-prefix' ] ).parse()
+ ),
+ { recoverable: false }
+ );
+ } else if ( warnings[ 'duplicate-archive' ] !==
undefined ) {
+ return new OO.ui.Error(
+ $( '<p>' ).html(
+ mw.message(
'api-error-duplicate-archive', warnings[ 'duplicate-archive' ] ).parse()
+ ),
+ { recoverable: false }
+ );
+ } else if ( warnings.badfilename !== undefined ) {
+ // Change the name if the current name isn't
acceptable
+ // TODO This might not really be the best place
to do this
+ this.filenameWidget.setValue(
warnings.badfilename );
+ return new OO.ui.Error(
+ $( '<p>' ).html(
+ mw.message( 'badfilename',
warnings.badfilename ).parse()
+ )
+ );
+ } else {
+ return new OO.ui.Error(
+ $( '<p>' ).html(
+ // Let's get all the help we
can if we can't pin point the error
+ mw.message(
'api-error-unknown-warning', JSON.stringify( stateDetails ) ).parse()
+ ),
+ { recoverable: false }
+ );
+ }
+ }
+ };
+
/* Form renderers */
/**
diff --git a/resources/src/mediawiki/mediawiki.Upload.js
b/resources/src/mediawiki/mediawiki.Upload.js
index 007c855..1432912 100644
--- a/resources/src/mediawiki/mediawiki.Upload.js
+++ b/resources/src/mediawiki/mediawiki.Upload.js
@@ -191,7 +191,7 @@
* Sets the state and state details (if any) of the upload.
*
* @param {mw.Upload.State} state
- * @param {string|Object} stateDetails
+ * @param {Object} stateDetails
*/
UP.setState = function ( state, stateDetails ) {
this.state = state;
@@ -254,8 +254,13 @@
upload.setState( Upload.State.UPLOADED );
upload.imageinfo = result.upload.imageinfo;
return result;
- }, function () {
- upload.setState( Upload.State.ERROR );
+ }, function ( errorCode, result ) {
+ if ( result && result.upload && result.upload.warnings
) {
+ upload.setState( Upload.State.WARNING, result );
+ } else {
+ upload.setState( Upload.State.ERROR, result );
+ }
+ return $.Deferred().reject( errorCode, result );
} );
};
@@ -282,8 +287,13 @@
} ).then( function ( finishStash ) {
upload.setState( Upload.State.STASHED );
return finishStash;
- }, function ( result ) {
- upload.setState( Upload.State.ERROR, mw.message(
'api-error-' + result ) );
+ }, function ( errorCode, result ) {
+ if ( result && result.upload && result.upload.warnings
) {
+ upload.setState( Upload.State.WARNING, result );
+ } else {
+ upload.setState( Upload.State.ERROR, result );
+ }
+ return $.Deferred().reject( errorCode, result );
} );
return this.stashPromise;
@@ -313,21 +323,13 @@
upload.setState( Upload.State.UPLOADED );
upload.imageinfo = result.upload.imageinfo;
return result;
- }, function ( result ) {
- // Errors are strings that can be used to get
error message
- if ( typeof result === 'string' ) {
- upload.setState( Upload.State.ERROR,
mw.message( 'api-error-' + result ) );
- return;
- }
-
- // Warnings come in the form of objects
- if ( $.isPlainObject( result ) ) {
+ }, function ( errorCode, result ) {
+ if ( result && result.upload &&
result.upload.warnings ) {
upload.setState( Upload.State.WARNING,
result );
- return;
+ } else {
+ upload.setState( Upload.State.ERROR,
result );
}
-
- // Throw an empty error if we can't figure it
out
- upload.setState( Upload.State.ERROR );
+ return $.Deferred().reject( errorCode, result );
} );
} );
};
--
To view, visit https://gerrit.wikimedia.org/r/244512
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: I4c0f619a4e483cca296c2fa2907ed1f81a99fdd6
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/core
Gerrit-Branch: master
Gerrit-Owner: Bartosz Dziewoński <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits