User:Saftzie/app.nuke.js

ShoutWiki — express yourself and be heard!
Jump to navigation Jump to search

Note: After saving, 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)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Go to Menu → Settings (Opera → Preferences on a Mac) and then to Privacy & security → Clear browsing data → Cached images and files.
/**
 * Block a list of users
 * Delete their titles
 *
 * Version 1.0: 25 Aug 2016
 *   Original version for ShoutWiki
 * Version 1.1: 25 Sep 2016
 *   Use query allmessages
 * Version 1.2: 21 Nov 2016
 *   Control block vs look-up only
 * Version 1.2.1: 1 Mar 2023
 *   Update obsolete token
 */
(function (mw, $) {
	'use strict';

	var
		PREFIX = 'js-nuke',
		urlAPI = mw.config.get('wgScriptPath') + '/api.php',
		statusBlock, statusDelete;

	// append text to textarea e
	// autoscroll by setting selection
	// less screen jump by setting scrollTop
	function status(e, text) {
		var
			t;

		e.value += text + '\n';
		t = e.scrollHeight - e.clientHeight;
		e.scrollTop = (t < 0) ? 0 : t;
		e.selectionStart = e.selectionEnd = e.value.length;
	}

	function populateTitles(user) {
		var
			title = $('#' + PREFIX + '-delete-pages')[0],
			count, pages;

		// get the parent IDs of selected revisions
		// output revision info if there is no parent
		function getParents() {
			var
				MAXREQ = 50;

			++count;
			status(statusBlock, 'Querying parent revisions for "' + user + '"... ' + count);
			$.post(urlAPI, {
				format: 'json',
				action: 'query',
				prop: 'revisions',
				rvprop: 'ids',
				revids: pages.slice(0, MAXREQ).join('|')
			}, function (data) {
				var
					a = [],
					t, i;

				if (data.query) {
					t = data.query.pages;
					// make it an array to sort it
					// because the query unsorts it
					for (i in t) {
						if (t.hasOwnProperty(i)) {
							a.push(t[i]);
						}
					}
					a.sort(function (x, y) {
						return (x.revisions[0].revid < y.revisions[0].revid) ?
							1 : -1;
					});
					for ( i = 0; i < a.length; ++i ) {
						// new pages are pages without a parent
						t = a[i].revisions[0];
						if (t.parentid === 0) {
							title.value += a[i].title + '\n';
						}
					}
					if (pages.length) {
						// there's more data to get
						getParents();
						return;
					}
					status(statusBlock, 'Query pages complete for "' + user + '"');
				} else if (data.error) {
					status(statusBlock, 'Error for "' + user + '" parent contributions:: ' +
						data.error.code + ': ' + data.error.info);
				}
			});
			pages = pages.slice(MAXREQ);
		}

		// get all the user contributions
		// keep only the earliest contribution for each page
		function getPages() {
			var
				MAXREQ = 500,
				o = {
					format: 'json',
					action: 'query',
					list: 'usercontribs',
					uclimit: MAXREQ,
					ucprop: 'ids',
					ucshow: 'new', // mw 1.23+
					ucuser: user
				};

			count = 1;
			pages = {};
			(function loop() {
				status(statusBlock, 'Querying page contributions for "' + user + '"... ' + count);
				$.post(urlAPI, o).done(function (data) {
					var
						a, i;

					if (data.query) {
						a = data.query.usercontribs;
						for ( i = 0 ; i < a.length ; ++i ) {
							// keep only the earliest contribution for each page
							if ((pages[a[i].pageid] === undefined) ||
								(a[i].revid < pages[a[i].pageid])) {
								pages[a[i].pageid] = a[i].revid;
							}
						}
						// find the continuation data, if it exists
						// continue is a reserved word, so quote it
						a = data['continue'] ||
							((a = data['query-continue']) && a.usercontribs);
						if (a) {
							// there's more data to get
							$.extend(o, a);
							++count;
							loop();
							return;
						}
						// make an array of rev IDs for sorting
						a = [];
						for (i in pages) {
							if (pages.hasOwnProperty(i)) {
								a.push(pages[i]);
							}
						}
						if (a.length === 0) {
							status(statusBlock, 'No contributions found for "' + user + '"');
							return;
						}
						a.sort(function (x, y) {
							// descending rev IDs
							return (x < y) ? 1 : -1;
						});
						pages = a;
						count = 0;
						getParents();
					} else if (data.error) {
						status(statusBlock, 'Error for "' + user + '" contributions:: ' +
							data.error.code + ': ' + data.error.info);
					}
				});
			}());
		}

		getPages();
	}

	// handle block "go" click
	// get ready to block
	function onBlock(button) {
		var
			expiry = $('#' + PREFIX + '-block-expiry-select').val(),
			select = $('#' + PREFIX + '-block-reason-select').val(),
			reason = $('#' + PREFIX + '-block-reason-other').val()
				.replace(/^\s+/, '').replace(/\s+$/, ''),
			user = $('#' + PREFIX + '-block-users').val().split('\n'),
			i = 0;

		// loop and block as long as there are users
		function execBlock() {
			var
				u = user.shift();

			if ($('#' + PREFIX + '-auto-titles').prop('checked')) {
				populateTitles(u);
			}
			if ($('#' + PREFIX + '-block-cb').prop('checked')) {
				// action=block requires a POST to "conceal" the token
				$.post(urlAPI, {
					format: 'json',
					action: 'block',
					user: u,
					expiry: expiry,
					reason: reason,
					allowusertalk: 1, // default is to block talk, so allow
//					anononly: 1,      // block only anon users for IP blocks
					autoblock: 1,     // default is not to block IPs, so block
					nocreate: 1,      // default is allow acct create, so block
					reblock: 1,       // overwrite existing block, if any (seems broken sometimes)
					token: mw.user.tokens.get('csrfToken')
				}, function (data) {
					if (data.block) {
						data = data.block;
						status(statusBlock, 'Blocked: "' + data.user + '" (' + data.userID + ') until "' + data.expiry + '" for "' + data.reason + '" (' + data.id + ')');
					} else {
						status(statusBlock, 'Error: "' + u + '"; ' + 
							data.error.code + ': ' + data.error.info);
					}
					if (user.length) {
						execBlock(); // keep going
					} else {
						button.disabled = false; // re-enable Go
					}
				});
			} else {
				button.disabled = false; // re-enable Go
			}
		}

		// parse textbox into a list of users
		while (i < user.length) {
			user[i] = user[i]
				.replace(/^\s+/, '')
				.replace(/\s+$/, '')
				.replace(/_/g, ' ');
			if (user[i]) {
				++i;
			} else {
				user.splice(i, 1);
			}
		}
		$('#' + PREFIX + '-block-users').val(user.join('\n'));
		if (user.length) {
			if (expiry === '0') {
				expiry = $('#' + PREFIX + '-block-expiry-other').val()
					.replace(/^\s+/, '').replace(/\s+$/, '');
			}
			if (select !== '0') {
				if (reason) {
					reason = select + ': ' + reason;
				} else {
					reason = select;
				}
			}
			execBlock();
		} else {
			status(statusBlock, 'No one to block');
			button.disabled = false; // re-enable Go
		}
	}

	// handle delete "go" click
	// get ready to delete
	function onDelete(button) {
		var
			select = $('#' + PREFIX + '-delete-select').val(),
			reason = $('#' + PREFIX + '-delete-other').val()
				.replace(/^\s+/, '').replace(/\s+$/, ''),
			title = $('#' + PREFIX + '-delete-pages').val().split('\n'),
			i = 0;

		// loop and delete as long as there are titles
		function execDelete() {
			var
				t = title.shift();

			// action=delete requires a POST to "conceal" the token
			$.post(urlAPI, {
				format: 'json',
				action: 'delete',
				title: t,
				reason: reason,
				token: mw.user.tokens.get('csrfToken')
			}, function (data) {
				if (data['delete']) {
					data = data['delete'];
					status(statusDelete, 'Deleted: "' + data.title + '" for "' + data.reason + '" (' + data.logid + ')');
				} else {
					status(statusDelete, 'Error: ' + t + '; ' + 
						data.error.code + ': ' + data.error.info);
				}
				if (title.length) {
					execDelete(); // keep going
				} else {
					button.disabled = false; // re-enable Go
				}
			});
		}

		// parse textbox into a list of titles
		while (i < title.length) {
			title[i] = title[i]
				.replace(/^\s+/, '')
				.replace(/\s+$/, '')
				.replace(/_/g, ' ');
			if (title[i]) {
				++i;
			} else {
				title.splice(i, 1);
			}
		}
		$('#' + PREFIX + '-delete-pages').val(title.join('\n'));
		if (title.length) {
			if (select !== '0') {
				if (reason) {
					reason = select + ': ' + reason;
				} else {
					reason = select;
				}
			}
			execDelete();
		} else {
			status(statusDelete, 'Nothing to delete');
			button.disabled = false; // re-enable Go
		}
	}

	// parse ipboptions
	function makeBlockExpiry(data) {
		var
			line = data.split(','),
			s = '',
			i;

		for ( i = 0 ; i < line.length ; ++i ) {
			s += '<option value="' + line[i].split(':')[1] + '">' + line[i].split(':')[0] + '</option>';
		}
		$('#' + PREFIX + '-block-expiry-select').append(s);
	}

	// parse ipbreason-dropdown
	function makeBlockReasonSelect(data) {
		var
			line = data.split('\n'),
			s = '',
			level, i;

		for ( i = 0 ; i < line.length ; ++i ) {
			level = line[i].search(/[^\*]/);
			line[i] = line[i]
				.replace(/^\*+/, '')
				.replace(/^\s+/, '')
				.replace(/\s+$/, '');
			if (level === 1) {
				if (s.length) {
					s += '</optgroup>';
				}
				s += '<optgroup label="' + line[i] + '">';
			} else { // level === 2
				s += '<option value="' + line[i] + '">' + line[i] + '</option>';
			}
		}
		if (s.length) {
			s += '</optgroup>';
		}
		$('#' + PREFIX + '-block-reason-select').append(s);
	}

	// parse deletereason-dropdown
	function makeDeleteSelect(data) {
		var
			line = data.split('\n'),
			s = '',
			level, i;

		for ( i = 0 ; i < line.length ; ++i ) {
			level = line[i].search(/[^\*]/);
			line[i] = line[i]
				.replace(/^\*+/, '')
				.replace(/^\s+/, '')
				.replace(/\s+$/, '');
			if (level === 1) {
				if (s.length) {
					s += '</optgroup>';
				}
				s += '<optgroup label="' + line[i] + '">';
			} else { // level === 2
				s += '<option value="' + line[i] + '">' + line[i] + '</option>';
			}
		}
		if (s.length) {
			s += '</optgroup>';
		}
		$('#' + PREFIX + '-delete-select').append(s);
	}

	// check for the signature ID & exit if not found
	// otherwise, crank it up
	$(function () {
		var
			s;

		if ($('#' + PREFIX).length === 0) {
			return; // nothing to do
		}
		$('#' + PREFIX).empty();
		// get system messages
		$.post(urlAPI, {
			format: 'json',
			action: 'query',
			meta: 'allmessages',
			ammessages: 'ipboptions|ipbreason-dropdown|deletereason-dropdown'
		}, function (data) {
			var
				i, d;

			// asked for 3 messages, so should get 3 messages
			if ((data = data.query) && (data = data.allmessages) &&
				$.isArray(data) && (data.length === 3)) {
				for ( i = 0; i < data.length; ++i ) {
					d = data[i];
					switch (d.normalizedname) {
					case 'ipboptions':
						if (d['*']) {
							makeBlockExpiry(d['*']);
						} else {
							status(statusBlock, 'ipboptions not found');
						}
						break;
					case 'ipbreason-dropdown':
						if (d['*']) {
							makeBlockReasonSelect(d['*']);
						} else {
							status(statusBlock, 'ipbreason-dropdown not found');
						}
						break;
					case 'deletereason-dropdown':
						if (d['*']) {
							makeDeleteSelect(d['*']);
						} else {
							status(statusDelete, 'deletereason-dropdown not found');
						}
					}
				}
			} else {
				status(statusBlock, 'message error');
				status(statusDelete, 'message error');
			}
		});
		s = $(String.prototype.concat(
			'<fieldset>',
				'<legend>Block and Delete</legend>',
				'<div style="display: table;">',
					'<fieldset>',
						'<legend>Block</legend>',
						'<table><tbody>',
							'<tr>',
								'<td><label for="' + PREFIX + '-block-expiry-select">Expiry: </label></td>',
								'<td><select id="' + PREFIX + '-block-expiry-select">',
									'<option value="0">Other period</option>',
								'</select></td>',
							'</tr>',
							'<tr>',
								'<td><label for="' + PREFIX + '-block-expiry-other">Other expiry: </label></td>',
								'<td><input type="text" id="' + PREFIX + '-block-expiry-other" maxlength="255" size="60"/></td>',
							'</tr>',
							'<tr>',
								'<td><label for="' + PREFIX + '-block-reason-select">Reason: </label></td>',
								'<td><select id="' + PREFIX + '-block-reason-select">',
									'<option value="0">Other reason</option>',
								'</select></td>',
							'</tr>',
							'<tr>',
								'<td><label for="' + PREFIX + '-block-reason-other">Other/Additional reason: </label></td>',
								'<td><input type="text" id="' + PREFIX + '-block-reason-other" maxlength="255" size="60"/></td>',
							'</tr>',
							'<tr>',
								'<td></td><td>',
									'<input type="checkbox" id="' + PREFIX + '-block-cb" checked="checked" style="vertical-align: middle;"/>',
									'<label for="' + PREFIX + '-block-cb" style="vertical-align: middle;"> Block users</label>',
									'<input type="checkbox" id="' + PREFIX + '-auto-titles" checked="checked" style="margin-left: 1em; vertical-align: middle;"/>',
									'<label for="' + PREFIX + '-auto-titles" style="vertical-align: middle;"> Auto-populate article titles</label>',
								'</td>',
							'</tr>',
						'</tbody></table>',
						'<div style="text-align: right;">',
							'<input type="button" id="' + PREFIX + '-block-go" value="Go"/>',
						'</div>',
						'<div>',
							'<span>Users to block:</span>',
							'<textarea id="' + PREFIX + '-block-users" rows="8" style="resize: none;"></textarea>',
						'</div>',
						'<div>',
							'<span>Block status:</span>',
							'<textarea id="' + PREFIX + '-block-status" rows="8" style="resize: none;" readonly="readonly"></textarea>',
						'</div>',
					'</fieldset>',
					'<fieldset>',
						'<legend>Delete</legend>',
						'<table><tbody>',
							'<tr>',
								'<td><label for="' + PREFIX + '-delete-select">Reason: </label></td>',
								'<td><select id="' + PREFIX + '-delete-select">',
									'<option value="0">Other reason</option>',
								'</select></td>',
							'</tr>',
							'<tr>',
								'<td><label for="' + PREFIX + '-delete-other">Other/Additional reason: </label></td>',
								'<td><input type="text" id="' + PREFIX + '-delete-other" maxlength="255" size="60"/></td>',
							'</tr>',
						'</tbody></table>',
						'<div style="text-align: right;">',
							'<input type="button" id="' + PREFIX + '-delete-go" value="Go"/>',
						'</div>',
						'<div>',
							'<span>Articles to delete:</span>',
							'<textarea id="' + PREFIX + '-delete-pages" rows="8" style="resize: none;"></textarea>',
						'</div>',
						'<div>',
							'<span>Delete status:</span>',
							'<textarea id="' + PREFIX + '-delete-status" rows="8" style="resize: none;" readonly="readonly"></textarea>',
						'</div>',
					'</fieldset>',
				'</div>',
			'<//fieldset>'
		));
		statusBlock = $('#' + PREFIX + '-block-status', s)[0];
		statusDelete = $('#' + PREFIX + '-delete-status', s)[0];
		$('#' + PREFIX + '-block-go', s).click(function () {
			this.disabled = true; // no double-clicking
			onBlock(this);
		});
		$('#' + PREFIX + '-delete-go', s).click(function () {
			this.disabled = true; // no double-clicking
			onDelete(this);
		});
		$('#' + PREFIX).empty().append(s);
	});
}(mediaWiki, jQuery));