/**
@Support for quick deletions and closing of deletion requests at the Commons.
@Authors:
[[User:Lupo]], October 2007 - January 2008,
[[User:DieBuche]], February 2011
[[User:Perhelion]], 2016; performance tuning
@Revision: 12:18, 1 June 2016 (UTC)
@License: Quadruple licensed GFDL, GPL, LGPL and Creative Commons Attribution 3.0 (CC-BY-3.0)
Choose whichever license of these you like best :-)
IE not supported
@Required modules: user.options, mediawiki.util, jquery.blockUI,
**/
//<nowiki>
/*global mediaWiki:false, jQuery:false, prompt:false, alert:false*/
/*jshint bitwise:true, curly:false, eqeqeq:true, forin:false, laxbreak:true,
undef:true, unused:true, white:false, smarttabs:true, multistr:true */
( function ( $, mw ) {
'use strict';
// Guard against double inclusions // Enable the whole shebang only for sysops.
if ( window.DelReqHandler || -1 === $.inArray( 'sysop', mw.config.get( 'wgUserGroups' ) ) ) return;
var server = mw.config.get( 'wgServer' ) + mw.config.get( 'wgArticlePath' ).replace( '$1', '' );
if ( /^\/\//.test( server ) )
server = document.location.protocol + server;
var serverR = new RegExp( '^' + server );
server = server.length;
var DRH = window.DelReqHandler = {
/*------------------------------------------------------------------------------------------
Deletion request closing: add "[del]" and "[keep]" links to the left of the section edit
links of a deletion request. [del] and [keep] prompt for an (optional) reason, then
add "delh" and "delf" with "Deleted." or "Kept." plus the reason and signature (four tildes).
Links are added to every non-deleted image mentioned on a deletion request page. The "[del]" link
triggers deletion (auto-completed!) of the image, with a deletion summary linking to the
deletion request. If the image has a talk page, it is deleted as well. The "[keep]" link
automatically removes the "delete" template from the image page and adds the "kept" template
to the image talk page, both linking back to the deletion request.
Additional there is a quick delete link [qd] without any prompt.
------------------------------------------------------------------------------------------*/
//running: [], // ?
titleFromHref: function ( href ) {
href = href.href;
if ( serverR.test( href ) ) // only full URL
return RegExp.rightContext || href.substring( server );
return '';
},
spanFragF: $( '<span class="navbar reqHandlerLinks"> [<a name="1" href="#">keep</a>] [<a href="#">del</a>] \
[<a href="#" onclick="DelReqHandler.quickDeleteFile(event)" title="QuickDelete">qd</a>]</span>' )[ 0 ],
spanFragC: $( '<span class="navbar reqHandlerLinks2"> [<a name="1" href="#">Close: Kept</a>] [<a href="#">Close: Deleted</a>]</span>' )[ 0 ],
quickDeleteFile: function ( e ) {
e.preventDefault();
e = e.target;
// take the function from the adjacent del link
$( e ).prev().attr( 'title', e.title ).click();
},
nextUntilH3: function ( cur ) {
var matched = [ cur ];
cur = cur.nextElementSibling;
while ( cur && !( cur.nodeName === 'H3' || ( cur.nodeName === 'DIV' && cur.className === 'delh' ) ) ) {
matched.push( cur );
cur = cur.nextElementSibling;
}
return matched;
},
parse: function () {
var $content = $( '#mw-content-text' ); // all skins have this
if ( !$content.length ) {
$content = $( '#bodyContent' ); // fallback really needed?
if ( !$content.length )
return;
}
if (window.delReqGlobalUsage)
this.spanFragF.appendChild(
$('<a>', { 'title' : 'GlobalUsage', 'onclick': 'DelReqHandler._onBadge(event)', 'class': 'guGU' }).badge('?', 1 , true).get(0));
var parent = $content.parent();
$content.detach(); // speedup DOM manipulation
var h3 = $content[ 0 ].getElementsByTagName( 'H3' );
var h = h3.length;
var linkReg = /Deletion_requests\/[^\n]*?§ion=(T-)?\d$/;
/*
* Main DOM loop: use as less as possibly operations, especially omit jQuery,
* as we could scan more than 10.000 links.
*/
while ( h-- ) {
var th = h3[ h ];
var headLine = th.querySelector( 'span.mw-headline' );
var requestPage = th.querySelector( 'span.mw-editsection a' ).href;
// It's really an editlink to a deletion request subpage, and not a section
// edit for a daily subpage or something else
if ( !linkReg.test( requestPage ) ) { continue; }
var discussion = this.nextUntilH3( th ); // .printfooter?
if ( th.parentNode.className !== 'delh' ) {
this.addLinks( requestPage, headLine, "", true, discussion );
}
var links = [];
var d = 0;
var i = discussion.length;
while ( i-- ) {
var al = discussion[ i ].getElementsByTagName( 'A' );
var l = al.length;
while ( l-- ) {
var a = al[ l ];
if ( a.className !== 'new' ) {
links[ d ] = a;
d++;
}
}
}
i = links.length;
while ( i-- ) {
var link = links[ i ];
var title = this.titleFromHref( link );
if ( /^File:/.test( title ) && !/\//.test( title ) ) { //We have an image link
this.addLinks( requestPage, link, title, false, discussion );
}
}
}
parent.append( $content );
},
addLinks: function ( requestPage, element, imagePage, closeRequest, discussion ) {
// jQuery is too slow here! // with vars tiny faster
var span = ( closeRequest ? this.spanFragC : this.spanFragF ).cloneNode( 1 );
function _click( e ) {
e.preventDefault();
// Use link.name for keep boolean // link.title for quick boolean
e = new DRH.process( e.target, closeRequest, requestPage, imagePage, element, span, discussion );
//DRH.running.push(e); // for what ?
}
var lks = span.children;
var lkD = lks[ 0 ];
var lkK = lks[ 1 ];
lkD.onclick = _click;
lkK.onclick = _click;
element.parentNode.insertBefore( span, element.nextSibling );
},
_onBadge: function(e) {
var query = {};
var $gu = $(e.target).closest("a.guGU");
var t = $gu.closest("span.reqHandlerLinks").prev("a");
t = window.DelReqHandler.titleFromHref(t[0]);
$gu[0].onclick = null;
if (!t) return;
t = decodeURIComponent(t).replace(/_/g, ' ');
query[t] = $gu;
$gu = window.mw.libs.GlobalUsage(5, 5);
$gu.tipsyGravity = ($('body').hasClass('rtl')) ? 'sw' : 'se';
$gu.query(query);
},
process: function ( e, closeRequestBool, requestPage, imagePage, element, span, discussion ) {
// Merge the page processing functions into our new process
$.extend( this, DRH.processHelpers );
var delReqReason = window.delReqReason || 'per nomination';
var keepReqReason = window.keepReqReason || 'no valid reason for deletion';
this.tasks = [];
this.requestPage = this.titleFromTitle( requestPage );
this.keep = e.name;
this.closeRequestBool = closeRequestBool;
this.imagePage = decodeURIComponent( imagePage ).replace( /_/g, ' ' );
this.imageTalkPage = this.imagePage.replace( /^File:/, 'File talk:' );
this.summary = 'Per [[' + this.requestPage + ']]';
this.domElements = [ $( element ), $( span ), $( discussion ) ];
//getToken
this.addTask( 'getPages' );
if ( closeRequestBool ) {
if ( this.keep ) {
this.reason = prompt( 'Why did you decide to keep this file?', keepReqReason );
//User canceled
if ( !this.reason ) return;
this.pagesToGet = [ this.requestPage ];
/*if ( this.imagePage ) { // If we close a request, keep same time the file?
this.pagesToGet.push( this.imagePage );
this.addTask( 'markAsKept' );
this.addTask( 'getDate' );
} */
} else {
this.reason = prompt( 'Why did you decide to delete this file?', delReqReason );
//User canceled
if ( !this.reason ) return;
this.pagesToGet = [ this.requestPage ];
}
this.addTask( 'closeRequest' );
} else {
this.pagesToGet = [ this.imagePage ];
if ( this.keep ) {
this.addTask( 'markAsKept' );
this.addTask( 'getDate' );
//first letter lowercase
this.summary = 'Kept p' + this.summary.slice( 1 );
} else {
this.addTask( 'deleteFile' );
this.addTask( 'nothing' ); // ?
}
this.summary = ( e.title === 'QuickDelete' ) ? this.summary : prompt( 'Summary:', this.summary );
//User canceled
if ( !this.summary ) return;
}
this.addTask( 'fakeReload' );
this.nextTask();
this.showProgress();
},
setup: function () {
var title = mw.config.get( 'wgTitle' );
if ( mw.config.get( 'wgNamespaceNumber' ) === 4 &&
/^Deletion requests\/|\/Deletion requests$/.test( title ) &&
mw.config.get( 'wgAction' ) === 'view' &&
document.URL.search( /[?&]oldid=/ ) === -1 ) {
// We're on COM:DEL or one of its daily subpages
// Don't do anything if we're not viewing the current version of the page
var ext = [ 'user.options', 'mediawiki.util' ];
if (window.delReqGlobalUsage)
ext.push( 'jquery.badge' );
mw.loader.using( ext,
function () {
$( document ).ready( function () {
DRH.parse();
setTimeout( function () { // not needed at startup
ext = [ 'ext.gadget.jquery.blockUI' ];
if (window.delReqGlobalUsage)
ext = ext.concat( [ 'ext.gadget.GlobalUsage', 'jquery.tipsy' ] );
mw.loader.load( ext );
}, 500 );
} );
}
);
}
}
};
DRH.processHelpers = {
titleFromTitle: function ( title ) {
if ( title ) {
title = mw.util.getParamValue( 'title', title );
if ( title ) {
return title.replace( /_/g, ' ' );
}
}
return '';
},
getPages: function () {
var query = {
action: 'query',
prop: 'revisions|info',
rvprop: 'content|timestamp',
intoken: 'edit',
titles: this.pagesToGet.join( '|' )
};
this.doAPICall( query, 'getPagesCallback' );
},
getPagesCallback: function ( result ) {
var pages = result.query.pages;
for ( var id in pages ) { // there should be only one, but we don't know it's ID
if ( pages.hasOwnProperty( id ) ) {
// The edittoken only changes between logins
this.edittoken = pages[ id ].edittoken;
var type;
switch ( pages[ id ].title ) {
case this.imagePage:
type = 'imagePage';
break;
case this.requestPage:
type = 'requestPage';
break;
default:
type = 'unknown';
break;
}
this[ type + 'Result' ] = {
pageContent: pages[ id ].revisions[ 0 ][ '*' ],
starttimestamp: pages[ id ].starttimestamp,
timestamp: pages[ id ].revisions[ 0 ].timestamp
};
}
}
this.nextTask();
},
closeRequest: function () {
var text = this.requestPageResult.pageContent,
watchFor = '<noinclude>[[Category:MobileUpload-related deletion requests',
replace = ']]</noinclude>';
this.decision = ( this.keep ) ? 'Kept' : 'Deleted';
text = text.replace( watchFor + replace, watchFor + '/' + this.decision.toLowerCase() + replace );
// Check for second nomination (we always load the whole page)
var sec = text.lastIndexOf( '{{delf}}\n' ) + 9; // Additional more accurately: text.substr(sec).search(/^==+/m) but not really needed
text = ( sec > 51 ) ? // minimum text-size
text.slice( 0, sec ) + '{{delh}}\n' + $.trim( text.slice( sec ) ) : '{{delh}}\n' + $.trim( text );
text += '\n----\n';
// Add dashes on 'lesser' individual signatures
var uSig = ( mw.user.options.get( 'fancysig' ) && mw.user.options.get( 'nickname' ).search( /^[ ']*\[\[/ ) !== 0 ) ?
'' : '--';
if ( this.reason ) {
this.decision += ':';
this.reason = this.reason.replace( /[.\s-]*$/, '. ' );
} else this.decision += '.';
text += '\'\'\'' + this.decision + '\'\'\' ' + this.reason + uSig + '~~\~~\n{{delf}}';
var page = {
title: this.requestPage,
text: text,
summary: this.decision + ' ' + this.reason,
editType: 'text'
};
this.savePage( page, 'nextTask' );
},
markAsKept: function () {
var text = this.imagePageResult.pageContent;
text = this.removeTemplate( text );
var page = {
title: this.imagePage,
text: text,
summary: this.summary,
editType: 'text'
};
this.savePage( page, 'nextTask' );
},
removeTemplate: function ( text ) {
var start = text.search( /\{\{[dD]elete/ );
if ( start >= 0 ) {
var level = 0;
var curr = start + 2;
var end = 0;
var opening = -1;
var closing = -1;
while ( curr < text.length && end === 0 ) {
opening = text.indexOf( '{{', curr );
closing = text.indexOf( '}}', curr );
if ( opening >= 0 && opening < closing ) {
level++;
curr = opening + 2;
} else {
if ( closing < 0 ) {
// No closing braces found
curr = text.length;
} else {
if ( level > 0 ) level--;
else end = closing + 2;
curr = closing + 2;
}
}
}
if ( end > start ) {
// Also strip whitespace after the "delete" template
if ( start > 0 )
text = text.substring( 0, start ) + text.substring( end ).replace( /^\s+/, '' );
else
text = text.substring( end ).replace( /^\s+/, '' );
return text;
}
}
alert( 'Couldn’t remove the {{delete}} template, please check the ' + this.imagePage + ' manually.' );
return text;
},
getDate: function () {
var query = {
action: 'query',
prop: 'revisions',
rvlimit: 1,
rvprop: 'timestamp',
rvdir: 'newer',
titles: this.requestPage
};
this.doAPICall( query, 'addKeepToTalk' );
},
addKeepToTalk: function ( result ) {
var pages = result.query.pages;
var date = '';
for ( var id in pages ) {
if ( pages.hasOwnProperty( id ) ) {
// there should be only one, but we don't know it's ID
var ts = pages[ id ].revisions[ 0 ].timestamp;
if ( ts ) {
// Extract year, month, and day from the timestamp.
// We don't care about the exact time.
var year = ts.substr( 0, 4 );
var month = ts.substr( 5, 2 );
var day = ts.substr( 8, 2 );
date = year + '-' + month + '-' + day;
}
}
}
var page = {
title: this.imageTalkPage,
text: '{{kept|' + date + '|' + this.requestPage + '}}\n',
summary: 'Adding {{kept}}',
editType: 'prependtext'
};
this.savePage( page, 'nextTask' );
},
reload: function () {
window.location.reload();
},
fakeReload: function () {
var dE = this.domElements;
dE[ 3 ].unblock();
//Remove links
dE[ 1 ].remove();
if ( this.closeRequestBool ) {
dE[ 3 ].toggleClass( 'delh delreqworking' );
dE[ 2 ].eq( 0 ).before( '<i>This deletion debate is now closed. Please do not make any edits to this archive.</i>' );
dE[ 2 ].eq( -1 ).after( '<br><span class="success">Saved successfully.\
<br>This is just an approximate rendering. Reload to see the actual request.</span>' );
dE[ 2 ].eq( -1 ).after( '<b>' + this.decision + '</b> ' + this.reason + ' --' + mw.config.get( 'wgUserName' ) );
dE[ 2 ].eq( -1 ).after( '<hr>' );
} else {
if ( !this.keep ) dE[ 0 ].addClass( 'new' ); //Color link red
}
},
/**
* 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.
**/
// 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 );
}
},
deleteFile: function () {
var edit = {
action: 'delete',
reason: this.summary,
title: this.imagePage,
recreate: ''
};
this.doAPICall( edit, 'nextTask' );
edit = {
action: 'delete',
reason: 'Talk page of deleted image',
title: this.imageTalkPage,
recreate: ''
};
this.doAPICall( edit, 'nextTask', true );
},
savePage: function ( page, callback ) {
var edit = {
action: 'edit',
summary: page.summary,
title: page.title
};
edit[ page.editType ] = page.text;
this.doAPICall( edit, callback );
},
fail: function ( e ) {
alert( e );
},
doAPICall: function ( params, callback, ignoreErrors ) {
var k = this;
params.format = 'json';
params.token = this.edittoken;
$.ajax( {
url: mw.util.wikiScript( 'api' ),
cache: false,
dataType: 'json',
data: params,
type: 'POST',
success: function ( result, status, x ) {
if ( ignoreErrors ) {
k[ callback ]( result );
return;
}
if ( !result ) return k.fail( 'Receive empty API response:\n' + x.responseText );
// In case we get the mysterious 231 unknown error, just try again
if ( result.error && result.error.info.indexOf( '231' ) !== -1 ) return setTimeout( function () {
k.doAPICall( params, callback );
}, 500 );
if ( result.error ) return k.fail( 'API request failed (' + result.error.code + '): ' + result.error.info );
k[ callback ]( result );
},
error: function ( x, status, error ) {
return k.fail( 'API request returned code ' + x.status + ' ' + status + 'Error code is ' + error );
}
} );
},
showProgress: function () {
var dE = this.domElements;
if ( this.closeRequestBool ) {
dE[ 2 ].wrapAll( '<div class="delreqworking">' );
dE[ 3 ] = dE[ 2 ].parent( '.delreqworking' );
dE[ 3 ].block( {
message: '<img src="https://upload.wikimedia.org/wikipedia/commons/3/39/Spinning_wheel_throbber_blue.gif"/> Closing request…',
css: { border: '3px solid #9C3', fontSize: '135%' }
} );
} else {
dE[ 3 ] = dE[ 0 ].parent();
dE[ 3 ].block( {
message: '<img src="https://upload.wikimedia.org/wikipedia/commons/f/f8/Ajax-loader%282%29.gif"/> Working…',
css: { color: '#9C3', fontWeight: 'bold', background: 'none', border: 'none' }
} );
}
},
nothing: function () {
//nothing
}
};
DRH.setup();
}( jQuery, mediaWiki ) );// EOF </nowiki>