Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/**
* Name: MultiUploadJS
* Author: Fujimaru-kun
* Based on: MultiUpload (by Gguigui1 and KhangND), Gadget-multiupload.js (by Pcj)
* Description: Allows upload of multiple files at the same time
*/
mw.loader.using([
'mediawiki.api',
'mediawiki.confirmCloseWindow',
'mediawiki.util',
'oojs-ui-core',
'ext.fandom.photoGallery.gallery.css',
]).then(function () {
'use strict';
window.dev = window.dev || {};
window.dev.multiupload = window.dev.multiupload || {};
var config = mw.config.get([
'wgCanonicalSpecialPageName',
'wgFormattedNamespaces',
'wgNamespaceIds',
'wgUserLanguage',
'wgUserGroups'
]);
var query = new URLSearchParams(document.location.search);
var useLegacyOptions = false;
if (
window.dev.multiupload.loaded ||
config.wgCanonicalSpecialPageName !== "Upload" ||
$("#wpForReUpload").val() || //Disabling for Reupload
query.get('wpDestFile') || // Disabling when following a redlink
!/autoconfirmed/.test(config.wgUserGroups.join())
) {
return;
}
window.dev.multiupload.loaded = true;
window.dev.multiupload.options = window.dev.multiupload.options || {};
// Handle Legacy Option for a smooth transition
if (!!window.MultiUploadoption) {
Object.assign(window.dev.multiupload.options, window.MultiUploadoption);
useLegacyOptions = true;
}
var api = new mw.Api(),
i18n,
preloads = 1,
allowCloseWindow,
token = mw.user.tokens.get('csrfToken'),
files = [],
stdmsgs = [
// General UI things
'filedesc', 'license-header', 'watchthisupload', 'ignorewarnings',
// Upload warnings
'badfilename', 'file-deleted-duplicate', 'file-exists-duplicate', 'fileexists', 'fileexists-no-change', 'filename-bad-prefix',
'filepageexists', 'filewasdeleted'
],
limit = window.dev.multiupload.options.max || -1,
defaultlicense = window.dev.multiupload.options.defaultlicense || '',
defaultdescription = window.dev.multiupload.options.defaultdescription || '',
curFile = 0,
progressBarWidget,
uploadResultCount = {
warnings: 0,
errors: 0,
};
function preload() {
if (--preloads === 0) {
window.dev.i18n.loadMessages('MultiUpload', {
cacheVersion: 1,
}).done(init);
}
}
function init(i18nData) {
i18n = i18nData;
// Legacy Options warning message
if (useLegacyOptions) {
var legacyWarningMsg = $('<p>').html(i18n.msg('legacy-options-warning').parse());
showNotification('warning', legacyWarningMsg, false);
}
setLimit();
api.loadMessagesIfMissing(stdmsgs).then(function() {
$("#wpUploadFile").parent().parent().addClass("regularFileSelect");
$("tr.regularFileSelect").after('<tr class="multipleFileSelect"><td class="mw-label"> </td><td class="mw-input"><input type="file" id="multiupload" multiple /></td></tr>');
$("#mw-htmlform-source > tbody > tr.mw-htmlform-field-UploadSourceField.regularFileSelect").remove();
$("#mw-upload-form > fieldset:nth-of-type(3)").remove();
$("#mw-upload-form > span > input.mw-htmlform-submit").remove();
$("#mw-upload-form > fieldset:nth-of-type(2)").hide();
// The core MediaWiki JS registers a beforeUnload event handler that is removed on form submit.
// If we used a button input with a click event handler (like you might intuitively do for this case),
// we would get the popup telling us that there are unsaved changes even after all uploads succeed.
// So instead we need to use a submit input with preventDefault() on the forms submit event.
$("span.mw-htmlform-submit-buttons").append('<input type="submit" value="' + i18n.msg('uploadfiles').escape() + '" class="multipleFileSelect" id="multiFileSubmit" />');
$("span.mw-htmlform-submit-buttons").append('<input type="button" value="' + i18n.msg('reset').escape() + '" class="multipleFileSelect" id="multiFileReset" />');
$("#multiupload").change(addFields);
$("#mw-upload-form").on('submit', uploadFiles);
$("#multiFileReset").click(() => {
reset();
$("#mw-upload-form")[0].reset();
});
allowCloseWindow = mw.confirmCloseWindow({
test: function() {
return files.length !== 0;
}
});
});
}
/*
* Set Maximum number of file the user can import at one time (can be overriden by a config var but its value is capped at 100)
*/
function setLimit() {
if (limit < 0 ||
limit > 100 ||
typeof limit !== 'number') {
if (/staff|util|bot-global|wiki-specialist/.test(config.wgUserGroups.join())) {
limit = 200;
} else if (/bureaucrat|bot/.test(config.wgUserGroups.join())) {
limit = 70;
} else if (/sysop/.test(config.wgUserGroups.join())) {
limit = 50;
} else if (/rollback|content-moderator/.test(config.wgUserGroups.join())) {
limit = 30;
} else {
limit = 20;
}
}
}
/*
* Add Fields to change the filename, description and license of each file to DOM
*/
function addFields() {
reset();
files = $("#multiupload")[0].files;
if (files.length <= limit) {
var watchuploads = mw.user.options.get('watchuploads') == "1" ? 'checked=\"checked\"' : '';
for (var index = 0; index < files.length; index++) {
var element = files[index];
var filedesc = $("#mw-upload-form > fieldset:nth-of-type(2)").clone();
filedesc.attr("id", "file-" + index);
filedesc.children("legend").text(i18n.msg('imagename').escape() + (index + 1));
filedesc.find("#mw-htmlform-description > tbody > tr.mw-htmlform-field-HTMLTextField > td.mw-label > label").attr("for", "wpDestFile" + index);
filedesc.find("#mw-htmlform-description > tbody > tr.mw-htmlform-field-HTMLTextAreaField > td.mw-label > label").attr("for", "wpUploadDescription" + index);
filedesc.find("#wpDestFile").attr("name", "wpDestFile" + index).attr("id", "wpDestFile" + index).val(element.name);
filedesc.find("#wpUploadDescription").attr("name", "wpUploadDescription" + index).attr("id", "wpUploadDescription" + index).val(defaultdescription);
filedesc.find("#wpLicense").attr("name", "wpLicense" + index).attr("id", "wpLicense" + index).val(defaultlicense);
filedesc.append("<hr />");
filedesc.append("<td class=\"mw-input\"><input name=\"wpWatchthis" + index + "\" type=\"checkbox\" value=\"1\" " + watchuploads + " id=\"wpWatchthis" + index + "\"> <label for=\"wpWatchthis" + index + "\">" + mw.message('watchthisupload').text() + "</label></td>");
filedesc.append("<td class=\"mw-input\"><input name=\"wpIgnoreWarning" + index + "\" type=\"checkbox\" value=\"1\" id=\"wpIgnoreWarning" + index + "\"> <label for=\"wpIgnoreWarning" + index + "\">" + mw.message('ignorewarnings').text() + "</label></td>");
$("#mw-upload-form > span").before(filedesc.show());
}
} else {
alert(i18n.msg('limit').plain());
$("#multiupload").val("");
reset();
}
}
/*
* Reset form to its initial state
*/
function reset() {
files = [];
$("#mw-upload-form > fieldset:nth-of-type(2) ~ fieldset").remove();
}
/*
* Initiate uploading process
*/
function uploadFiles(event) {
event.preventDefault();
if (files.length === 0) {
alert(i18n.msg('nofile').plain());
return false;
} else if (files.length > limit) {
alert(i18n.msg('limit').plain());
return false;
}
$("#mw-content-text").hide();
$("<h3>" + i18n.msg('successful').escape() + "</h3><ul id='multiUploadDone'></ul><div style='display:none;' id='multiUploadWarnings'><h3>" + i18n.msg('warnings').escape() + "</h3><ul></ul></div><div style='display:none;' id='multiUploadFailed'><h3>" + i18n.msg('failed').escape() + "</h3><ul></ul></div>").prependTo("#content");
progressBarWidget = new OO.ui.ProgressBarWidget();
progressBarWidget.$element.prependTo('#content');
document.body.scrollIntoView(true);
apiUpload();
}
/*
* Make API Requests to upload files
*/
function apiUpload() {
if (curFile > files.length) {
allowCloseWindow.release();
if (uploadResultCount.warnings === 0 && uploadResultCount.errors === 0) {
showUploadNotification('success');
} else {
if (uploadResultCount.warnings !== 0) {
showUploadNotification('warning', uploadResultCount.warnings);
}
if (uploadResultCount.errors !== 0) {
showUploadNotification('error', uploadResultCount.errors);
}
}
progressBarWidget.setProgress(100);
progressBarWidget.$element.hide();
return;
}
if (files[curFile] === undefined) {
curFile++;
apiUpload();
return;
}
var progress = 100 * curFile / files.length;
progressBarWidget.setProgress(progress);
var filename = $("#wpDestFile" + curFile).val() || files[curFile].name;
var license = $("#wpLicense" + curFile + " option:selected").prop("title") !== "{{}}" ? "== " + mw.message('license-header').plain() + " ==\n" + $("#wpLicense" + curFile + " option:selected").prop("title") : "";
var comment = $("#wpUploadDescription" + curFile).val() || "";
var description = !!comment ? ("== " + mw.message('filedesc').plain() + " ==\n" + comment) : "";
var watch = $("#wpWatchthis" + curFile).is(":checked") ? 'watch' : 'nochange';
var ignoreWarnings = $("#wpIgnoreWarning" + curFile).is(":checked");
var params = {
action: 'upload',
comment: comment,
file: files[curFile],
filename: filename,
filesize: files[curFile].size,
token: token,
text: description + "\n" + license,
watchlist: watch,
format: 'json',
formatversion: 2,
errorformat: 'html',
errorlang: config.wgUserLanguage,
};
if (ignoreWarnings) {
params.ignorewarnings = 1;
}
var options = {
contentType: 'multipart/form-data',
// No timeout (copied from mw.Api.upload code)
timeout: 0,
};
// Use `api.post()` manually instead of the `api.upload()` convenience wrapper. This is because:
// a) `api.upload()` uses a parameter allowlist that is incomplete. In particular `errorformat` is filtered,
// which would prevent us from getting back human readable error message.
// b) If you upload with `ignorewarnings` and the upload produced warnings, but succeeded,
// `api.upload()` will classify the call as failed, meaning we would need to add a special case for this.
api.post(params, options).done(function (data) {
if (data.upload.result === 'Warning') {
uploadResultCount.warnings++;
getUploadWarningTexts(data.upload.warnings, filename).done(function(warnings) {
$("#multiUploadWarnings > ul").append(makeFailureItem(filename, warnings));
$("#multiUploadWarnings").show();
});
} else if (data.upload.result === 'Success') {
$("#multiUploadDone").append('<li><a href="' + data.upload.imageinfo.descriptionurl + '" target="_blank">' + data.upload.filename + '</a></li>');
} else {
console.warn('Unexpected success result', data);
}
curFile++;
apiUpload();
}).fail(function (code, data) {
uploadResultCount.errors++;
var failureItem;
if (data.errors) {
var errorReasons = data.errors.map(function(error) { return error.html; });
failureItem = makeFailureItem(filename, errorReasons);
} else if (code === 'http') {
failureItem = makeFailureItem(filename, i18n.msg('network-error').escape());
} else {
failureItem = makeFailureItem(filename, i18n.msg('unknown-error').escape());
}
$("#multiUploadFailed > ul").append(failureItem);
$("#multiUploadFailed").show();
curFile++;
apiUpload();
});
}
function makeFailureItem(filename, reasons) {
if (reasons && !Array.isArray(reasons)) {
reasons = [reasons];
}
var item = $('<li>');
if (!reasons || reasons.length === 0) {
item.text(filename);
} else if (reasons.length === 1) {
item.text(filename + ': ').append(reasons[0]);
} else {
var reasonList = $('<ul>');
reasons.forEach(function(reason) {
reasonList.append($('<li>').html(reason));
});
item.text(filename + ':').append(reasonList);
}
return item;
}
function getUploadWarningTexts(warnings, filename) {
var deferred = $.Deferred();
var parseDataWrapper = $('<div>');
var correctedFilename = warnings['badfilename'] || filename;
for (var warningKey in warnings) {
var warningData = warnings[warningKey];
var message = getLocalizedWarningMessage(warningKey, warningData, correctedFilename);
$('<div>').html(message).appendTo(parseDataWrapper);
}
api.parse(parseDataWrapper.html()).done(function(parsedData) {
var parsedWarnings = $(parsedData).children().map(function() {
return this.innerHTML;
}).get();
deferred.resolve(parsedWarnings);
}).fail(function() {
deferred.reject();
});
return deferred.promise();
}
/**
* Retrieve and format the warning message
* @param key the warning we received
* @param args the data associated with the warning
* @param filenameWithoutNamespace the filename associated with the warning
* @returns the localized warning message formatted
*/
function getLocalizedWarningMessage(key, args, filenameWithoutNamespace) {
switch (key) {
case 'badfilename': {
var message = mw.message('badfilename', args).plain();
return message;
}
case 'bad-prefix': {
var message = mw.message('filename-bad-prefix', args).plain();
return message;
}
case 'duplicate': {
var filenames = args;
var message = mw.message('file-exists-duplicate', filenames.length).plain();
var gallery = '<gallery>\n';
filenames.forEach(function(name) {
gallery += name + '\n';
});
gallery += '</gallery>';
message += '\n' + gallery;
return message;
}
case 'duplicate-archive': {
var filename = prependNamespace('file', args);
var message = mw.message('file-deleted-duplicate', filename).plain();
return message;
}
case 'exists-normalized':
case 'exists': {
var filename = prependNamespace('file', args);
var message = mw.message('fileexists', filename).plain().replace('[['+filename+'|thumb]]', '');
return message;
}
case 'nochange': {
var filename = prependNamespace('file', filenameWithoutNamespace);
var message= mw.message('fileexists-no-change', filename).plain();
return message;
}
case 'page-exists': {
var filename = prependNamespace('file', filenameWithoutNamespace);
var message= mw.message('filepageexists', filename).plain();
return message;
}
case 'thumb-name': {
return mw.message('filename-thumb-name').plain();
}
case 'was-deleted': {
var filename = prependNamespace('file', filenameWithoutNamespace);
var message= mw.message('filewasdeleted', filename).plain();
return message;
}
default: {
// There is no exhaustive documentation on all possible warnings.
// For now the (seemingly) most common ones are implemented.
// For the rest we fall back to this generic message and wait for feedback.
var warningKey = '<code>' + key + '</code>';
var warningDetails = '<code>' + JSON.stringify(args) + '</code>';
var message = i18n.msg('unknown-warning', warningKey, warningDetails).plain();
return message;
}
}
}
/**
* Display a notification
* @param type type of notification (error, warning, success)
* @param count number of error or warning encountered
*/
function showUploadNotification(type, count) {
var text = i18n.msg(type + '-notification', count).parse();
showNotification(type, text);
}
/**
* Display a notification
* @param type type of notification (error, warning, success)
* @param msg the message to display (either plaintext, mw message or HTML Element)
* @param autohide Option to autohide the notification after a certain time (default is true)
*/
function showNotification(type, msg, autohide = true) {
var messageWidget = new OO.ui.MessageWidget({
type: type,
inline: true,
label: msg
});
mw.notify(messageWidget.$element, { autoHide: autohide });
}
/**
* Prepend the namespace to a page name
* @param canoncialNamespace the namespace
* @param pageName the page name
* @returns the pagename with its namespace
*/
function prependNamespace(canoncialNamespace, pageName) {
var namespaceId = config.wgNamespaceIds[canoncialNamespace];
var formattedNamespace = config.wgFormattedNamespaces[namespaceId];
var fullPageName = formattedNamespace + ':' + pageName;
return fullPageName;
}
mw.hook('dev.i18n').add(preload);
importArticle({
type: 'script',
article: 'u:dev:MediaWiki:I18n-js/code.js'
});
});