MediaWiki:ProcessFileMoverRequests.js

From Wikimedia Commons, the free media repository
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.
/**
** @description
**   Easily process the commands left by filemovers and post them to the actual Commons-Delinker-Command page
** @usage
**   [[User:CommonsDelinker/commands/filemovers]]
** @author 
**   Rillke, 2012, 2013
** @license
**   GPL, v.3.0
**
** <nowiki>
** jshint valid
**/

/*global jQuery:false, mediaWiki:false, AjaxQuickDelete:false, alert:false*/
/*jshint curly:false, laxbreak: true*/

mediaWiki.loader.using('ext.gadget.AjaxQuickDelete', function () {
'use strict';
if (!AjaxQuickDelete) return;
var sDelinker = 'User:CommonsDelinker/commands';
var sDelinkerF = sDelinker + '/filemovers';
if (sDelinkerF !== mediaWiki.config.get('wgPageName')) return;

AjaxQuickDelete.showProgress("Reading list and checking for errors. This can take up to 1 minute");

setTimeout(function () {
var AQD = AjaxQuickDelete,
	$ = jQuery,
	mw = mediaWiki;

$.extend(AQD, {
	cdFileExists: function () {
		this.fail("Can\'t move file back because something exists at the old position. Reason was " + this.reason);
	},
	cdDone: function () {
		this.showProgress();
	},
	cdRemoveFileMoverBit: function () {
		var _this = this,
			u = _this.cdFilemover;

		_this.showProgress("Obtaining information about " + u);

		mw.libs.commons.api.$query({
			action: 'query',
			list: 'users',
			usprop: 'groups',
			ususers: u,
			meta: 'tokens',
			type: 'userrights'
		}, {
			method: 'POST',
			cache: false
		}).done(function (r) {
			if (r.query.users[0] && r.query.tokens.userrightstoken) {
				if ($.inArray('filemover', r.query.users[0].groups) === -1) {
					// Nothing to do, simply proceed
					_this.nextTask();
				}
				_this.showProgress("Removing -" + u + "- from file mover group.");
				$.post(mw.util.wikiScript('api'), {
					format: 'json',
					action: 'userrights',
					user: u,
					remove: 'filemover',
					token: r.query.tokens.userrightstoken,
					reason: _this.cdFilemoverRemoveReason
				}).done(function (r2) {
					if (r2.userrights.removed.length) {
						_this.nextTask();
					} else {
						_this.fail("Error while changing user rights!");
					}
				}).fail(function () {
					_this.fail("Server-Error while changing user rights!");
				});
			} else {
				_this.fail("Error while retrieving information about user -- unable to get token!");
			}
		}).fail(function () {
			_this.fail("API-Error while retrieving information about user -- unable to get token!");
		});
	},
	cdMoveBack: function (params) {
		var oPN = mw.config.get('wgPageName'),
			from = 'File:' + params.movedata.from,
			to = 'File:' + params.movedata.to;

		mw.config.set('wgPageName', to);
		this.pageName = to;
		this.initialize();
		this.showProgress();
		mw.config.set('wgPageName', oPN);

		this.reason = 'Moving back because ' + params.reason;
		this.wpLeaveRedirect = params.redirect;
		this.replaceUsingCORS = true;
		this.destination = from;

		this.addTask('getMoveToken');
		this.addTask('doesFileExist');
		this.fileNameExistsCB = 'cdFileExists';
		this.addTask('movePage');
		this.addTask('queryRedirects');
		this.addTask('replaceUsage');
		if (params.notifyuser) {
			this.addTask('notifyUploaders');
			this.uploaders = {};
			this.uploaders[params.movedata.moved_by] = true;
			// TODO: Create templates
			var templateparams = '|from=' + from + '|to=' + to + '|reason=' + params.movedata.reason + '|reason_wrong=' + params.reason;
			this.talk_tag = params.removefromfilemovergroup ? '{{subst:Filemover removed' + templateparams + '}}' : '{{subst:Inappropriate move' + templateparams + '}}';
		}
		if (params.removefromfilemovergroup) {
			this.addTask('cdRemoveFileMoverBit');
			this.cdFilemover = params.movedata.moved_by;
			this.cdFilemoverRemoveReason = params.reason + ' @[[' + from + ']]';
		}
		this.addTask('cdDone');

		this.nextTask();
	},
	cdMoveRequests: function (requests) {
		this.initialize();
		this.comDelMoveRequests = requests;

		this.pageName = sDelinkerF;

		this.addTask('getMoveToken');
		this.addTask('cdRemoveItemsToProcess');
		this.addTask('cdPostRequests');
		this.addTask('cdPostManualRequests');
		this.addTask('reloadPage');

		this.nextTask();
	},
	cdRemoveItemsToProcess: function () {
		var newContent = this.pageContent;
		$.each(this.comDelMoveRequests, function (i, el) {
			newContent = newContent.replace(el.raw, '');
		});
		newContent = $.trim(newContent);

		var page = {};
		page.title = sDelinkerF;
		page.text = newContent;
		page.editType = 'text';
		page.starttimestamp = this.starttimestamp;
		page.timestamp = this.timestamp;
		page.watchlist = 'nochange';

		this.showProgress('Removing items to process from list');
		this.savePage(page, 'Moving replacement requests to [[' + sDelinker + ']]', 'nextTask');
	},
	cdPostRequests: function () {
		var addText = '';
		$.each(this.comDelMoveRequests, function (i, el) {
			if (el.auto[0].checked) addText += '\n{{universal replace|' + el.from + '|' + el.to + '|reason=' + el.reasonArea.val() + '}}';
		});

		if (!addText) return this.nextTask();

		var page = {};
		page.title = sDelinker;
		page.text = addText;
		page.editType = 'appendtext';
		page.watchlist = 'nochange';

		this.showProgress(this.i18n.replacingUsage);
		this.savePage(page, 'adding requests from [[' + sDelinkerF + ']]', 'nextTask');
	},
	cdPostManualRequests: function () {
		var addText = '';
		$.each(this.comDelMoveRequests, function (i, el) {
			if (el.manual[0].checked) addText += '\n{{universal replace|' + el.from + '|' + el.to + '|reason=' + el.reasonArea.val() + '}}';
		});

		if (!addText) return this.nextTask();

		var page = {};
		page.title = sDelinker + '/byHand';
		page.text = addText;
		page.editType = 'appendtext';
		page.watchlist = 'nochange';

		this.showProgress("Listing items to process by hand");
		this.savePage(page, 'adding requests from [[' + sDelinkerF + ']]', 'nextTask');
	}
});

mw.util.addCSS('table.hovertable tr:hover { background-color: white !important; border: 1px solid #A7D7F9 !important; outline: 1px solid #A7D7F9; }\n' + 'table.hovertable tr:hover > td { background-color: white !important; border: 1px solid #A7D7F9 !important; }\n' + '.pfml-mb-dlg > div { padding: 5px }\n' + 'button.ui-button-icon-only { width: 2.4em !important }\n' // Fixing broken jQuery style (broken by MW)
);

var rxLine = /\{\{\universal replace\|[^\{\}]*\}\}/ig,
	rxSubmatches = /\{\{\universal replace\|([^\{\}]*?)\|([^\{\}]*?)(?:\|\s*reason\s*\=([^\{\}]*))?\}\}/i,
	$tb = $('#wpTextbox1'),
	val = $tb.val(),
	matrix = [],
	matrixSrcIndex = {},
	matrixDestIndex = {},
	problemCount = 0;

var cleanFilename = function (fn) {
	return fn.toLowerCase().replace(/\.tif$/, '.tiff').replace(/\.jpeg$/, '.jpg').replace(/\.og[av]$/, '.ogg');
};
var isProblem = function (src, dest) {
	//	if (/BS.?icon/i.test(src)) return 'BSicon';
	//special treatment no longer needed, renaming handled by [[User:Jc86035]] and [[User:JJMC89 bot]]
	try {
		var extSrc = cleanFilename(src, true).match(/\.(\w{2,5})$/)[1];
		var extDest = cleanFilename(dest, true).match(/\.(\w{2,5})$/)[1];
		if ('svg' === extDest && 'svg' !== extSrc) return 'x → svg';
		if (extSrc !== extDest) return 'new filetype';
	} catch (ex) {
		return 'no file ext';
	}
	if ((src) in matrixSrcIndex) return 'duplicate request';
	if ((src) in matrixDestIndex) return 'file moved twice';
	return '';
};
var m = val.match(rxLine);
if (!m) {
	alert("Nothing to process!");
	AQD.showProgress();
	return;
}
$.each(m, function (i, l) {
	var p = l.match(rxSubmatches);
	if (p.length < 4) return true;
	var el = {
		from: $.trim(p[1].replace(/_/g, ' ')),
		to: $.trim(p[2].replace(/_/g, ' ')),
		reason: $.trim(p[3]),
		raw: l
	};
	el.fromHref = mw.util.getUrl('File:' + el.from);
	el.toHref = mw.util.getUrl('File:' + el.to);
	el.isProblem = isProblem(el.from, el.to);
	if (el.isProblem) problemCount++;
	matrixSrcIndex[el.from] = matrix.length;
	matrixDestIndex[el.to] = matrix.length;
	matrix.push(el);
});

var $submitButton,
	$dlg = $('<div>', {
		text: 'Order Commons Delinker (auto), put the manual requests on the appropriate page and remove all items from this page.'
	}),
	$table = $('<table>', {
		'class': 'hovertable wikitable',
		style: 'width:100%;'
	}).append($.parseHTML('<thead><tr><th>Source</th><th>Destination</th><th>Move back</th><th>Potential issues</th><th>Reason</th><th>Details</th><th>Manual</th><th>Auto</th></tr></thead>')),
	$tbody = $('<tbody>').appendTo($table);

var timoutId = 0;
var updateCount = function () {
	var autos = 0,
		manuals = 0;

	$.each(matrix, function (i, el) {
		if (el.manual[0].checked) manuals++;
		if (el.auto[0].checked) autos++;
	});
	$manualCount.text(manuals);
	$autoCount.text(autos);
};
var _updateCount = function (e) {
	clearTimeout(timoutId);
	timoutId = setTimeout(updateCount, 750);
	if (e) e.stopPropagation();
};
var _onMoveBackImmediateClick = function () {
	mw.loader.using(['ext.gadget.libAPI', 'ext.gadget.jquery.blockUI'], $.proxy(__onMoveBackImmediateClick, this));
};
var __onMoveBackImmediateClick = function () {
	var $button = $(this),
		$buttonset = $button.closest('div'),
		$tr = $buttonset.closest('tr'),
		d = $tr.data('movedata'),
		$autobox = d.auto;

	$autobox[0].checked = false;
	$autobox.triggerHandler('change');
	$buttonset.block({
		message: "Moving back",
		css: {
			width: '98%'
		}
	});
	d.$moveBackButton.button({
		disabled: true
	});
	d.$moveBackButtonFast.button({
		disabled: true
	});

	mw.libs.commons.api.movePage({
		from: 'File:' + d.to,
		to: 'File:' + d.from,
		reason: 'Not renamed in compliance with the policy (please stick to [[COM:FR]] when renaming files.',
		movetalk: true,
		watchlist: 'nochange',
		cb: function () {
			$buttonset.block({
				message: "Done"
			});
			setTimeout(function () {
				$buttonset.unblock();
			}, 2000);
		},
		errCb: function (err) {
			$tr.block({
				message: "ERROR " + err
			});
			setTimeout(function () {
				$buttonset.unblock();
			}, 10000);
		}
	});
};
var _onMoveBackClick = function () {
	mw.loader.using(['ext.gadget.libJQuery'], $.proxy(__onMoveBackClick, this));
};
var __onMoveBackClick = function () {
	var d = $(this).closest('tr').data('movedata'),
		$autobox = d.auto;

	var $mb_dlg = $('<div>').attr({
		title: "Moving file “" + d.to + "” back to “" + d.from + "”",
		'class': 'pfml-mb-dlg'
	});

	var $r_mb_pfml_wrap = $('<div>').appendTo($mb_dlg),
		$r_mb_pfml_l = $('<label>').attr({
			'for': 'r_mb_pfml'
		}).text("Reason for moving back (this must be a better one than the provided default one)")
			.appendTo($r_mb_pfml_wrap),
		$r_mb_pfml = $('<textarea>').attr({
			id: 'r_mb_pfml',
			style: 'width: 98%; height: 3.5em'
		}).text("Not renamed in compliance with the policy (please stick to [[COM:FR]] when renaming files as MediaWiki\'s file moving implementation suffers from several issues)")
			.appendTo($r_mb_pfml_wrap),
		$redir_mb_pfml_wrap = $('<div>').appendTo($mb_dlg),
		$redir_mb_pfml = $('<input type="checkbox" id="redir_mb_pfml" checked="checked"/>')
			.appendTo($redir_mb_pfml_wrap),
		$redir_mb_pfml_l = $('<label>').attr({
			'for': 'redir_mb_pfml'
		}).text("Leave redirect")
			.appendTo($redir_mb_pfml_wrap),
		$usr_mb_pfml_wrap = $('<div>').appendTo($mb_dlg),
		$usr_mb_pfml = $('<input type="checkbox" id="usr_mb_pfml"/>')
			.appendTo($usr_mb_pfml_wrap),
		$usr_mb_pfml_l = $('<label>').attr({
			'for': 'usr_mb_pfml'
		}).text("Send a reminder to the file mover to stick to [[COM:FR]]")
			.appendTo($usr_mb_pfml_wrap),
		$usr_rm_right_mb_pfml_wrap = $('<div>').appendTo($mb_dlg),
		$usr_rm_right_mb_pfml = $('<input type="checkbox" id="usr_rm_right_mb_pfml"/>')
			.appendTo($usr_rm_right_mb_pfml_wrap),
		$usr_rm_right_mb_pfml_l = $('<label>').attr({
			'for': 'usr_rm_right_mb_pfml'
		}).text("Remove file mover user right for abusing it providing this one as an example")
			.appendTo($usr_rm_right_mb_pfml_wrap);


	var _onMoveBackSubmit = function () {
		$autobox[0].checked = false;
		$autobox.triggerHandler('change');

		d.$moveBackButton.button({
			disabled: true
		});
		d.$moveBackButtonFast.button({
			disabled: true
		});

		$(this).dialog('close');
		AQD.cdMoveBack({
			movedata: d,
			reason: $r_mb_pfml.val(),
			notifyuser: $usr_mb_pfml[0].checked,
			redirect: $redir_mb_pfml[0].checked,
			removefromfilemovergroup: $usr_rm_right_mb_pfml[0].checked
		});
	};

	$mb_dlg.dialog({
		modal: true,
		width: 600,
		buttons: {
			"Move back": _onMoveBackSubmit,
			"Cancel": function () {
				$(this).dialog('close');
			}
		},
		close: function () {
			$(this).remove();
		},
		open: function () {
			$r_mb_pfml.select();
			var $buttons = $(this).parent().find('.ui-dialog-buttonpane button');
			$buttons.eq(0).specialButton('proceed');
			$buttons.eq(1).specialButton('cancel');
		}
	});

};

$.each(matrix, function (i, el) {
	var $mbb = el.$moveBackButton = $('<button>', {
		text: "with opt.",
		role: 'button',
		title: "Move back with options"
	})
		.button({
			icons: {
				primary: 'ui-icon-arrowthick-1-w'
			},
			disabled: true,
			text: false
		}).click(_onMoveBackClick),
		$mbbf = el.$moveBackButtonFast = $('<button>', {
			text: "immediately",
			role: 'button',
			title: "Move back immediately"
		})
			.button({
				icons: {
					primary: 'ui-icon-circle-triangle-w'
				},
				disabled: true,
				text: false
			}).click(_onMoveBackImmediateClick),
		$tr = $('<tr>').append(
			$('<td>', {
			style: 'max-width:35%'
		}).append(
			$('<a>', {
			text: el.from,
			href: el.fromHref,
			target: '_blank'
		}),
			' ',
			$('<a>', {
			text: '(gu)',
			href: mw.util.getUrl('Special:GlobalUsage/' + el.from),
			target: '_blank'
		})),
			$('<td>').append(
			$('<a>', {
			text: el.to,
			href: el.toHref,
			target: '_blank'
		}),
			' ',
			$('<a>', {
			text: '(hist)',
			href: el.toHref + '?action=history',
			target: '_blank'
		})),
			$('<td>', {
			style: 'min-width:70px'
		}).append(
			$('<div>').append($mbb, $mbbf).buttonset()),
			$('<td>', {
			'class': el.isProblem ? 'ui-state-highlight' : ''
		}).append(
			$('<span>', {
			text: el.isProblem
		})));
	el.reasonArea = $('<textarea></textarea>', {
		cols: 40,
		rows: 2,
		style: 'width:99%'
	}).val(el.reason);
	$('<td>', {
		style: 'min-width:35%'
	}).append(el.reasonArea).appendTo($tr);

	el.$details = $('<td>').appendTo($tr);

	el.manual = $('<input>', {
		type: 'checkbox'
	}).change(_updateCount).click(_updateCount);
	el.auto = $('<input>', {
		type: 'checkbox'
	}).change(_updateCount).click(_updateCount);
	if (el.isProblem) el.manual[0].checked = true;
	if (!el.isProblem) el.auto[0].checked = true;
	$('<td>').append(el.manual).appendTo($tr).click(function (e) {
		_updateCount(e);
		el.manual[0].checked = !el.manual[0].checked;
	});
	$('<td>').append(el.auto).appendTo($tr).click(function (e) {
		_updateCount(e);
		el.auto[0].checked = !el.auto[0].checked;
	});
	$tr.appendTo($tbody).data('movedata', el);
});

var $problemCount = $('<td>', {
	text: problemCount
}),
	$manualCount = $('<td>', {
		text: 0
	}),
	$autoCount = $('<td>', {
		text: 0
	}),
	$tf = $('<tfoot>').append($('<tr>').append($.parseHTML('<td></td><td></td><td></td>'), $problemCount, $.parseHTML('<td></td><td></td>'), $manualCount, $autoCount)).appendTo($table);

_updateCount();

var $abuseStatusNote = $('<div>', {
	id: 'AbuseFilterStatus',
	text: 'AbuseFiler Status: ...'
});
var $delinkerStatus = $('<div>', {
	id: 'CommonsDelinkerStatus'
});

$dlg.append($abuseStatusNote, $delinkerStatus, $table);

AQD.showProgress();

var w = Math.min($(window).width(), 1200);
$dlg.dialog({
	'title': "Commons Delinker - Universal Replace Transfer",
	'width': w,
	'height': $(window).height(),
	'buttons': {
		'Execute': function () {
			$submitButton.button('option', 'disabled', true);
			AjaxQuickDelete.cdMoveRequests(matrix);
			setTimeout(function () {
				$submitButton.button('option', 'disabled', false);
			}, 1000);
		}
	},
	'open': function () {
		// Look out for http://bugs.jqueryui.com/ticket/6830 / jQuery UI 1.9
		var $buttons = $(this).parent().find('.ui-dialog-buttonpane button');
		$submitButton = $buttons.eq(0).button({
			icons: {
				primary: 'ui-icon-circle-check'
			}
		});
		if (-1 === $.inArray('sysop', mw.config.get('wgUserGroups'))) $submitButton.button('option', 'disabled', true);
	}
});

// Now get the delinker and AbuseFiler status
var _showAbuseFilterStatus = function (result) {
	if (!result) {
		$abuseStatusNote.text('AbuseFilter Status: page-not-found. Script update required.');
		return false;
	}

	var $r = $(result);
	if (0 === $r.find('#wpFilterEnabled').length) {
		$abuseStatusNote.text('AbuseFilter Status: Unknown. Script update required or you are not logged in your admin account.');
		return false;
	}
	if ($r.find('#wpFilterEnabled').attr('checked')) {
		$abuseStatusNote.text('').append($('<span>', {
			style: 'font-weight:bold;',
			text: 'AbuseFilter Status: '
		})).append($('<span>', {
			style: 'font-weight:bold; color:#77E9C7',
			text: 'ON'
		}));
		$abuseStatusNote.append(' (', $r.find('#mw-abusefilter-edit-hitcount').find('.mw-input > a').css('font-size', '0.8em'), ')<br/>Last modification: ', $r.find('#mw-abusefilter-edit-lastmod').find('.mw-input').css('font-size', '0.8em'));
		return true;
	} else {
		$abuseStatusNote.text('').append($('<span>', {
			style: 'font-weight:bold;',
			text: 'AbuseFilter Status: '
		})).append($('<span>', {
			style: 'font-weight:bold; color:#E977C7',
			text: 'OFF'
		}));
		$abuseStatusNote.append('<br/>Last modification: ', $r.find('#mw-abusefilter-edit-lastmod').find('.mw-input').css('font-size', '0.8em'));
		return true;
	}
};
var _showDelinkerLastContribDate = function (result, user) {
	var $botNode = $('<div>', {
		style: 'font-weight:bold;',
		text: user + ' Status: '
	}).appendTo($delinkerStatus);
	var lastEdit = getDateFromMWDate(result.query.usercontribs[0].timestamp);

	if (currentDate - lastEdit > 3 * 60 * 60 * 1000) {
		$botNode.append($('<span>', {
			style: 'font-weight:bold; color:#E977C7',
			text: 'Last edit ' + lastEdit.toLocaleString()
		}));
	} else {
		$botNode.append($('<span>', {
			style: 'font-weight:bold; color:#77E9C7',
			text: 'Last edit ' + lastEdit.toLocaleString()
		}));
	}
};
$.get(mw.config.get('wgArticlePath').replace('$1', 'Special:AbuseFilter/75'), '', _showAbuseFilterStatus);

var currentDate;
var setCurrentDate = function (x) {
	var shortNames = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
	try {
		var dat = x.getResponseHeader('date').match(/\D+(\d\d) (\D{3}) (\d{4}) (\d\d):(\d\d):(\d\d)/);
		currentDate = new Date(dat[3], $.inArray(dat[2], shortNames), dat[1], dat[4], dat[5], dat[6]);
		// The date is initialized/ constructed in local time but the server returned GMT-Time, so remove the offset
		// According to w3c under- and overflow (<0, >60) are handled by the date-object itself
		currentDate.setMinutes(currentDate.getMinutes() - currentDate.getTimezoneOffset());
	} catch (ex) {
		currentDate = new Date();
	}
};

var getDateFromMWDate = function (stTimestamp) {
	var regex = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})Z$/;
	var m1 = stTimestamp.match(regex);
	var d = new Date(m1[1], m1[2] - 1, m1[3], m1[4], m1[5], m1[6]); // Wer hat sich diesen Unsinn ausgedacht?

	// The date is initialized/ constructed in local time but the server returned GMT-Time, so remove the offset
	// According to w3c under- and overflow (<0, >60) are handled by the date-object itself
	d.setMinutes(d.getMinutes() - d.getTimezoneOffset());
	return d;
};

var doContribsRequest = function (user) {
	$.ajax({
		url: mw.util.wikiScript('api'),
		data: {
			action: 'query',
			format: 'json',
			list: 'usercontribs',
			ucuser: user,
			ucprop: 'timestamp',
			uclimit: 1
		},
		success: function (result, status, x) {
			if (!currentDate && x && x.getResponseHeader) setCurrentDate(x);
			_showDelinkerLastContribDate(result, user);
		}
	});
};
doContribsRequest('CommonsDelinker');

var lastpos = 0,
	$checkMoveDef,
	killer = 0,
	_checkMoveLog = function (result) {
		var les = result.query.logevents,
			pos;

		$.each(les, function (i, le) {
			// get the position of the file in our matrix (remove File:-prefix)
			pos = matrixSrcIndex[le.title.slice(5)];
			if (typeof pos !== 'number') return;
			var mpos = matrix[pos],
				$el = mpos.$details;

			if (mpos.moved_by) return;

			mpos.moved_by = le.user;
			mpos.$moveBackButton.button({
				disabled: false
			});
			mpos.$moveBackButtonFast.button({
				disabled: false
			});

			$('<a>', {
				href: mw.util.getUrl('User talk:' + le.user),
				text: 'Moved by ' + le.user
			}).appendTo($el);
			if (le.params && le.params.target_title !== 'File:' + mpos.to) {
				$('<div>', {
					'class': 'ui-state-highlight',
					text: 'Wrong destination? Log says moved to ' + le.params.target_title
				}).appendTo($el);
			}
		});
		if (lastpos === pos || 0 === pos) {
			killer++;
			if (killer > 2)
				$checkMoveDef.kill = true;
		} else
			killer = 0;
		lastpos = pos;
	};

mw.loader.using('ext.gadget.libAPI', function () {
	$checkMoveDef = mw.libs.commons.api.$autoQuery({
		format: 'json',
		action: 'query',
		list: 'logevents',
		utf8: 1,
		leprop: 'title|user|timestamp|details',
		// not?  lenamespace: "6",
		lelimit: Math.min(matrix.length * 20, 500),
		letype: 'move'
	}, {
		method: 'POST',
		cache: false
	}).progress(_checkMoveLog);
});

}, 1);
});
// </nowiki>