Jump to content

MediaWiki:Unblock-wizard.js

From Wikipedia, the free encyclopedia
This is an old revision of this page, as edited by Chaotic Enby (talk | contribs) at 18:39, 15 April 2025 (username). The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
/**
 * User:Chaotic Enby/Unblock wizard.js
 *
 * JavaScript used for submitting unblock requests.
 * Used on [[User:Chaotic Enby/Unblock wizard]].
 * Loaded via [[mw:Snippets/Load JS and CSS by URL]].
 * 
 * Edits can be proposed via pinging me somewhere.
 *
 * Author: [[User:Chaotic Enby]] (derived from a script by [[User:SD0001]])
 * Licence: MIT
 */

/* jshint maxerr: 999 */
/* globals mw, $, OO */
/* <nowiki> */

(function () {

$.when(
	$.ready,
	mw.loader.using([
		'mediawiki.util', 'mediawiki.api', 'mediawiki.Title',
		'mediawiki.widgets', 'oojs-ui-core', 'oojs-ui-widgets'
	])
).then(function () {
	if (!(mw.config.get('wgPageName').includes('User:Chaotic_Enby/Unblock_wizard/')) ||
		mw.config.get('wgAction') !== 'view') {
		return;
	}
	init();
});

var afc = {}, ui = {};
window.afc = afc;
afc.ui = ui;

var config = {
	debounceDelay: 500,
	redirectionDelay: 1000,
	defaultAfcTopic: 'other'
};

// TODO: move to a separate JSON subpage, would be feasible once [[phab:T198758]] is resolved
var messages = {
	"document-title": "Wikipedia Unblock Wizard",
	"page-title": "Wikipedia Unblock Wizard",
	"fieldset-label": "I was blocked for promotional activity",
	"explain-label": "Can you explain, in your own words, what you were blocked for?",
	"future-label": "If unblocked, what edits would you make, and what (if applicable) would you do differently?",
	"other-label": "Is there anything else that may be helpful to your unblock request?",
	"explain-promo-label": "Can you explain, in your own words, why your edits were or were not promotional?",
	"coi-label": "What is your relationship with the subjects you have been editing about?",
	"future-promo-label": "If unblocked, would you make edits outside of that topic?",
	"username-label": "What new username do you want to pick?",
	"submit-label": "Submit",
	"footer-text": "<small>If you are not sure about what to enter in a field, you can skip it. If you need help, you can ask on <b>[[Special:MyTalk|your talkpage]]</b> with <b>{{[[Template:Help me|Help me]]}}</b> or get live help via <b>[[WP:IRCHELP|IRC]]</b> or <b>[[WP:DISCORD|Discord]]</b>.<br>Facing some issues in using this form? <b>[/w/index.php?title=User_talk:Chaotic_Enby&action=edit&section=new&preloadtitle=Issue%20with%20submission%20form&editintro=User_talk:Chaotic_Enby/editintro Report it]</b>.</small>",
	"submitting-as": "Submitting as User:$1",
	"validation-notitle": "User not found",
	"validation-invalidtitle": "User page does not exist.",
	"validation-missingtitle": "User page does not exist.",
	"warning-norefs": "This draft doesn't appear to contain any references. Please add references, without which it is likely to be declined. See [[Help:Introduction to referencing with Wiki Markup/2|help on adding references]].",
	"status-processing": "Processing ...",
	"status-saving": "Saving draft page ...",
	"editsummary-main": "Submitting using [[WP:AFCSW|AfC-submit-wizard]]",
	"status-redirecting": "Submission succeeded. Redirecting you to your talk page ...",
	"captcha-label": "Please enter the letters appearing in the box below",
	"captcha-placeholder": "Enter the letters here",
	"captcha-helptip": "CAPTCHA security check. Click \"Submit\" again when done.",
	"error-saving-main": "An error occurred ($1). Please try again or refer to the help desk.",
	"status-saving-talk": "Saving draft talk page ...",
	"editsummary-talk": "Adding WikiProject tags using [[WP:AFCSW|AfC-submit-wizard]]",
	"status-talk-success": "Successfully added WikiProject tags to talk page",
	"error-saving-talk": "An error occurred in editing the talk page ($1).",
	"error-main": "An error occurred ($1). Please try again or refer to the help desk."
};

var questionLabels = [];
var questionFields = {'explain': 0, 'future': 0, 'other': 0, 'explain-promo': 0, 'coi': 0, 'future-promo': 0, 'username': 1};

function init() {
	for (var key in messages) {
		mw.messages.set('afcsw-' + key, messages[key]);
	}
	
	switch (mw.config.get('wgPageName')) {
		case "User:Chaotic_Enby/Unblock_wizard/Promo":
			questionLabels = ['explain-promo', 'coi', 'future-promo', 'username', 'other'];
			break;
		case "User:Chaotic_Enby/Unblock_wizard/Other":
			questionLabels = ['explain', 'future', 'other'];
			break;
		default:
			questionLabels = [];
	}

	document.title = msg('document-title');
	$('#firstHeading').text(msg('page-title'));
	
	mw.util.addCSS(
		// CSS adjustments for vector-2022: hide prominent page controls which are
		// irrelevant and confusing while using the wizard
		'.vector-page-toolbar { display: none } ' +
		'.vector-page-titlebar #p-lang-btn { display: none } ' + 
		
		// Hide categories as well, prevents accidental HotCat usage
		'#catlinks { display: none } '
	);

	var apiOptions = {
		parameters: {
			format: 'json',
			formatversion: '2'
		},
		ajax: {
			headers: {
				'Api-User-Agent': 'w:en:User:Chaotic Enby/Unblock wizard.js'
			}
		}
	};

	// Two different API objects so that aborts on the lookupApi don't stop the final
	// evaluate process
	afc.api = new mw.Api(apiOptions);
	afc.lookupApi = new mw.Api(apiOptions);

	constructUI();
}

function constructUI() {
	ui.itemsLayout = [];
	ui.itemsInput = [];
	
	for(var label of questionLabels){
		switch(questionFields(label)) {
			case 0:
				ui.itemsInput.push(new OO.ui.MultilineTextInputWidget({
						// placeholder: msg(label + '-placeholder'),
						multiline: true,
						autosize: true,
					}));
				break;
			case 1:
				ui.itemsInput.push(new OO.ui.TextInputWidget({
						// placeholder: msg(label + '-placeholder'),
						maxLength: 85,
					}));
				break;
			default:
				debug("Field type not found");
		}
		ui.itemsLayout.push(new OO.ui.FieldLayout(ui.itemsInput[ui.itemsInput.length - 1], {
				label: msg(label + '-label'),
				align: 'top',
				// help: msg(label + '-helptip'),
				helpInline: true
			}));
	}
	
	ui.itemsLayout.push(ui.submitLayout = new OO.ui.FieldLayout(ui.submitButton = new OO.ui.ButtonWidget({
			label: msg('submit-label'),
			flags: [ 'progressive', 'primary' ],
		})));

	ui.fieldset = new OO.ui.FieldsetLayout({
		classes: [ 'container' ],
		items: ui.itemsLayout
	});

	ui.footerLayout = new OO.ui.FieldLayout(new OO.ui.LabelWidget({
		label: $('<div>')
			.append(linkify(msg('footer-text')))
	}), {
		align: 'top'
	});

	var asUser = mw.util.getParamValue('username');
	if (asUser && asUser !== mw.config.get('wgUserName')) {
		ui.fieldset.addItems([
			new OO.ui.FieldLayout(new OO.ui.MessageWidget({
				type: 'notice',
				inline: true,
				label: msg('submitting-as', asUser)
			}))
		], /* position */ 5); // just before submit button
	}

	// Attach
	$('#unblock-wizard-container').empty().append(ui.fieldset.$element, ui.footerLayout.$element);
	
	mw.track('counter.gadget_afcsw.opened');

	ui.submitButton.on('click', handleSubmit);

	initLookup();

	// The default font size in monobook and modern are too small at 10px
	mw.util.addCSS('.skin-modern .projectTagOverlay, .skin-monobook .projectTagOverlay { font-size: 130%; }');

	afc.beforeUnload = function (e) {
		e.preventDefault();
		e.returnValue = '';
		return '';
	};
	$(window).on('beforeunload', afc.beforeUnload);
}

function initLookup() {
	afc.lookupApi.abort(); // abort older API requests

	var userTalk = "User talk:" + mw.config.get('wgUserName');
	if (!mw.config.get('wgUserName')) { // empty
		return; // here we should get the ip or something
	}
	debug('user talk page: "' + userTalk + '"');

	// re-initialize
	afc.oresTopics = null;
	afc.talktext = null;
	afc.pagetext = null;

	afc.lookupApi.get({
		"action": "query",
		"prop": "revisions|description|info",
		"titles": userTalk,
		"rvprop": "content",
		"rvslots": "main"
	}).then(setPrefillsFromPageData);

	var titleObj = mw.Title.newFromText(drafttitle);
	if (!titleObj || titleObj.isTalkPage()) {
		return;
	}
	var talkpagename = titleObj.getTalkPage().toText();
	afc.lookupApi.get({
		"action": "query",
		"prop": "revisions",
		"titles": talkpagename,
		"rvprop": "content",
		"rvslots": "main",
	}).then(setPrefillsFromTalkPageData);

}

function setPrefillsFromPageData(json) {
	debug('page fetch query', json);
	var page = json.query.pages[0];
	var preNormalizedTitle = json.query.normalized && json.query.normalized[0] &&
		json.query.normalized[0].from;
	debug('page.title: "' + page.title + '"');
	var errors = errorsFromPageData(page);
	if (errors.length) {
		return;
	}

	afc.pagetext = page.revisions[0].slots.main.content;
}

function setPrefillsFromTalkPageData (json) {
	var talkpage = json.query.pages[0];
	if (!talkpage || talkpage.missing) {
		return;
	}
	afc.talktext = talkpage.revisions[0].slots.main.content;
	debug(afc.talktext);

	var existingWikiProjects = extractWikiProjectTagsFromText(afc.talktext);
	var existingTags = existingWikiProjects.map(function (e) {
		return e.name;
	});
	debug(existingTags);
	ui.addTalkTags(existingTags);
}

/**
 * @param {Object} page - from query API response
 * @returns {string[]}
 */
function errorsFromPageData(page) {
	if (!page || page.invalid) {
		return [msg('validation-invalidtitle')];
	}
	if (page.missing) {
		return [msg('validation-missingtitle')];
	}
	return [];
}

/**
 * @param {Object} page - from query API response
 * @returns {string[]}
 */
function warningsFromPageData(page) {
	var pagetext = page.revisions[0].slots.main.content;

	var warnings = [];

	// Show no refs warning
	if (!/<ref/i.test(pagetext) && !/\{\{([Ss]fn|[Hh]arv)/.test(pagetext)) {
		warnings.push('warning-norefs');
	}

	// TODO: Show warning for use of deprecated/unreliable sources
	// TODO: Show tip for avoiding peacock words or promotional language?

	return warnings.map(function (warning) {
		return new OO.ui.HtmlSnippet(linkify(msg(warning)));
	});
}

/**
 * @param {string} type
 * @param {string} message
 */
function setMainStatus(type, message) {
	if (!ui.mainStatusLayout || !ui.mainStatusLayout.isElementAttached()) {
		ui.fieldset.addItems([
			ui.mainStatusLayout = new OO.ui.FieldLayout(ui.mainStatusArea = new OO.ui.MessageWidget())
		]);
	}
	ui.mainStatusArea.setType(type);
	ui.mainStatusArea.setLabel(message);
}


/**
 * @param {string} type
 * @param {string} message
 */
function setTalkStatus(type, message) {
	if (!ui.talkStatusLayout) {
		ui.fieldset.addItems([
			ui.talkStatusLayout = new OO.ui.FieldLayout(ui.talkStatusArea = new OO.ui.MessageWidget())
		]);
	}
	ui.talkStatusArea.setType(type);
	ui.talkStatusArea.setLabel(message);
}

function handleSubmit() {

	setMainStatus('notice', msg('status-processing'));
	mw.track('counter.gadget_afcsw.submit_attempted');
	ui.submitButton.setDisabled(true);
	ui.mainStatusLayout.scrollElementIntoView();
	
	var userTalk = "User talk:" + mw.config.get('wgUserName');
	if (!mw.config.get('wgUserName')) { // empty
		ui.fieldset.removeItems([ui.mainStatusLayout]);
		ui.submitButton.setDisabled(false);
		return; // really get the ip please
	}
	debug('debug user talk page: "' + userTalk + '"');

	afc.api.get({
		"action": "query",
		"prop": "revisions|description",
		"titles": userTalk,
		"rvprop": "content",
		"rvslots": "main",
	}).then(function (json) {
		var apiPage = json.query.pages[0];

		var errors = errorsFromPageData(apiPage);
		if (errors.length) {
			ui.fieldset.removeItems([ui.mainStatusLayout]);
			ui.submitButton.setDisabled(false);
			debug(errors);
			return;
		}
		debug("no errors");

		var text = prepareUserTalkText(apiPage);
		debug("text[0] = " + text[0]);

		setMainStatus('notice', msg('status-saving'));
		saveUserTalkPage(userTalk, text).then(function () {
			setMainStatus('success', msg('status-redirecting'));
			mw.track('counter.gadget_afcsw.submit_succeeded');

			$(window).off('beforeunload', afc.beforeUnload);
			setTimeout(function () {
				location.href = mw.util.getUrl(userTalk);
			}, config.redirectionDelay);
		}, function (code, err) {
			if (code === 'captcha') {
				ui.fieldset.removeItems([ui.mainStatusLayout, ui.talkStatusLayout]);
				ui.captchaLayout.scrollElementIntoView();
				mw.track('counter.gadget_afcsw.submit_captcha');
			} else {
				setMainStatus('error', msg('error-saving-main', makeErrorMessage(code, err)));
				mw.track('counter.gadget_afcsw.submit_failed');
				mw.track('counter.gadget_afcsw.submit_failed_' + code);
			}
			ui.submitButton.setDisabled(false);
		});
	}).catch(function (code, err) {
		setMainStatus('error', msg('error-main', makeErrorMessage(code, err)));
		ui.submitButton.setDisabled(false);
		mw.track('counter.gadget_afcsw.submit_failed');
		mw.track('counter.gadget_afcsw.submit_failed_' + code);
	});

}

function saveUserTalkPage(title, text) {

	// TODO: handle edit conflict
	var editParams = {
		"action": "edit",
		"title": title,
		"text": text,
		"summary": msg('editsummary-main')
	};
	if (ui.captchaLayout && ui.captchaLayout.isElementAttached()) {
		editParams.captchaid = afc.captchaid;
		editParams.captchaword = ui.captchaInput.getValue();
		ui.fieldset.removeItems([ui.captchaLayout]);
	}
	return afc.api.postWithEditToken(editParams).then(function (data) {
		if (!data.edit || data.edit.result !== 'Success') {
			if (data.edit && data.edit.captcha) {
				// Handle captcha for non-confirmed users

				var url = data.edit.captcha.url;
				afc.captchaid = data.edit.captcha.id; // abuse of global?
				ui.fieldset.addItems([
					ui.captchaLayout = new OO.ui.FieldLayout(ui.captchaInput = new OO.ui.TextInputWidget({
						placeholder: msg('captcha-placeholder'),
						required: true
					}), {
						warnings: [ new OO.ui.HtmlSnippet('<img src=' + url + '>') ],
						label: msg('captcha-label'),
						align: 'top',
						help: msg('captcha-helptip'),
						helpInline: true,
					}),
				], /* position */ 6); // just after submit button // TODO: fix number
				// TODO: submit when enter key is pressed in captcha field

				return $.Deferred().reject('captcha');

			} else {
				return $.Deferred().reject('unexpected-result');
			}
		}
	});
}

/**
 * @param {Object} page - page information from the API
 * @returns {string} final talk page text to save
 */
function prepareUserTalkText(page) {
	var text = page.revisions[0].slots.main.content;

	var unblock = '';
	
	// put unblock template
	unblock += '\n{{unblock|reason=';
	
	for(var [i, label] of questionLabels.entries()){
		unblock += "'''''" + msg(label + '-label') + "'''''" + "{{pb}}" + ui.itemsInput[i].getValue() + "{{pb}}";
	}
		
	unblock += '~~~~}}\n'; // Placeholder text

	// insert everything to the top
	text = text + unblock;
	debug(text);

	return text;
}

/**
 * Load a JSON page from the wiki.
 * Use API (instead of $.getJSON with action=raw) to take advantage of caching
 * @param {string} page
 * @returns {jQuery.Promise<Record<string, any>>}
 **/
function getJSONPage (page) {
	return afc.api.get({
		action: 'query',
		titles: page,
		prop: 'revisions',
		rvprop: 'content',
		rvlimit: 1,
		rvslots: 'main',
		uselang: 'content',
		maxage: '3600', // 1 hour
		smaxage: '3600',
		formatversion: 2
	}).then(function (json) {
		var content = json.query.pages[0].revisions[0].slots.main.content;
		return JSON.parse(content);
	}).catch(function (code, err) {
		console.error(makeErrorMessage(code, err));
	});
}

/**
 * Expands wikilinks and external links into HTML.
 * Used instead of mw.msg(...).parse() because we want links to open in a new tab,
 * and we don't want tags to be mangled.
 * @param {string} input
 * @returns {string}
 */
function linkify(input) {
	return input
		.replace(
			/\[\[:?(?:([^|\]]+?)\|)?([^\]|]+?)\]\]/g,
			function(_, target, text) {
				if (!target) {
					target = text;
				}
				return '<a target="_blank" href="' + mw.util.getUrl(target) +
					'" title="' + target.replace(/"/g, '&#34;') + '">' + text + '</a>';
			}
		)
		// for ext links, display text should be given
		.replace(
			/\[(\S*?) (.*?)\]/g,
			function (_, target, text) {
				return '<a target="_blank" href="' + target + '">' + text + '</a>';
			}
		);
}

function msg(key) {
	var messageArgs = Array.prototype.slice.call(arguments, 1);
	return mw.msg.apply(mw, ['afcsw-' + key].concat(messageArgs));
}

function makeErrorMessage(code, err) {
	if (code === 'http') {
		return 'http: there is no internet connectivity';
	}
	return code + (err && err.error && err.error.info ? ': ' + err.error.info : '');
}

function debug() {
	Array.prototype.slice.call(arguments).forEach(function (arg) {
		console.log(arg);
	});
}

})(); // File-level closure to protect functions from being exposed to the global scope or overwritten

/* </nowiki> */