User:Krinkle/Tools/Global SUL.js

From Meta, a Wikimedia project coordination wiki

Note: After publishing, 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: Press Ctrl-F5.
/**
 * This script provides an extra Special-page action called "Globalize SUL" which
 * allows easy auto-creation and unification of your SUL account on all local wikis.
 * After enabling the script, the tool is accessible from [[Special:BlankPage/globalsul]].
 *
 * @source https://meta.wikimedia.org/wiki/User:Krinkle/Tools/Global_SUL
 * @revision 2023-02-28
 * @stats [[File:Krinkle_Global_SUL.js]]
 */
/* global mw, $ */
/* jshint laxbreak: true */
(function () {

	/**
	 * Iterate over all Wikimedia Foundation wikis using the SiteMatrix API.
	 *
	 * See also:
	 * - <https://meta.wikimedia.org/wiki/User:Krinkle/Tools/Global_SUL>
	 *
	 * @revision 2012-01-29 <https://meta.wikimedia.org/wiki/User:Krinkle/Scripts/iterate-sitematrix.js>
	 * @example
	 * <code>
	 * var it = new mw.siteMatrix.Iterator({
	 *     centralApiPath: mw.util.wikiScript('api'),
	 *     // iterator {Iterator}
	 *     // wikiObj {Object}: contains 'url', 'dbname' and maybe 'private'
	 *     // iterationNr {Number}: starting at 0
	 *     // listLength {Number}: length of list iteration array
	 *     onIteration: function (iterator, wikiObj, iterationNr, listLength) {
	 *         iterator.next();
	 *     },
	 *     onComplete: function (iterator, wikiObj, listLength) {
	 *     }
	 * });
	 * it.start();
	 * </code>
	 *
	 * @param options {Object}
	 * - centralApiPath string: Path to api of a wiki that has the SiteMatrix installed
	 * - onIteration function: callback for action on each wiki
	 */
	if (mw.siteMatrix === undefined) {
		mw.siteMatrix = {};
	}
	mw.siteMatrix.Iterator = function Iterate(options) {
		var i;
		var current;
		var list;
		var instance = this;
		if (!options.centralApiPath || !options.onIteration) {
			throw new Error('Invalid arguments');
		}
		if (!(this instanceof mw.siteMatrix.Iterator)) {
			throw new TypeError('Illegal function call');
		}
		instance.start = function start() {
			if (i !== undefined) {
				throw new Error('Cannot start twice');
			}
			$.getJSON(options.centralApiPath + '?format=json&action=sitematrix&callback=?', function (data) {
				i = 0;
				list = [];
				if (!data || !data.sitematrix) {
					return;
				}
				$.each(data.sitematrix, function (key, value) {
					if (key === 'count') return;
					var group = key === 'specials' ? value : value.site;
					if ($.isArray(group) && group.length) {
						for (var wi = 0; wi < group.length; wi += 1) {
							 // Only public wikis
							if (group[wi].private === undefined && group[wi].closed === undefined && group[wi].fishbowl === undefined) {
								list.push(group[wi]);
							}
						}
					}
				});
				instance.next();
			});
		};
		instance.next = function next() {
			if (i < list.length) {
				current = list[i];
				options.onIteration(instance, current, i, list.length);
				i += 1;
			} else {
				options.onComplete(instance, current, list.length);
			}
		};
		return instance;
	};

	function initGlobalSUL() {
		var $content = $('#bodyContent').empty();
		var $fieldset = $('<fieldset>');
		var $subtitle = $('<div id="contentSub"></div>');
		var $table = $(
			'<table class="wikitable" style="width: 100%;"><tbody>'
				+ '<tr><th>Status</th><th>Progress</th></tr>'
				+ '<tr>'
					+ '<td id="mw-globalsul-status" style="width: 80%;">Ready for action! <button id="mw-globalsul-start">Start</button></td>'
					+ '<td style="vertical-align: top;"><span id="mw-globalsul-progress">0%</span>'
						+ '<span id="mw-globalsul-done" style="float: right;"></span>'
					+ '</td>'
				+ '</tr>'
				+ '<tr>'
				+ '<td colspan="2" id="mw-globalsul-log" style="padding-top: 1em;"><ul></ul></td>'
				+ '</tr></tbody></table>'
		);

		var $status = $table.find('#mw-globalsul-status');
		var $btnStart = $table.find('#mw-globalsul-start');
		var $log = $table.find('#mw-globalsul-log > ul');
		var $progress = $table.find('#mw-globalsul-progress');
		var $done = $table.find('#mw-globalsul-done');

		function doUpdate(msg, iterationNr, listLength) {
			$status.text(msg);
			$log.prepend('<li>' + new Date().toString().replace(/^\w+ /, '').replace(/:[^:]+$/, '') + ': ' + mw.html.escape(msg) + '</li>');
			if (iterationNr && listLength) {
				$progress.text((Math.round(((iterationNr) / listLength) * 100 * 10) / 10) + '%');
				$done.text('(' + iterationNr + '/' + listLength + ' wikis)');
			}
		}

		function getGlobalAccountInfo(ok, err) {
			$.ajax({
				url: mw.util.wikiScript('api'),
				dataType: 'json',
				data: {
					format: 'json',
					action: 'query',
					meta: 'globaluserinfo',
					guiuser: mw.user.getName(),
					guiprop: 'merged|unattached'
				},
				success: function (data) {
					if (data && data.query && data.query.globaluserinfo) {
						ok(data.query.globaluserinfo);
					} else {
						err();
					}
				},
				error: err
			});
		}

		// Build front-end
		$('#firstHeading').text('Globalize SUL');
		document.title = 'Globalize SUL - ' + mw.config.get('wgSiteName');

		$fieldset
			.text('Check all public Wikimedia wikis and auto-create and unify your account where it '
				+ 'doesn\'t exist yet.')
			.prepend($('<legend>').text('Create account globally'))
			.append($table);

		// Bind events
		$btnStart.click(function (e) {
			$(this).remove();
			doUpdate('Initializing...');
			var mergedList;
			var unattachedList;
			var attemptedList;
			var iterator = new mw.siteMatrix.Iterator({
				centralApiPath: mw.util.wikiScript('api'),
				onIteration: function (instance, wikiObj, iterationNr, listLength) {
					// All WMF wikis support https, but canonical is still 'http'
					// convert API absolute urls to relative urls so that
					// users of this gadget can be on either http or https and it works
					var url = wikiObj.url.replace(/^https?:/, '');
					var hostname = url.replace(/^\/\/?/, '');
					doUpdate(hostname + ': checking...', iterationNr, listLength);
					if ($.inArray(wikiObj.dbname, mergedList) !== -1) {
						doUpdate(hostname + ': already merged');
						setTimeout(instance.next, 1);
					} else if ($.inArray(wikiObj.dbname, unattachedList) !== -1) {
						doUpdate(hostname + ': local unattached account exists');
						setTimeout(instance.next, 1);
					} else {
						doUpdate(hostname + ': attempting auto-create');
						attemptedList.push(hostname);

						var api = new mw.ForeignApi(wikiObj.url + '/w/api.php');
						// NOTE: This is an empty empty dummy query
						// Any request will work to trigger session setup and thus CentralAuth autocreate
						api.get({
							action: 'query'
						}).then(instance.next).catch(instance.next);
					}
				},
				onComplete: function (instance, wikiObj, listLength) {
					doUpdate('Finished.', listLength, listLength);
					$status.html('Made ' + attemptedList.length + ' auto-create attempts.');
					if (attemptedList.length > 0) {
						$status.append('<ul><li>' + attemptedList.join('</li><li>') + '</li></ul>');
					}
				}
			});

			doUpdate('Getting global account info...');
			getGlobalAccountInfo(function (gui) {
				doUpdate('Global account info received');
				mergedList = [];
				unattachedList = [];
				attemptedList = [];
				if (gui.merged) {
					$.each(gui.merged, function (i, obj) {
						mergedList.push(obj.wiki);
					});
				}
				if (gui.unattached) {
					$.each(gui.unattached, function (i, obj) {
						unattachedList.push(obj.wiki);
					});
				}
				doUpdate('Loading wiki SiteMatrix...');
				iterator.start();
			}, function () {
				doUpdate('Download of global account info failed!');
			});
		});

		// Output
		$content.append($subtitle, $fieldset);
	}

	// Enqueue init
	if (mw.config.get('wgCanonicalSpecialPageName') === 'Blankpage' && mw.config.get('wgTitle').indexOf('/globalsul') > 2) {
		mw.loader.using(['mediawiki.util', 'mediawiki.api'], initGlobalSUL);
	}

}());