resources/handlers/mw.ApiUploadHandler.js
( function () {
var NS_FILE = mw.config.get( 'wgNamespaceIds' ).file;
/**
* @class
* @param {mw.UploadWizardUpload} upload
* @param {mw.Api} api
*/
mw.ApiUploadHandler = function ( upload, api ) {
this.upload = upload;
this.api = api;
this.ignoreWarnings = [
// we ignore these warnings, because the title is not our final title.
'page-exists',
'exists',
'exists-normalized',
'was-deleted',
'badfilename',
'bad-prefix'
];
this.upload.on( 'remove-upload', this.abort.bind( this ) );
};
/**
* @method
* @abstract
*/
mw.ApiUploadHandler.prototype.abort = null;
/**
* @method
* @abstract
* @return {jQuery.Promise}
*/
mw.ApiUploadHandler.prototype.submit = null;
/**
* @return {jQuery.Promise}
*/
mw.ApiUploadHandler.prototype.start = function () {
return this.submit().then(
this.setTransported.bind( this ),
this.setTransportError.bind( this )
);
};
/**
* Process a successful upload.
*
* @param {Object} result
*/
mw.ApiUploadHandler.prototype.setTransported = function ( result ) {
var code;
if ( result.upload && result.upload.warnings ) {
for ( code in result.upload.warnings ) {
if ( !this.isIgnoredWarning( code ) ) {
this.setTransportWarning( code, result );
return;
}
}
}
if ( !result.upload || result.upload.result !== 'Success' ) {
this.setError( 'unknown', mw.message( 'unknown-error' ).parse() );
return;
}
if ( !result.upload.imageinfo ) {
this.setError( 'noimageinfo', mw.message( 'api-error-noimageinfo' ).parse() );
return;
}
this.upload.setSuccess( result );
};
/**
* Process an upload with a warning.
*
* @param {string} code The API warning code
* @param {Object} result The API result in parsed JSON form
*/
mw.ApiUploadHandler.prototype.setTransportWarning = function ( code, result ) {
var param, duplicates, links;
switch ( code ) {
case 'duplicate':
duplicates = result.upload.warnings.duplicate;
if ( result.upload.warnings.exists && result.upload.warnings.nochange ) {
// An existing same (nochange) file will not show up as
// duplicate, but it should also be present in order to
// figure out how to process the attempted upload)
duplicates.push( result.upload.warnings.exists );
}
this.processDuplicateError( code, result, result.upload.warnings.duplicate );
return;
case 'nochange':
// This is like 'duplicate', but also the filename is the same, which doesn't matter
if ( result.upload.warnings.exists ) {
links = this.getFileLinks( [ result.upload.warnings.exists ] );
this.setDuplicateError( code, result, links, {}, 1 - Object.keys( links ).length );
}
return;
case 'duplicateversions':
this.setDuplicateOldError( code, result, result.upload.warnings.exists, result.upload.warnings.duplicateversions.length );
return;
case 'duplicate-archive':
this.setDuplicateArchiveError( code, result, result.upload.warnings[ 'duplicate-archive' ] );
return;
default:
param = code;
if ( typeof result.upload.warnings[ code ] === 'string' ) {
// tack the original error code onto the warning message
param += mw.message( 'colon-separator' ).text() + result.upload.warnings[ code ];
}
// we have an unknown warning, so let's say what we know
this.setError( code, mw.message( 'api-error-unknown-warning', param ).parse() );
return;
}
};
mw.ApiUploadHandler.prototype.makeOverrideButton = function () {
return new OO.ui.ButtonWidget( {
label: mw.message( 'mwe-upwiz-override' ).text(),
title: mw.message( 'mwe-upwiz-override-upload' ).text(),
flags: 'progressive',
framed: false
} );
};
/**
* Process an erroneous upload.
*
* @param {string} code The API error code
* @param {Object} result The API result in parsed JSON form
*/
mw.ApiUploadHandler.prototype.setTransportError = function ( code, result ) {
var $extra;
if ( code === 'badtoken' ) {
this.api.badToken( 'csrf' );
// Try again once
if ( this.ignoreWarning( code ) ) {
this.start();
return;
}
}
if ( code === 'abusefilter-warning' ) {
$extra = this.makeOverrideButton().on( 'click', () => {
// No need to ignore the error, AbuseFilter will only return it once
this.start();
} ).$element;
}
this.setError( code, result.errors[ 0 ].html, $extra );
};
/**
* Figure out the source of duplicates (local or foreign) and distribute
* them to the correct function to display the accurate error messages.
*
* @param {string} code
* @param {Object} result
* @param {string[] }duplicates
* @return {jQuery.Promise}
*/
mw.ApiUploadHandler.prototype.processDuplicateError = function ( code, result, duplicates ) {
var files = this.getFileLinks( duplicates ),
unknownAmount = duplicates.length - Object.keys( files ).length;
return this.getDuplicateSource( Object.keys( files ) ).then(
( data ) => {
this.setDuplicateError( code, result, data.local, data.foreign, unknownAmount );
},
() => {
// if anything goes wrong trying to figure out the source of
// duplicates, just move on with local duplicate handling
this.setDuplicateError( code, result, files, {}, unknownAmount );
}
);
};
/**
* @param {string[]} duplicates Array of duplicate filenames
* @return {jQuery.Promise}
*/
mw.ApiUploadHandler.prototype.getDuplicateSource = function ( duplicates ) {
return this.getImageInfo( duplicates, 'url' ).then( ( result ) => {
var local = [],
foreign = [],
normalized = [];
if ( !result.query || !result.query.pages ) {
return $.Deferred().reject();
}
// map of normalized titles, so we can find original title
if ( result.query.normalized ) {
result.query.normalized.forEach( ( data ) => {
normalized[ data.to ] = data.from;
} );
}
Object.keys( result.query.pages ).forEach( ( pageId ) => {
var page = result.query.pages[ pageId ],
title = normalized[ page.title ] || page.title;
if ( page.imagerepository === 'local' ) {
local[ title ] = page.imageinfo[ 0 ].descriptionurl;
} else if ( page.imagerepository !== '' ) {
foreign[ title ] = page.imageinfo[ 0 ].descriptionurl;
}
} );
return $.Deferred().resolve( { local: local, foreign: foreign } );
} );
};
/**
* Helper function to generate existing duplicate errors in a possibly collapsible list.
*
* @param {string} code Warning code, should have matching strings in .i18n.php
* @param {Object} result The API result in parsed JSON form
* @param {Object} localDuplicates Array of [duplicate filenames => local url]
* @param {Object} foreignDuplicates Array of [duplicate filenames => foreign url]
* @param {number} unknownAmount Amount of unknown filenames (e.g. revdeleted)
*/
mw.ApiUploadHandler.prototype.setDuplicateError = function ( code, result, localDuplicates, foreignDuplicates, unknownAmount ) {
var allDuplicates = $.extend( {}, localDuplicates, foreignDuplicates ),
$extra = $( '<div>' ),
$ul = $( '<ul>' ).appendTo( $extra ),
$a,
override,
i;
unknownAmount = unknownAmount || 0;
Object.keys( allDuplicates ).forEach( ( filename ) => {
var href = allDuplicates[ filename ];
$a = $( '<a>' ).text( filename );
$a.attr( { href: href, target: '_blank' } );
$ul.append( $( '<li>' ).append( $a ) );
} );
for ( i = 0; i < unknownAmount; i++ ) {
$a = $( '<em>' ).text( mw.msg( 'mwe-upwiz-deleted-duplicate-unknown-filename' ) );
$ul.append( $( '<li>' ).append( $a ) );
}
if ( allDuplicates.length > 1 ) {
$ul.makeCollapsible( { collapsed: true } );
}
// allow upload to continue if it's only a duplicate of files in a
// foreign repo, not when it's a local dupe
if ( Object.keys( localDuplicates ).length === 0 ) {
override = this.makeOverrideButton().on( 'click', () => {
// mark this warning as ignored & process the API result again
this.ignoreWarning( 'duplicate' );
this.setTransported( result );
} );
override.$element.appendTo( $extra );
}
this.setError( code, mw.message( 'file-exists-duplicate', allDuplicates.length ).parse(), $extra );
};
/**
* @param {string} code Warning code, should have matching strings in .i18n.php
* @param {Object} result The API result in parsed JSON form
* @param {string} duplicate Duplicate filename
* @param {number} count Number of duplicate versions
*/
mw.ApiUploadHandler.prototype.setDuplicateOldError = function ( code, result, duplicate, count ) {
var filename = mw.Title.makeTitle( NS_FILE, duplicate ).getPrefixedText(),
uploadDuplicate = this.makeOverrideButton().on( 'click', () => {
// mark this warning as ignored & process the API result again
this.ignoreWarning( 'duplicateversions' );
this.setTransported( result );
} );
this.setError( code, mw.message( 'fileexists-duplicate-version', filename, count ).parse(), uploadDuplicate.$element );
};
/**
* @param {string} code Warning code, should have matching strings in .i18n.php
* @param {Object} result The API result in parsed JSON form
* @param {string} duplicate Duplicate filename
*/
mw.ApiUploadHandler.prototype.setDuplicateArchiveError = function ( code, result, duplicate ) {
var filename = mw.Title.makeTitle( NS_FILE, duplicate ).getPrefixedText(),
uploadDuplicate = this.makeOverrideButton().on( 'click', () => {
// mark this warning as ignored & process the API result again
this.ignoreWarning( 'duplicate-archive' );
this.setTransported( result );
} );
this.setError( code, mw.message( 'file-deleted-duplicate', filename ).parse(), uploadDuplicate.$element );
};
/**
* @param {string|string[]} titles File title or array of titles
* @param {string|string[]} prop Image props
* @return {jQuery.Promise}
*/
mw.ApiUploadHandler.prototype.getImageInfo = function ( titles, prop ) {
return this.api.get( {
action: 'query',
titles: titles,
prop: 'imageinfo',
iiprop: prop
} );
};
/**
* Convert an array of non-prefixed filenames into a [filename => url] map.
*
* @param {string[]} filenames Array of non-prefixed filenames
* @return {Object} Map of [prefixed filename => url]
*/
mw.ApiUploadHandler.prototype.getFileLinks = function ( filenames ) {
var files = {};
filenames.forEach( ( filename ) => {
var title;
try {
title = mw.Title.makeTitle( NS_FILE, filename );
files[ title.getPrefixedText() ] = title.getUrl( {} );
} catch ( e ) {
// invalid filename (e.g. file was revdeleted)
}
} );
return files;
};
/**
* @param {string} code Error code from API
* @param {string} html Error message
* @param {jQuery} [$extra]
*/
mw.ApiUploadHandler.prototype.setError = function ( code, html, $extra ) {
this.upload.setError( code, html, $extra );
};
/**
* Marks a warning to be ignored.
*
* @param {string} code
* @return {boolean}
*/
mw.ApiUploadHandler.prototype.ignoreWarning = function ( code ) {
if ( this.isIgnoredWarning( code ) ) {
return false;
}
// mark the warning as being ignored, then restart the request
this.ignoreWarnings.push( code );
return true;
};
/**
* Returns whether or not the warning is being ignored.
*
* @param {string} code
* @return {boolean}
*/
mw.ApiUploadHandler.prototype.isIgnoredWarning = function ( code ) {
return this.ignoreWarnings.indexOf( code ) > -1;
};
}() );