' ).addClass( 'mwe-upwiz-file-status-line' )
)
);
$( this.dataDiv ).append(
this.$form,
this.submittingDiv
).morphCrossfader();
$( this.div ).append(
this.thumbnailDiv,
this.dataDiv
);
uri = new mw.Uri( location.href, { overrideKeys: true } );
if ( mw.UploadWizard.config.defaults.caption || uri.query.captionlang ) {
this.captionsDetails.setSerialized( {
inputs: [
{
language: uri.query.captionlang ?
uw.SingleLanguageInputWidget.static.getClosestAllowedLanguage( uri.query.captionlang ) :
uw.SingleLanguageInputWidget.static.getDefaultLanguage(),
text: mw.UploadWizard.config.defaults.caption || ''
}
]
} );
}
if ( mw.UploadWizard.config.defaults.description || uri.query.descriptionlang ) {
this.descriptionsDetails.setSerialized( {
inputs: [
{
language: uri.query.descriptionlang ?
uw.SingleLanguageInputWidget.static.getClosestAllowedLanguage( uri.query.descriptionlang ) :
uw.SingleLanguageInputWidget.static.getDefaultLanguage(),
text: mw.UploadWizard.config.defaults.description || ''
}
]
} );
}
this.populate();
this.interfaceBuilt = true;
if ( this.savedSerialData ) {
this.setSerialized( this.savedSerialData );
this.savedSerialData = undefined;
}
},
/*
* Append the div for this details object to the DOM.
* We need to ensure that we add divs in the right order
* (the order in which the user selected files).
*
* Will only append once.
*/
attach: function () {
var $window = $( window ),
details = this;
function maybeBuild() {
if ( !this.interfaceBuilt && $window.scrollTop() + $window.height() + 1000 >= details.div.offset().top ) {
details.buildInterface();
$window.off( 'scroll', maybeBuild );
}
}
if ( !this.isAttached ) {
$( this.containerDiv ).append( this.div );
if ( $window.scrollTop() + $window.height() + 1000 >= this.div.offset().top ) {
this.buildInterface();
} else {
$window.on( 'scroll', maybeBuild );
}
this.isAttached = true;
}
},
/**
* Get file page title for this upload.
*
* @return {mw.Title|null}
*/
getTitle: function () {
return this.titleDetails.getTitle();
},
/**
* Display error message about multiple uploaded files with the same title specified
*
* @return {mw.UploadWizardDetails}
* @chainable
*/
setDuplicateTitleError: function () {
// TODO This should give immediate response, not only when submitting the form
this.titleDetailsField.setErrors( [ mw.message( 'mwe-upwiz-error-title-duplicate' ) ] );
return this;
},
/**
* @private
*
* @return {uw.FieldLayout[]}
*/
getAllFields: function () {
return [].concat(
this.mainFields,
this.upload.deedChooser.deed ? this.upload.deedChooser.deed.getFields() : [],
this.campaignDetailsFields
);
},
/**
* Check all the fields for validity.
*
* @return {jQuery.Promise} Promise resolved with multiple array arguments, each containing a
* list of error messages for a single field. If API requests necessary to check validity
* fail, the promise may be rejected. The form is valid if the promise is resolved with all
* empty arrays.
*/
getErrors: function () {
return $.when.apply( $, this.getAllFields().map( function ( fieldLayout ) {
return fieldLayout.fieldWidget.getErrors();
} ) );
},
/**
* Check all the fields for warnings.
*
* @return {jQuery.Promise} Same as #getErrors
*/
getWarnings: function () {
return $.when.apply( $, this.getAllFields().map( function ( fieldLayout ) {
return fieldLayout.fieldWidget.getWarnings();
} ) );
},
/**
* Check all the fields for errors and warnings and display them in the UI.
*
* @param {boolean} thorough True to perform a thorough validity check. Defaults to false for a fast on-change check.
* @return {jQuery.Promise} Combined promise of all fields' validation results.
*/
checkValidity: function ( thorough ) {
var fields = this.getAllFields();
return $.when.apply( $, fields.map( function ( fieldLayout ) {
// Update any error/warning messages
return fieldLayout.checkValidity( thorough );
} ) );
},
/**
* Get a thumbnail caption for this upload (basically, the first caption).
*
* @return {string}
*/
getThumbnailCaption: function () {
var captions = [];
if ( mw.UploadWizard.config.wikibase.enabled ) {
captions = this.captionsDetails.getSerialized().inputs;
} else {
captions = this.descriptionsDetails.getSerialized().inputs;
}
if ( captions.length > 0 ) {
return mw.Escaper.escapeForTemplate( captions[ 0 ].text.trim() );
} else {
return '';
}
},
/**
* toggles whether we use the 'macro' deed or our own
*/
useCustomDeedChooser: function () {
this.customDeedChooser = true;
this.deedChooserDetails.useCustomDeedChooser( this.upload );
},
/**
* Pull some info into the form ( for instance, extracted from EXIF, desired filename )
*/
populate: function () {
var thumbnailDiv = this.thumbnailDiv;
this.upload.getThumbnail().done( function ( thumb ) {
mw.UploadWizard.placeThumbnail( thumbnailDiv, thumb );
} );
this.prefillDate();
this.prefillAuthor();
this.prefillTitle();
this.prefillDescription();
this.prefillLocation();
},
/**
* Check if we got an EXIF date back and enter it into the details
* XXX We ought to be using date + time here...
* EXIF examples tend to be in ISO 8601, but the separators are sometimes things like colons, and they have lots of trailing info
* (which we should actually be using, such as time and timezone)
*/
prefillDate: function () {
var dateObj, metadata, dateTimeRegex, matches, dateStr, saneTime,
dateMode = 'calendar',
yyyyMmDdRegex = /^(\d\d\d\d)[:/-](\d\d)[:/-](\d\d)\D.*/,
timeRegex = /\D(\d\d):(\d\d):(\d\d)/;
// XXX surely we have this function somewhere already
function pad( n ) {
return ( n < 10 ? '0' : '' ) + String( n );
}
function getSaneTime( date ) {
var str = '';
str += pad( date.getHours() ) + ':';
str += pad( date.getMinutes() ) + ':';
str += pad( date.getSeconds() );
return str;
}
if ( this.upload.imageinfo.metadata ) {
metadata = this.upload.imageinfo.metadata;
$.each( [ 'datetimeoriginal', 'datetimedigitized', 'datetime', 'date' ], function ( i, propName ) {
var matches, timeMatches,
dateInfo = metadata[ propName ];
if ( dateInfo ) {
matches = dateInfo.trim().match( yyyyMmDdRegex );
if ( matches ) {
timeMatches = dateInfo.trim().match( timeRegex );
if ( timeMatches ) {
dateObj = new Date( parseInt( matches[ 1 ], 10 ),
parseInt( matches[ 2 ], 10 ) - 1,
parseInt( matches[ 3 ], 10 ),
parseInt( timeMatches[ 1 ], 10 ),
parseInt( timeMatches[ 2 ], 10 ),
parseInt( timeMatches[ 3 ], 10 ) );
} else {
dateObj = new Date( parseInt( matches[ 1 ], 10 ),
parseInt( matches[ 2 ], 10 ) - 1,
parseInt( matches[ 3 ], 10 ) );
}
return false; // break from $.each
}
}
} );
}
// If we don't have EXIF lets try other sources - Flickr
if ( dateObj === undefined && this.upload.file !== undefined && this.upload.file.date !== undefined ) {
dateTimeRegex = /^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/;
matches = this.upload.file.date.match( dateTimeRegex );
if ( matches ) {
this.dateDetails.setSerialized( {
mode: dateMode,
value: this.upload.file.date
} );
return;
}
}
// if we don't have EXIF or other metadata, just don't put a date in.
// XXX if we have FileAPI, it might be clever to look at file attrs, saved
// in the upload object for use here later, perhaps
if ( dateObj === undefined ) {
return;
}
dateStr = dateObj.getFullYear() + '-' + pad( dateObj.getMonth() + 1 ) + '-' + pad( dateObj.getDate() );
// Add the time
// If the date but not the time is set in EXIF data, we'll get a bogus
// time value of '00:00:00'.
// FIXME: Check for missing time value earlier rather than blacklisting
// a potentially legitimate time value.
saneTime = getSaneTime( dateObj );
if ( saneTime !== '00:00:00' ) {
dateStr += ' ' + saneTime;
// Switch to freeform date field. DateInputWidget (with calendar) handles dates only, not times.
dateMode = 'arbitrary';
}
// ok by now we should definitely have a dateObj and a date string
this.dateDetails.setSerialized( {
mode: dateMode,
value: dateStr
} );
},
/**
* Set the title of the thing we just uploaded, visibly
*/
prefillTitle: function () {
this.titleDetails.setSerialized( {
title: this.upload.title.getNameText()
} );
},
/**
* Prefill the image description if we have a description
*
* Note that this is not related to specifying the description from the query
* string (that happens earlier). This is for when we have retrieved a
* description from an upload_by_url upload (e.g. Flickr transfer)
* or from the metadata.
*/
prefillDescription: function () {
var m, descText;
if (
this.descriptionsDetails.getWikiText() === '' &&
this.upload.file !== undefined
) {
m = this.upload.imageinfo.metadata;
descText = this.upload.file.description ||
( m && m.imagedescription && m.imagedescription[ 0 ] && m.imagedescription[ 0 ].value );
if ( descText ) {
// strip out any HTML tags
descText = descText.replace( /<[^>]+>/g, '' );
// & and " are escaped by Flickr, so we need to unescape
descText = descText.replace( /&/g, '&' ).replace( /"/g, '"' );
this.descriptionsDetails.setSerialized( {
inputs: [
{
// The language is probably wrong in many cases...
language: uw.DescriptionDetailsWidget.static.getClosestAllowedLanguage( mw.config.get( 'wgContentLanguage' ) ),
text: descText.trim()
}
]
} );
}
}
},
/**
* Prefill location input from image info and metadata
*
* As of MediaWiki 1.18, the exif parser translates the rational GPS data tagged by the camera
* to decimal format. Let's just use that.
*/
prefillLocation: function () {
var dir,
m = this.upload.imageinfo.metadata,
modified = false,
values = {};
if ( mw.UploadWizard.config.defaults.lat ) {
values.latitude = mw.UploadWizard.config.defaults.lat;
modified = true;
}
if ( mw.UploadWizard.config.defaults.lon ) {
values.longitude = mw.UploadWizard.config.defaults.lon;
modified = true;
}
if ( mw.UploadWizard.config.defaults.heading ) {
values.heading = mw.UploadWizard.config.defaults.heading;
modified = true;
}
if ( m ) {
dir = m.gpsimgdirection || m.gpsdestbearing;
if ( dir ) {
if ( dir.match( /^\d+\/\d+$/ ) !== null ) {
// Apparently it can take the form "x/y" instead of
// a decimal value. Mighty silly, but let's save it.
dir = dir.split( '/' );
dir = parseInt( dir[ 0 ], 10 ) / parseInt( dir[ 1 ], 10 );
}
values.heading = dir;
modified = true;
}
// Prefill useful stuff only
if ( Number( m.gpslatitude ) && Number( m.gpslongitude ) ) {
values.latitude = m.gpslatitude;
values.longitude = m.gpslongitude;
modified = true;
} else if (
this.upload.file &&
this.upload.file.location &&
this.upload.file.location.latitude &&
this.upload.file.location.longitude
) {
values.latitude = this.upload.file.location.latitude;
values.longitude = this.upload.file.location.longitude;
modified = true;
}
}
this.locationInput.setSerialized( values );
if ( modified ) {
this.$form.find( '.mwe-more-details' )
.data( 'mw-collapsible' ).expand();
}
},
/**
* Prefill author (such as can be determined) from image info and metadata
* XXX user pref?
*/
prefillAuthor: function () {
if ( this.upload.imageinfo.metadata && this.upload.imageinfo.metadata.author ) {
$( this.authorInput ).val( this.upload.imageinfo.metadata.author );
}
},
/**
* Get a machine-readable representation of the current state of the upload details. It can be
* passed to #setSerialized to restore this state (or to set it for another instance of the same
* class).
*
* Note that this doesn't include custom deed's state.
*
* @return {Object.
}
*/
getSerialized: function () {
if ( !this.interfaceBuilt ) {
// We don't have the interface yet, but it'll get filled out as
// needed.
return;
}
return {
title: this.titleDetails.getSerialized(),
caption: this.captionsDetails.getSerialized(),
description: this.descriptionsDetails.getSerialized(),
date: this.dateDetails.getSerialized(),
categories: this.categoriesDetails.getSerialized(),
location: this.locationInput.getSerialized(),
other: this.otherDetails.getSerialized(),
campaigns: this.campaignDetailsFields.map( function ( field ) {
return field.fieldWidget.getSerialized();
} ),
deed: this.deedChooserDetails.getSerialized()
};
},
/**
* Set the state of this widget from machine-readable representation, as returned by
* #getSerialized.
*
* Fields from the representation can be omitted to keep the current value.
*
* @param {Object.} [serialized]
*/
setSerialized: function ( serialized ) {
var i;
if ( !this.interfaceBuilt ) {
// There's no interface yet! Don't load the data, just keep it
// around.
if ( serialized === undefined ) {
// Note: This will happen if we "undo" a copy operation while
// some of the details interfaces aren't loaded.
this.savedSerialData = undefined;
} else {
this.savedSerialData = $.extend( true,
this.savedSerialData || {},
serialized
);
}
return;
}
if ( serialized === undefined ) {
// This is meaningless if the interface is already built.
return;
}
if ( serialized.title ) {
this.titleDetails.setSerialized( serialized.title );
}
if ( serialized.caption ) {
this.captionsDetails.setSerialized( serialized.caption );
}
if ( serialized.description ) {
this.descriptionsDetails.setSerialized( serialized.description );
}
if ( serialized.date ) {
this.dateDetails.setSerialized( serialized.date );
}
if ( serialized.categories ) {
this.categoriesDetails.setSerialized( serialized.categories );
}
if ( serialized.location ) {
this.locationInput.setSerialized( serialized.location );
}
if ( serialized.other ) {
this.otherDetails.setSerialized( serialized.other );
}
if ( serialized.campaigns ) {
for ( i = 0; i < this.campaignDetailsFields.length; i++ ) {
this.campaignDetailsFields[ i ].fieldWidget.setSerialized( serialized.campaigns[ i ] );
}
}
if ( serialized.deed ) {
this.deedChooserDetails.setSerialized( serialized.deed );
}
},
/**
* Convert entire details for this file into wikiText, which will then be posted to the file
*
* This function assumes that all input is valid.
*
* @return {string} wikitext representing all details
*/
getWikiText: function () {
var deed, info, key,
information,
wikiText = '';
// https://commons.wikimedia.org/wiki/Template:Information
// can we be more slick and do this with maps, applys, joins?
information = {
// {{lang|description in lang}}* required
description: '',
// YYYY, YYYY-MM, or YYYY-MM-DD required - use jquery but allow editing, then double check for sane date.
date: '',
// {{own}} or wikitext optional
source: '',
// any wikitext, but particularly {{Creator:Name Surname}} required
author: '',
// leave blank unless OTRS pending; by default will be "see below" optional
permission: '',
// pipe separated list, other versions optional
'other versions': ''
};
information.description = this.descriptionsDetails.getWikiText();
$.each( this.campaignDetailsFields, function ( i, layout ) {
information.description += layout.fieldWidget.getWikiText();
} );
information.date = this.dateDetails.getWikiText();
deed = this.upload.deedChooser.deed;
information.source = deed.getSourceWikiText( this.upload );
information.author = deed.getAuthorWikiText( this.upload );
info = '';
for ( key in information ) {
if ( information.hasOwnProperty( key ) ) {
info += '|' + key.replace( /:/g, '_' );
info += '=' + mw.Escaper.escapeForTemplate( information[ key ] ) + '\n';
}
}
wikiText += '=={{int:filedesc}}==\n';
wikiText += '{{Information\n' + info + '}}\n';
wikiText += this.locationInput.getWikiText() + '\n';
// add an "anything else" template if needed
wikiText += this.otherDetails.getWikiText() + '\n\n';
// add licensing information
wikiText += '\n=={{int:license-header}}==\n';
wikiText += deed.getLicenseWikiText( this.upload ) + '\n\n';
if ( mw.UploadWizard.config.autoAdd.wikitext !== undefined ) {
wikiText += mw.UploadWizard.config.autoAdd.wikitext + '\n';
}
// add parameters for list callback bot
// this cue will be used to supplement a wiki page with an image thumbnail
if ( $( '#imgPicker' + this.upload.index ).prop( 'checked' ) ) {
wikiText += '\n\n';
}
// categories
wikiText += '\n' + this.categoriesDetails.getWikiText();
// sanitize wikitext if TextCleaner is defined (MediaWiki:TextCleaner.js)
if ( typeof window.TextCleaner !== 'undefined' && typeof window.TextCleaner.sanitizeWikiText === 'function' ) {
wikiText = window.TextCleaner.sanitizeWikiText( wikiText, true );
}
// remove too many newlines in a row
wikiText = wikiText.replace( /\n{3,}/g, '\n\n' );
return wikiText;
},
/**
* @return {jQuery.Promise}
*/
submit: function () {
var details = this,
wikitext, captions, promise;
$( 'form', this.containerDiv ).submit();
this.upload.title = this.getTitle();
this.upload.state = 'submitting-details';
this.setStatus( mw.message( 'mwe-upwiz-submitting-details' ).text() );
this.showIndicator( 'progress' );
wikitext = this.getWikiText();
promise = this.submitWikiText( wikitext );
captions = this.captionsDetails.getValues();
if ( mw.UploadWizard.config.wikibase.enabled && Object.keys( captions ).length > 0 ) {
promise = promise
.then( function ( result ) {
// after having submitted the upload, fetch the entity id from page_props
// we might fail to retrieve the prop if it has not yet been created,
// so try at least 20 times...
var status = mw.message( 'mwe-upwiz-submitting-captions', Object.keys( captions ).length ),
title = mw.Title.makeTitle( 6, result.upload.filename ),
callable = details.getPageProp.bind( details, title, 'mediainfo_entity' );
details.setStatus( status.text() );
return details.attemptExecute( callable, 20 );
} )
// submit captions to wikibase
.then( this.submitCaptions.bind( this, captions ) )
.catch( function ( code, result ) {
var languageCodes = Object.keys( captions ),
allLanguages = mw.UploadWizard.config.uwLanguages,
message = mw.message(
'mwe-upwiz-error-submit-captions',
details.upload.imageinfo.canonicaltitle,
result.errors[ 0 ].html,
languageCodes.length
).parse(),
$list,
i;
// add captions as well, so they can easily be copied over
for ( i = 0; i < languageCodes.length; i++ ) {
$list = $( '' );
$list.append( $( '- ' ).text( allLanguages[ languageCodes[ i ] ] ) );
$list.append( $( '
- ' ).text( captions[ languageCodes[ i ] ] ) );
}
message += $list[ 0 ].outerHTML;
details.upload.state = 'error';
details.showError( 'caption-fail', message );
return $.Deferred().reject( code, result ).promise();
} );
}
return promise.then( function () {
details.showIndicator( 'uploaded' );
details.setStatus( mw.message( 'mwe-upwiz-published' ).text() );
} );
},
/**
* Attempt to resolve a promise (a couple of times, with an increasing delay).
*
* @param {function} callable
* @param {number} attempts
* @return {jQuery.Promise}
*/
attemptExecute: function ( callable, attempts ) {
var deferred = $.Deferred(),
attempt = 0,
retry;
attempts = attempts || 10;
retry = function () {
callable().then(
deferred.resolve,
function () {
attempt++;
if ( attempt >= attempts ) {
// don't keep trying forever; just give up if we fail a few times
deferred.reject.apply( deferred, arguments );
} else {
// try again on failure, with an increasing timeout...
setTimeout( retry, 2000 * attempt );
}
}
);
};
retry();
return deferred.promise();
},
/**
* @param {mw.Title} title
* @param {string} prop
* @return {jQuery.Promise}
*/
getPageProp: function ( title, prop ) {
return this.upload.api.get( {
action: 'query',
prop: 'pageprops',
titles: title.getPrefixedDb()
} ).then( function ( result ) {
var props, message;
if ( result.query.pages[ 0 ].missing ) {
// page doesn't exist (yet)
message = mw.message( 'mwe-upwiz-error-pageprops-missing-page' ).parse();
return $.Deferred().reject( 'pageprops-missing-page', { errors: [ { html: message } ] } ).promise();
}
props = result.query.pages[ 0 ].pageprops;
if ( !props || !( prop in props ) ) {
// prop doesn't exist (yet)
message = mw.message( 'mwe-upwiz-error-pageprops-missing-prop' ).parse();
return $.Deferred().reject( 'pageprops-missing-prop', { errors: [ { html: message } ] } ).promise();
}
return props[ prop ];
} );
},
/**
* Post wikitext as edited here, to the file
*
* This function is only called if all input seems valid (which doesn't mean that we can't get
* an error, see #processError).
*
* @param {string} wikiText
* @return {jQuery.Promise}
*/
submitWikiText: function ( wikiText ) {
var params,
tags = [ 'uploadwizard' ];
this.firstPoll = ( new Date() ).getTime();
if ( this.upload.file.source ) {
tags.push( 'uploadwizard-' + this.upload.file.source );
}
params = {
action: 'upload',
filekey: this.upload.fileKey,
filename: this.getTitle().getMain(),
comment: 'User created page with ' + mw.UploadWizard.userAgent,
tags: mw.UploadWizard.config.CanAddTags ? tags : [],
// we can ignore upload warnings here, we've already checked
// when stashing the file
// not ignoring warnings would prevent us from uploading a file
// that is a duplicate of something in a foreign repo
ignorewarnings: true,
text: wikiText
};
// Only enable async publishing if file is larger than 10MiB
if ( this.upload.transportWeight > 10 * 1024 * 1024 ) {
params.async = true;
}
params.text = this.getWikiText();
return this.submitWikiTextInternal( params );
},
/**
* @param {object} captions {: } map
* @param {string} entityId
* @return {jQuery.Promise}
*/
submitCaptions: function ( captions, entityId ) {
var languages = Object.keys( captions ),
// `promises` will hold all promises for all captions;
// prefilling with a bogus promise to ensure $.when always
// resolves with an array of multiple results (if there's
// just 1, it would otherwise have just that one's arguments,
// instead of a multi-dimensional array of results)
promises = [ $.Deferred().resolve().promise() ],
callable, promise, language, text, i;
for ( i = 0; i < languages.length; i++ ) {
language = languages[ i ];
text = captions[ language ];
callable = this.submitCaption.bind( this, entityId, language, text );
promise = this.attemptExecute( callable, 3 );
promises.push( promise );
}
return $.when.apply( $, promises );
},
/**
* @param {string} id
* @param {string} language
* @param {string} value
* @return {jQuery.Promise}
*/
submitCaption: function ( id, language, value ) {
var config = mw.UploadWizard.config.wikibase,
params = {
action: 'wbsetlabel',
id: id,
language: language,
value: value
},
ajaxOptions = { url: config.api };
if ( !config.enabled ) {
return $.Deferred().reject().promise();
}
return this.upload.api.postWithEditToken( params, ajaxOptions );
},
/**
* Perform the API call with given parameters (which is expected to publish this file) and
* handle the result.
*
* @param {Object} params API call parameters
* @return {jQuery.Promise}
*/
submitWikiTextInternal: function ( params ) {
var details = this,
apiPromise = this.upload.api.postWithEditToken( params );
return apiPromise
// process the successful (in terms of HTTP status...) API call first:
// there may be warnings or other issues with the upload that need
// to be dealt with
.then( this.validateWikiTextSubmitResult.bind( this, params ) )
// making it here means the upload is a success, or it would've been
// rejected by now (either by HTTP status code, or in validateWikiTextSubmitResult)
.then( function ( result ) {
details.upload.extractImageInfo( result.upload.imageinfo );
details.upload.thisProgress = 1.0;
details.upload.state = 'complete';
return result;
} )
// uh-oh - something went wrong!
.catch( function ( code, result ) {
uw.eventFlowLogger.logApiError( 'details', result );
details.upload.state = 'error';
details.processError( code, result );
return $.Deferred().reject( code, result );
} )
.promise( { abort: apiPromise.abort } );
},
/**
* Validates the result of a submission & returns a resolved promise with
* the API response if all went well, or rejects with error code & error
* message as you would expect from failed mediawiki API calls.
*
* @param {Object} params What we passed to the API that caused this response.
* @param {Object} result API result of an upload or status check.
* @return {jQuery.Promise}
*/
validateWikiTextSubmitResult: function ( params, result ) {
var wx, warningsKeys, existingFile, existingFileUrl, existingFileExt, ourFileExt, code, message,
details = this,
warnings = null,
ignoreTheseWarnings = false,
deferred = $.Deferred();
if ( result && result.upload && result.upload.result === 'Poll' ) {
// if async publishing takes longer than 10 minutes give up
if ( ( ( new Date() ).getTime() - this.firstPoll ) > 10 * 60 * 1000 ) {
return deferred.reject( 'server-error', { errors: [ {
code: 'server-error',
html: 'Unknown server error'
} ] } );
} else {
if ( result.upload.stage === undefined ) {
return deferred.reject( 'no-stage', { errors: [ {
code: 'no-stage',
html: 'Unable to check file\'s status'
} ] } );
} else {
// Messages that can be returned:
// * mwe-upwiz-queued
// * mwe-upwiz-publish
// * mwe-upwiz-assembling
this.setStatus( mw.message( 'mwe-upwiz-' + result.upload.stage ).text() );
setTimeout( function () {
if ( details.upload.state !== 'aborted' ) {
details.submitWikiTextInternal( {
action: 'upload',
checkstatus: true,
filekey: details.upload.fileKey
} ).then( deferred.resolve, deferred.reject );
} else {
deferred.resolve( 'aborted' );
}
}, 3000 );
return deferred.promise();
}
}
}
if ( result && result.upload && result.upload.warnings ) {
warnings = result.upload.warnings;
}
if ( warnings && warnings.exists ) {
existingFile = warnings.exists;
} else if ( warnings && warnings[ 'exists-normalized' ] ) {
existingFile = warnings[ 'exists-normalized' ];
existingFileExt = mw.Title.normalizeExtension( existingFile.split( '.' ).pop() );
ourFileExt = mw.Title.normalizeExtension( this.getTitle().getExtension() );
if ( existingFileExt !== ourFileExt ) {
delete warnings[ 'exists-normalized' ];
ignoreTheseWarnings = true;
}
}
if ( warnings && warnings[ 'was-deleted' ] ) {
delete warnings[ 'was-deleted' ];
ignoreTheseWarnings = true;
}
for ( wx in warnings ) {
if ( warnings.hasOwnProperty( wx ) ) {
// if there are other warnings, deal with those first
ignoreTheseWarnings = false;
}
}
if ( result && result.upload && result.upload.imageinfo ) {
return $.Deferred().resolve( result );
} else if ( ignoreTheseWarnings ) {
params.ignorewarnings = 1;
return this.submitWikiTextInternal( params );
} else if ( result && result.upload && result.upload.warnings ) {
if ( warnings.thumb || warnings[ 'thumb-name' ] ) {
code = 'error-title-thumbnail';
message = mw.message( 'mwe-upwiz-error-title-thumbnail' ).parse();
} else if ( warnings.badfilename ) {
code = 'title-invalid';
message = mw.message( 'mwe-upwiz-error-title-invalid' ).parse();
} else if ( warnings[ 'bad-prefix' ] ) {
code = 'title-senselessimagename';
message = mw.message( 'mwe-upwiz-error-title-senselessimagename' ).parse();
} else if ( existingFile ) {
existingFileUrl = mw.config.get( 'wgServer' ) + mw.Title.makeTitle( NS_FILE, existingFile ).getUrl();
code = 'api-warning-exists';
message = mw.message( 'mwe-upwiz-api-warning-exists', existingFileUrl ).parse();
} else if ( warnings.duplicate ) {
code = 'upload-error-duplicate';
message = mw.message( 'mwe-upwiz-upload-error-duplicate' ).parse();
} else if ( warnings[ 'duplicate-archive' ] !== undefined ) {
// warnings[ 'duplicate-archive' ] may be '' (empty string) for revdeleted files
if ( this.upload.handler.isIgnoredWarning( 'duplicate-archive' ) ) {
// We already told the interface to ignore this warning, so
// let's steamroll over it and re-call this handler.
params.ignorewarnings = true;
return this.submitWikiTextInternal( params );
} else {
// This should _never_ happen, but just in case....
code = 'upload-error-duplicate-archive';
message = mw.message( 'mwe-upwiz-upload-error-duplicate-archive' ).parse();
}
} else {
warningsKeys = [];
$.each( warnings, function ( key ) {
warningsKeys.push( key );
} );
code = 'unknown-warning';
message = mw.message( 'api-error-unknown-warning', warningsKeys.join( ', ' ) ).parse();
}
return $.Deferred().reject( code, { errors: [ { html: message } ] } );
} else {
return $.Deferred().reject( 'this-info-missing', result );
}
},
/**
* Create a recoverable error -- show the form again, and highlight the problematic field.
*
* @param {string} code
* @param {string} html Error message to show.
*/
recoverFromError: function ( code, html ) {
uw.eventFlowLogger.logError( 'details', { code: code, message: html } );
this.upload.state = 'recoverable-error';
this.dataDiv.morphCrossfade( '.detailsForm' );
this.titleDetailsField.setErrors( [ { code: code, html: html } ] );
},
/**
* Show error state, possibly using a recoverable error form
*
* @param {string} code Error code
* @param {string} html Error message
*/
showError: function ( code, html ) {
uw.eventFlowLogger.logError( 'details', { code: code, message: html } );
this.showIndicator( 'error' );
this.setStatus( html );
},
/**
* Decide how to treat various errors
*
* @param {string} code Error code
* @param {Object} result Result from ajax call
*/
processError: function ( code, result ) {
var recoverable = [
'abusefilter-disallowed',
'abusefilter-warning',
'spamblacklist',
'fileexists-shared-forbidden',
'protectedpage',
'titleblacklist-forbidden',
// below are not actual API errors, but recoverable warnings that have
// been discovered in validateWikiTextSubmitResult and fabricated to resemble
// API errors and end up here to be dealt with
'error-title-thumbnail',
'title-invalid',
'title-senselessimagename',
'api-warning-exists',
'upload-error-duplicate',
'upload-error-duplicate',
'upload-error-duplicate-archive',
'unknown-warning'
];
if ( code === 'badtoken' ) {
this.api.badToken( 'csrf' );
// TODO Automatically try again instead of requiring the user to bonk the button
}
if ( code === 'ratelimited' ) {
// None of the remaining uploads is going to succeed, and every failed one is going to
// ping the rate limiter again.
this.upload.wizard.steps.details.queue.abortExecuting();
} else if ( code === 'http' && result && result.exception === 'abort' ) {
// This upload has just been aborted because an earlier one got the 'ratelimited' error.
// This could potentially also come up when an upload is removed by the user, but in that
// case the UI is invisible anyway, so whatever.
code = 'ratelimited';
}
if ( recoverable.indexOf( code ) > -1 ) {
this.recoverFromError( code, result.errors[ 0 ].html );
return;
}
this.showError( code, result.errors[ 0 ].html );
},
setStatus: function ( s ) {
this.div.find( '.mwe-upwiz-file-status-line' ).html( s ).show();
},
showIndicator: function ( statusStr ) {
this.div.find( '.mwe-upwiz-file-indicator' )
.show()
.removeClass( 'mwe-upwiz-status-progress mwe-upwiz-status-error mwe-upwiz-status-uploaded' )
.addClass( 'mwe-upwiz-status-' + statusStr );
},
setVisibleTitle: function ( s ) {
$( this.submittingDiv )
.find( '.mwe-upwiz-visible-file-filename-text' )
.text( s );
}
};
}( mediaWiki, mediaWiki.uploadWizard, jQuery, OO ) );