Jump to content

User:R'n'B/wrappi.js

From Wikipedia, the free encyclopedia
This is an old revision of this page, as edited by R'n'B (talk | contribs) at 18:18, 3 June 2011 (create). The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.
(diff) ← Previous revision | Latest revision (diff) | Newer revision → (diff)
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.
/* wrappi.js : Javascript wrapper for MediaWiki API *
 * Copyright (c) 2011 [[en:User:R'n'B]]
 *
 * This wrapper is intended as a user script for pages loaded from a MediaWiki
 * wiki; requires MediaWiki 1.17 and jQuery 1.4.2 (included with MediaWiki)
 */
(function ($) {
	var lagpattern = new RegExp("Waiting for [\\d.]+: (\\d+) seconds? lagged");
	// reserve a private namespace.
	if (!mw.RnB) {
		mw.RnB = {};
	}
	// shim if mw.log is not active and console is
	if ( ! mw.config.get("debug") && console !== undefined) {
		mw.log = function() {
			console.log(arguments);
		};
	}
	/* Wiki: an object providing methods used to submit requests
	 *       to a wiki's API.
	 * options: an Object containing values for any of the following settings --
	 * options.url: URL of a remote wiki to access; default is to access the 
	 * local wiki on which this script is used
	 */
	mw.RnB.Wiki = function (options) {
		var self = this,
			lock = false,
			lastwriteaction = 0,
			queryqueue = [],
			handlerequest,
			handlefailure,
			handleresponse,
			process;
		
		this.settings = $.extend({}, this.defaults, options);
		if (this.settings.url.charAt(0) === "/") { 
			//local api
			if ( ! mw.config.exists("wgEnableAPI") ||
					! mw.config.get("wgEnableAPI")) {
				throw "API is not enabled on this wiki.";
			}
		}
		/* handlerequest: submit the request
		 */
		handlerequest = function (query, onsuccess) {
			// TODO: hook for user-supplied pre-processing?
			// TODO: retry count?
			// TODO: hook for user-supplied error handler?
			$.ajaxSetup({
				error: function (xhr, textStatus, errorThrown) {
					handlefailure.call(self, query, textStatus, errorThrown, onsuccess);
				}
			});
			// submit query with inserted callback for post-processing
			$.post(this.settings.url, query, function (data) { 
				handleresponse.call(self, data, query, onsuccess);
			});
		};
		handlefailure = function (query, status, errorThrown, onsuccess) {
			var answer = confirm(
				"Ajax query failed; status '" + status 
				+ "'; error code '" + errorThrown +"'. Retry?");
			if (answer) {
				handlerequest(query, onsuccess);
			} else {
				// give up
				lock = false;
				if (queryqueue.length > 0) {
					process.call(self);
				}
			}
		};
		/* handleresponse: check response for errors,
		 * dispatch callback, trigger processing of next request, and
		 * TODO: handle retry on server errors
		 */
		handleresponse = function (response, query, callback) {
			var module, warningtext, lag, pause, i;
			// TODO: hook for user-supplied post-processing?
			if (response.error) {
				if (response.error.code === "maxlag") {
					lag = parseInt(lagpattern.exec(response.error.info)[1], 10);
					// pause half of lag, but >= 5 and <= 60
					pause = Math.min(Math.max(5, lag/2), 60);
					mw.log("Pausing " + pause + " sec. due to database lag: "
							+ response.code.info);
					setTimeout(function() {
							handlerequest.call(self, query, callback);
						}, 1000 * pause);
					// keep the lock on so that no other requests can run
					// during the lag-induced pause
					return;
				} else {
					lock = false;
					alert("API error " + response.error.code +
						  ": '" + response.error.info + "'.");
				}
			} else {
				lock = false;
				if (response.warnings) {
					for (module in response.warnings) 
					if (response.warnings.hasOwnProperty(module)) {
						warningtext = response.warnings[module].split("\n");
						for (i=0; i<warningtext.length; i++) {
							mw.log("API warning in " + module + " module: "
									+ warningtext[i]);
						}
					}
				}
				callback.call(self, response, query);
			}
			if (queryqueue.length > 0) {
				process.call(self);
			}
			
		};
		/* process: check the queryqueue for pending requests and submit
		   the next one if possible
		 */
		process = function() {
			var item, nextitem, now, query, writequeue = [];
			if (queryqueue.length === 0
					// nothing there to process, so forget it
					|| lock) {
					// there's another instance running, so forget it
				return;
			}
			lock = true;
			item = queryqueue.shift();
			query = item[0];
			now = (new Date()).getTime();
			if (now - lastwriteaction < this.settings.write_interval) {
				// write throttle time hasn't expired yet
				while ($.inArray(query.action, this.writeactions) !== -1) {
					// query is a write action, so throttle it
					writequeue.push(item);
					if (queryqueue.length === 0) {
						// this was last query on the queue, so we have to wait
						queryqueue = writequeue;
						setTimeout(lastwriteaction 
						           + this.settings.write_interval - now,
								   process);
						return;
					}
					// try the next one in line
					nextitem = queryqueue.shift();
					query = item[0];
				}
				queryqueue = writequeue.concat(queryqueue);
			}		
			handlerequest.call(self, query, item[1]);
			lock = false;
		};
		/* request: higher-level method that formats parameters, and puts the
		   request on the queue, to prevent overlapping requests to server.
		 */
		this.request = function(query, onsuccess) {
			if (query.action === undefined) {
				mw.log(query);
				throw "Invalid query: no 'action' parameter.";
			}
			query.format = 'json';
			if (this.settings.maxlag !== 0) {
				query.maxlag = this.settings.maxlag;
			}
			query = mw.RnB.Wiki.serialize(query);
			queryqueue.push([query, onsuccess]);
			if (queryqueue.length === 1) {
				// this is the first item on queue, so start processor
				process.call(self);
			}
		};
	};
	mw.RnB.Wiki.prototype.defaults = {
		url:  mw.config.get("wgScriptPath") + "/api.php",
		maxlag:  0, // time in seconds
		write_interval:  10000 // time in microseconds
    };
	/* actions that require write privileges */
	mw.RnB.Wiki.prototype.writeactions = [
		'review', 'emailcapture', 'articlefeedback', 'stabilize', 'purge',
		'rollback', 'delete', 'undelete', 'protect', 'block', 'unblock', 
		'move', 'edit', 'upload', 'emailuser', 'watch', 'patrol', 'import', 
		'userrights'
	];
	/* serialize: convert any arrays or numbers in param values to strings
	 *     returns converted query object
	 */
	mw.RnB.Wiki.serialize = function (query) {
		var key,
			params = {};
		for (key in query) {
			if (query.hasOwnProperty(key)) {
				if ($.isArray(query[key])) {
					// serialize any array values
					params[key] = query[key].join("|");
				} else {
					// stringify all other values
					params[key] = query[key].toString();
				}
			}
		}
		return params;
	};
} (jQuery));