User:DieBuche/delete.js
Jump to navigation
Jump to search
Note: After saving, you have to bypass your browser's cache to see the changes. Internet Explorer: press Ctrl-F5, Mozilla: hold down Shift while clicking Reload (or press Ctrl-Shift-R), Opera/Konqueror: press F5, Safari: hold down Shift + Alt while clicking Reload, Chrome: hold down Shift while clicking Reload.
Documentation for this user script can be added at User:DieBuche/delete. |
- Report page listing warnings and errors.
// Ajax-based replacement for [[MediaWiki:Quick-delete-code.js]]
// *** EXPERIMENTAL ***
// TODO: more reliable localization loading
// DONE: use prependtext/appendtext API parameters for more reliable editing
// DONE: display detailed progress;
// TODO: better error handling/reporting
// TODO: allow user to choose which uploaders to notify
// TODO: (somehow) detect bots, don't notify them
// TODO: try to find an actual user to notify for bot-uploaded files
// TODO: follow talk page redirects (including {{softredirect}})
// DONE: Add the ability to be extended by more userdefined buttons
// <source lang="JavaScript">
if (typeof (AjaxQuickDelete) == 'undefined' && wgNamespaceNumber >= 0) {
if (window.QuickDeleteEnhanced) {
var insertTagButtons = [
{
'label': 'No source',
'tag': '{\{subst:nsd}}',
'talk_tag': '{\{subst:image source|1=%FILE%}}',
'img_summary': 'File has no source',
'talk_summary': '%FILE% does not have a source'
},
{
'label': 'No permission',
'tag': '{\{subst:npd}}',
'talk_tag': '{\{subst:image permission|1=%FILE%}}',
'img_summary': 'Missing permission',
'talk_summary': 'Please send a permission for %FILE% to [[COM:OTRS|OTRS]]'
},
{
'label': 'No license',
'tag': '{\{subst:nld}}',
'talk_tag': '{\{subst:image license|1=%FILE%}}',
'img_summary': 'Missing license',
'talk_summary': '%FILE% does not have a license'
}
];
} else {
var insertTagButtons = [];
}
var AjaxQuickDelete = {
/**
* Set up the AjaxQuickDelete object and add the toolbox link. Called via
* addOnloadHook() during page loading.
*/
install : function () {
// abort if AJAX is not supported
if (!wfSupportsAjax || !wfSupportsAjax()) return;
// abort if page seems to be already nominated for deletion
if (document.getElementById("nuke")) return;
// remove old toolbox link if called twice
var tool = document.getElementById('t-ajaxquickdelete');
if (tool) tool.parentNode.removeChild(tool);
// set up toolbox link
//For each in etc.
if (skin == 'vector') {
mw.util.addPortletLink('p-tb', 'javascript:AjaxQuickDelete.nominateForDeletion();',
this.i18n.toolboxLink + " (Ajax)", 't-ajaxquickdelete', null);
} else {
addToolLink('p-tb', 'javascript:AjaxQuickDelete.nominateForDeletion();',
this.i18n.toolboxLink + " (Ajax)", 't-ajaxquickdelete', null);
}
for (var i = 0; i < insertTagButtons.length; i++) {
var inb = insertTagButtons[i];
if (skin == 'vector') {
mw.util.addPortletLink('p-tb', 'javascript:AjaxQuickDelete.insertTagOnPage("' + inb['tag'] + '","' + inb['img_summary'] + '","' + inb['talk_tag'] + '","' + inb['talk_summary'] + '");', inb['label']);
} else {
addToolLink('p-tb', 'javascript:AjaxQuickDelete.insertTagOnPage("' + inb['tag'] + '","' + inb['img_summary'] + '","' + inb['talk_tag'] + '","' + inb['talk_summary'] + '");', inb['label']);
}
}
},
/**
* This is the main entry point which actually starts the nomination process.
* Takes as an optional parameter a string used as the deletion reason; if none
* is given, prompts the user for one first.
*/
insertTagOnPage : function (tag, img_summary, talk_tag, talk_summary) {
//Todo: Allow customization of question..
this.tag=tag + '\n';
this.img_summary=img_summary;
if(tag.indexOf("%PARAMETER%") != -1){
reason = prompt( this.i18n.reasonForDeletion );
if (!reason) return;
this.tag=tag.replace('%PARAMETER%', reason);
this.img_summary=this.img_summary.replace('%PARAMETER%', reason);
this.img_summary=this.img_summary.replace('%PARAMETER-LINKED%', '[[:'+reason+']]');
}
// first schedule some API queries to fetch the info we need...
this.tasks = []; // reset task list in case an earlier error left it non-empty
if (talk_tag != "undefined") {
this.talk_tag=talk_tag.replace('%FILE%', wgPageName);
this.talk_summary=talk_summary.replace('%FILE%', '[[:'+wgPageName+']]');
this.addTask( 'findUploaders' );
}
//get token
this.addTask( 'loadPages' );
// ...then schedule the actual edits
this.addTask( 'prependAnyTemplate' );
this.addTask( 'notifyUploaders' );
// finally reload the page to show the deletion tag
this.addTask( 'reloadPage' );
// now go do all the stuff we just scheduled!
document.body.style.cursor = 'wait';
bodycontent = document.getElementById('bodyContent');
this.feedbackDiv = document.createElement('div');
this.feedbackDiv.setAttribute("class", 'ajaxDeleteFeedback');
//The following will be moved to common.css later
this.feedbackDiv.style.border = '1px #A9DE16 solid';
this.feedbackDiv.style.background = '#EAF2CB url(http://bits.wikimedia.org/skins-1.5/common/images/ajax-loader.gif) no-repeat 8px 14px';
this.feedbackDiv.style.padding = '1em 1em 1em 2.5em';
this.feedbackDiv.style.position = 'fixed';
this.feedbackDiv.style.top = '50%';
this.feedbackDiv.style.left = '50%';
this.feedbackDiv.style.zIndex = '100';
bodycontent.parentNode.insertBefore(this.feedbackDiv, bodycontent);
this.nextTask();
},
nominateForDeletion : function (reason) {
this.startDate = new Date ();
// prompt for reason if necessary
// TODO: replace this with a nice textbox / drop menu entry form like in TWINKLE
if (!reason) reason = prompt( this.i18n.reasonForDeletion );
if (!reason) return;
this.reason = reason;
// set up some page names we'll need later
this.requestPage = this.requestPagePrefix + wgPageName;
this.dailyLogPage = this.requestPagePrefix + this.formatDate( "YYYY/MM/DD" );
// first schedule some API queries to fetch the info we need...
this.tasks = []; // reset task list in case an earlier error left it non-empty
this.addTask( 'findUploaders' ); // TODO: allow user to edit list
this.addTask( 'loadPages' );
// ...then schedule the actual edits
this.addTask( 'prependDeletionTemplate' );
this.addTask( 'createRequestSubpage' );
this.addTask( 'listRequestSubpage' );
this.addTask( 'notifyUploaders' );
// finally reload the page to show the deletion tag
this.addTask( 'reloadPage' );
// now go do all the stuff we just scheduled!
document.body.style.cursor = 'wait';
bodycontent = document.getElementById('bodyContent');
this.feedbackDiv = document.createElement('div');
this.feedbackDiv.setAttribute("class", 'ajaxDeleteFeedback');
//The following will be moved to common.css later
this.feedbackDiv.style.border = '1px #A9DE16 solid';
this.feedbackDiv.style.background = '#EAF2CB url(http://bits.wikimedia.org/skins-1.5/common/images/ajax-loader.gif) no-repeat 8px 14px';
this.feedbackDiv.style.padding = '1em 1em 1em 2.5em';
this.feedbackDiv.style.position = 'fixed';
this.feedbackDiv.style.top = '50%';
this.feedbackDiv.style.left = '50%';
this.feedbackDiv.style.zIndex = '100';
bodycontent.parentNode.insertBefore(this.feedbackDiv, bodycontent);
this.nextTask();
},
/**
* Edit the current page to add the {{delete}} tag. Assumes that the page hasn't
* been tagged yet; if it is, a duplicate tag will be added.
*/
prependDeletionTemplate : function () {
var page = this.pages[ wgPageName ];
page.text = "{{delete|reason=" + this.reason + this.formatDate("|year=YYYY|month=MON|day=DAY}}\n");
page.editType = 'prependtext';
//Update status
this.feedbackDiv.innerHTML = this.i18n.addingTemplate;
this.savePage( page, "Nominating for deletion", 'nextTask' );
},
prependAnyTemplate : function () {
var page = this.pages[ wgPageName ];
page.text = this.tag;
page.editType = 'prependtext';
//Update status
this.feedbackDiv.innerHTML = this.i18n.addingTemplate;
this.savePage( page, this.img_summary, 'nextTask' );
},
/**
* Create the DR subpage (or append a new request to an existing subpage). The request
* page will always be watchlisted.
* TODO: if the page exists, check that any earlier nomination has been closed already
*/
createRequestSubpage : function () {
this.templateAdded = true; // we've got this far; if something fails, user can follow instructions on template to finish
var page = this.pages[ this.requestPage ];
page.text = "\n\n=== [[:" + wgPageName + "]] ===\n" + this.reason + " --~~"+"~~\n";
page.watchlist = 'watch';
page.editType = 'appendtext';
//Update status
this.feedbackDiv.innerHTML = this.i18n.creatingNomination;
this.savePage( page, "Starting deletion request", 'nextTask' );
},
/**
* Transclude the nomination page onto today's DR log page, creating it if necessary.
* The log page will never be watchlisted (unless the user is already watching it).
*/
listRequestSubpage : function () {
var page = this.pages[ this.dailyLogPage ];
if (!page.text) page.text = "{{"+"subst:" + this.requestPagePrefix + "newday}}"; // add header to new log pages
page.text = "\n{{" + this.requestPage + "}}\n";
page.watchlist = 'nochange';
page.editType = 'appendtext';
//Update status
this.feedbackDiv.innerHTML = this.i18n.listingNomination;
this.savePage( page, "Listing [[" + this.requestPage + "]]", 'nextTask' );
},
/**
* Notify any uploaders/creators of this page using {{idw}}.
* TODO: follow talk page redirects (including {{softredirect}})
* TODO: obey {{nobots}} and/or other opt-out mechanisms
*/
notifyUploaders : function () {
this.uploadersToNotify = 0;
for (var user in this.uploaders) {
if (this.tag){
var page = this.pages[ this.userTalkPrefix + user ];
page.text = "\n"+ this.talk_tag + "\n";
page.editType = 'appendtext';
this.savePage( page, this.talk_summary, 'uploaderNotified' );
//Update status
this.feedbackDiv.innerHTML = this.i18n.notifyingUploader.replace('%USER%', user);
this.uploadersToNotify++;
}else{
var page = this.pages[ this.userTalkPrefix + user ];
page.text = "\n{{"+"subst:idw|" + wgPageName + "}}\n";
page.editType = 'appendtext';
this.savePage( page, "[[:" + wgPageName + "]] has been nominated for deletion", 'uploaderNotified' );
//Update status
this.feedbackDiv.innerHTML = this.i18n.notifyingUploader.replace('%USER%', user);
this.uploadersToNotify++;
}
}
if (this.uploadersToNotify == 0) this.nextTask();
},
uploaderNotified : function () {
this.uploadersToNotify--;
if (this.uploadersToNotify == 0) this.nextTask();
},
/**
* Compile a list of uploaders to notify. Users who have only reverted the file to an
* earlier version will not be notified.
* TODO: notify creator of non-file pages
* TODO: handle continuations if there are more than 50 revisions
* TODO: don't notify bots, try to figure out a real human user to notify instead
* TODO: allow nominator to choose which users to actually notify
*/
findUploaders : function () {
var query = {
action : 'query',
prop : 'imageinfo',
iiprop : 'user|sha1',
iilimit : 50,
titles : wgPageName };
this.doAPICall( query, 'findUploadersCB' );
},
findUploadersCB : function (result) {
this.uploaders = {};
var pages = result.query.pages;
for (var id in pages) { // there should be only one, but we don't know its ID
var info = pages[id].imageinfo;
if (!info) continue; // not a file?
var seenHashes = {};
for (var i = info.length-1; i >= 0; i--) { // iterate in reverse order
if (info[i].sha1 && seenHashes[ info[i].sha1 ]) continue; // skip reverts
this.uploaders[ info[i].user ] = true;
}
// TODO: improve handling of bot uploads
}
//Update status
this.feedbackDiv.innerHTML = this.i18n.preparingToEdit.replace('%COUNT%', this.uploaders.length);
this.nextTask();
},
/**
* Fetch page content for editing. Currently we do it all in one batch; if we'd
* like to fetch more pages later, this code would need some redesign.
* TODO: follow redirects?
*/
loadPages : function () {
var pages = [ wgPageName, this.requestPage, this.dailyLogPage ];
if (this.tag) var pages = [ wgPageName ];
for (var user in this.uploaders) pages.push( this.userTalkPrefix + user );
var query = {
action : 'query',
prop : 'info|revisions',
intoken : 'edit',
//If we use append/prependtext, we won't need the content
rvprop : 'timestamp',
titles : pages.join('|') };
this.doAPICall( query, 'loadPagesCB' );
},
loadPagesCB : function (result) {
// build a denormalization map so that we can keep using the same (possibly non-canonical) page names as we originally queried:
var denorm = {};
var norm = result.query.normalized || [];
for (var i = 0; i < norm.length; i++) {
denorm[ norm[i].to ] = norm[i].from;
}
// save results:
this.pages = {};
var pages = result.query.pages;
for (var id in pages) {
var page = pages[id];
var name = denorm[ page.title ] || page.title;
//Won't need this
//page.text = ((page.revisions || [])[0] || {})['*'] || ""; // copy of revision text for editing
this.pages[ name ] = page;
}
this.nextTask();
},
/**
* Submit an edited page.
*/
savePage : function (page, summary, callback) {
var edit = {
action : 'edit',
summary : summary,
watchlist : (page.watchlist || 'preferences'),
title : page.title,
token : page.edittoken,
//If we use append/prepend, there'll never be editconflicts
//starttimestamp : page.starttimestamp
};
//Is there a way to declare this inside the brackets?
edit[page.editType]=page.text;
if (page.revisions && page.revisions.length) {
edit.basetimestamp = page.revisions[0].timestamp;
edit.nocreate = 1;
} else {
edit.createonly = 1;
}
this.doAPICall( edit, callback );
},
/**
* Does a MediaWiki API request and passes the result to the supplied callback (method name).
* Uses POST requests for everything for simplicity.
* TODO: better error handling
*/
doAPICall : function ( params, callback ) {
var query = [ "format=json" ];
for (var name in params) {
query.push( encodeURIComponent(name) + "=" + encodeURIComponent(params[name]) );
}
query = query.sort().join("&"); // conveniently, "text" sorts before "token"
var o = this;
var x = sajax_init_object();
x.open( 'POST', this.apiURL, true );
x.setRequestHeader( 'Content-Type', 'application/x-www-form-urlencoded' );
x.onreadystatechange = function () {
if (x.readyState != 4) return;
if (x.status >= 300) return o.fail( "API request returned code " + x.status + " " + x.statusText );
try {
var result = eval( "(" + x.responseText + ")" );
} catch (e) {
return o.fail( "Error evaluating API result: " + e + "\n\nResponse content:\n" + x.responseText );
}
if (!result) return o.fail( "Receive empty API response:\n" + x.responseText );
if (result.error) return o.fail( "API request failed (" + result.error.code + "): " + result.error.info );
try { o[callback](result); } catch (e) { return o.fail(e); }
};
x.send(query);
},
/**
* Simple task queue. addTask() adds a new task to the queue, nextTask() executes
* the next scheduled task. Tasks are specified as method names to call.
*/
tasks : [], // list of pending tasks
currentTask : '', // current task, for error reporting
addTask : function ( task ) {
this.tasks.push( task );
},
nextTask : function () {
var task = this.currentTask = this.tasks.shift();
try { this[task](); } catch (e) { this.fail(e); }
},
/**
* Once we're all done, reload the page.
*/
reloadPage : function () {
if (wgAction == 'view') location.reload();
else location.href = mw.config.get('wgServer') + mw.config.get('wgArticlePath').replace("$1", encodeURIComponent(mw.config.get('wgPageName')));
},
/**
* Crude error handler. Just throws an alert at the user and (if we managed to
* add the {{delete}} tag) reloads the page.
*/
fail : function ( err ) {
var msg = this.i18n.taskFailure[this.currentTask] || this.i18n.genericFailure;
var fix = (this.templateAdded ? this.i18n.completeRequestByHand : this.i18n.addTemplateByHand );
alert( msg + " " + fix + "\n\n" + this.i18n.errorDetails + "\n" + err );
if (this.templateAdded) this.reloadPage();
else document.body.style.cursor = 'default';
},
/**
* Very simple date formatter. Replaces the substrings "YYYY", "MM" and "DD" in a
* given string with the UTC year, month and day numbers respectively. Also
* replaces "MON" with the English full month name and "DAY" with the unpadded day.
*/
formatDate : function ( fmt, date ) {
var pad0 = function ( s ) { s = "" + s; return (s.length > 1 ? s : "0" + s); }; // zero-pad to two digits
if (!date) date = this.startDate;
fmt = fmt.replace( /YYYY/g, date.getUTCFullYear() );
fmt = fmt.replace( /MM/g, pad0( date.getUTCMonth()+1 ) );
fmt = fmt.replace( /DD/g, pad0( date.getUTCDate() ) );
fmt = fmt.replace( /MON/g, this.months[ date.getUTCMonth() ] );
fmt = fmt.replace( /DAY/g, date.getUTCDate() );
return fmt;
},
months : "January February March April May June July August September October November December".split(" "), // srsly? I have to do this myself?? wtf?
// Constants
requestPagePrefix : "Commons:Deletion requests/", // DR subpage prefix
userTalkPrefix : wgFormattedNamespaces[3] + ":", // user talk page prefix
apiURL : mw.config.get('wgServer') + mw.config.get('wgScriptPath') + "/api.php", // MediaWiki API script URL
// Translatable strings (many unused for now)
i18n : {
toolboxLink : "Nominate for deletion",
// GUI reason prompt form (mostly unused)
reasonForDeletion : "Reason for deletion:",
notifyFollowingUsers : "Notify the following users:",
submitButtonLabel : "Nominate",
cancelButtonLabel : "Cancel",
// GUI progress messages (unused)
preparingToEdit : "Preparing to edit %COUNT% pages... ",
creatingNomination : "Creating nomination page... ",
listingNomination : "Adding nomination page to daily list... ",
addingTemplate : "Adding deletion template to file description page... ",
notifyingUploader : "Notifying %USER%... ",
// GUI results (unused)
operationSucceeded : "done",
operationFailed : "ERROR",
// Errors
genericFailure : "An error occurred while nominating this "+(6==wgNamespaceNumber?"file":"page")+" for deletion.",
taskFailure : {
listUploaders : "An error occurred while determining the " + (6==wgNamespaceNumber ? "uploader(s) of this file" : "creator of this page") + ".",
loadPages : "An error occurred while preparing to nominate this "+(6==wgNamespaceNumber?"file":"page")+" for deletion.",
prependDeletionTemplate : "An error occurred while adding the {{delete}} template to this "+(6==wgNamespaceNumber?"file":"page")+".",
createRequestSubpage : "An error occurred while creating the request subpage.",
listRequestSubpage : "An error occurred while adding the deletion request to today's log.",
notifyUploaders : "An error occurred while notifying the " + (6==wgNamespaceNumber ? "uploader(s) of this file" : "creator of this page") + ".",
dummy : "" // IE doesn't like trailing commas
},
addTemplateByHand : "To nominate this "+(6==wgNamespaceNumber?"file":"page")+" for deletion, please edit the page to add the {{delete}} template and follow the instructions shown on it.",
completeRequestByHand : "Please follow the instructions on the deletion notice to complete the request.",
errorDetails : "A detailed description of the error is shown below:",
dummy : "" // IE doesn't like trailing commas
}
};
if (!/^en\b/.test(wgUserLanguage)) importScript( 'User:Ilmari_Karonen/ajax_quick_delete.js/' + wgUserLanguage.replace(/-.*/, "") );
$ (function () { AjaxQuickDelete.install(); });
} // end if (guard)
// </source>