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 13:51, 28 July 2011 (fix). 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.
/* wrappi.js : Javascript wrapper for MediaWiki API *
 * Copyright (c) 2011 [[en:User:R'n'B]]
 * Creative Commons Attribution-ShareAlike License applies
 *
 * 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)
 */
/*global mw, jQuery */ 
 (function ($) {
	var lagpattern = new RegExp("Waiting for [\\d.]+: (\\d+) seconds? lagged"),
		noop = function () {}; // dummy function object to use for hooks
	// 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
	 * options.maxlag: (time in seconds) pause operation if wiki server lag
	 *     equals or exceeds this value; default is 0, which deactivates lag
	 *     checking. This is fine for manual editing scripts, but anything
	 *     that does automated editing should set maxlag:5 (or higher).
	 * options.writeinterval: (time in microseconds) wait at least this long
	 *     between operations that alter the wiki's content or metadata (to
	 *     prevent flooding recent changes; see wiki's bot policy for guidance
	 *     if script is doing automated editing).  Default is 10000 (10 sec.).
	 * options.beforeRequest: a function (taking one argument, an Object
	 *     containing query parameters) to be called before the query is
	 *     submitted to the wiki. Must return an Object containing query
	 *     parameters. Default: returns argument unchanged.
	 * options.uponResponse: a function (taking one argument, a JSON object
	 *     containing the server's response to a query) to be called immediately
	 *     upon receipt of the response and before any error-checking. Must
	 *     return a JSON object to take the place of the server response.
	 *     Default: returns argument unchanged.
	 */
	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: retry count?
			// TODO: hook for user-supplied error handler?
			// hook for user-supplied pre-processing
			var params, sparams, token;
			query = this.settings.beforeRequest(query);
			// remove edit token, if present before serializing
			params = $.extend(true, {}, query);
			if (params.token !== undefined) {
				token = params.token;
				delete params.token;
			}
			sparams = $.param(params);
			// add token back at the end, for security
			if (token !== undefined) {
				sparams = sparams + "&token=" + encodeURIComponent(token);
			}
			$.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, sparams, 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;
			// hook for user-supplied post-processing
			response = this.settings.uponResponse(response);
			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 += 1) {
							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.
		   Arguments: 
				query - an Object containing keys and values for the
					API query parameters
				onsuccess - a Function that is called upon receiving a non-error
					response, and is passed two arguments, the JSON response
					from the server and a copy of query
		 */
		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
		beforeRequest: function (q) { return q; },
		uponResponse: function (r) { return r; }
    };
	/* 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));