MediaWiki:Gadget-DelReqHandler.js

From Wikimedia Commons, the free media repository
Revision as of 14:00, 1 June 2016 by Steinsplitter (talk | contribs) (fix by Perhelion, see talk)
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.
/**
@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]*?&section=(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>