User:R'n'B/wrappi.js

/* wrappi.js : Javascript wrapper for MediaWiki API * * Copyright (c) 2011-2012 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.19 or higher and jQuery 1.6 (included with MediaWiki) */ /*global mw, jQuery, console */ (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.api: a mediaWiki.Api object that this wrapper should use to *     submit requests [default is the local wiki's Api] * 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,			lastwriteaction = 0,			queryqueue = [],			handlerequest,			handlefailure,			handleresponse,			process;		this.lock = false;		this.settings = $.extend({}, this.defaults, options);		/* handlerequest: submit the request		 */		handlerequest = function (query, resolve) {			// TODO: retry count?			// TODO: hook for user-supplied error handler?			var params = {},				needspost = function (query) {					// return true if POST action is required					var k, 						total = 0;					if (! query.action) {						throw "Invalid request: no 'action' specified";					}					if ($.inArray(query.action, self.postactions) !== -1) {						return true;					}					if (query.action === "query") {						// special case for action=query						if ((query.action.meta							 && $.inArray(query.action.meta, self.postqueries.meta) !== -1) || (query.action.prop							 && $.inArray(query.action.prop, self.postqueries.prop) !== -1) || (query.action.list							 && $.inArray(query.action.list, self.postqueries.prop) !== -1)) {							return true;						}					}					// make quick-and-dirty estimate of query string length					for (k in query) {						if (query.hasOwnProperty(k)) {							total += k.length + query[k].length;						}					}					// if query leads to a "too long" URI then we need to POST;					// this is a very conservative estimate of how long					// would be "too long"					if (total > 2000) {						return true;					}					return false;				};			// hook for user-supplied pre-processing			query = this.settings.beforeRequest(query);			$.each(query, function (key, val) { if (val === null) { console.log(query); } else // skip any underscored keys if (key.charAt(0) !== '_') { if ($.isArray(val)) { // convert array to a |-separated string params[key] = val.join("|"); } else { // make sure all values are strings params[key] = val.toString; }				}			});			// submit query with inserted callback for post-processing			if (needspost(params)) {				this.settings.api.post(params, options).done( function (data) { console.log("Query succeeded, data received:", data); handleresponse.call(self, data, query, resolve); }		       ).fail( function (code, details, result=null, jqHXR=null) { if (code === 'http') { console.log("Query failed, status:", details.textStatus, 			           	            "; error thrown:", details.exception); handlefailure.call(self, query, details.textStatus, details.exception, resolve); } else { console.log("Query failed, API error: ", code) }		           }            	);			} else {				this.settings.api.get(params, options).done( function (data) { console.log("Query succeeded, data received:", data); handleresponse.call(self, data, query, resolve); }		       ).fail( function (code, details, result=null, jqHXR=null) { if (code === 'http') { console.log("Query failed, status:", details.textStatus, 			           	            "; error thrown:", details.exception); handlefailure.call(self, query, details.textStatus, details.exception, resolve); } else { console.log("Query failed, API error: ", code) }		           }	            );			}		};		handlefailure = function (query, status, errorThrown, resolve) {			var answer = false; /*confirm( "Ajax query failed; status '" + status + "'; error code '" + errorThrown +"'. Retry?"); */			if (answer) {				handlerequest(query, resolve);			} else {				// give up				this.lock = false;				if (queryqueue.length > 0) {					process.call(this);				}			}		};		/* 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   		console.log("Processing response:", response);			response = self.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);					console.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 {					self.lock = false;					alert("API error " + response.error.code + ": '" + response.error.info + "'.");				}			} else {				self.lock = false;				if (response.warnings) {					for (module in response.warnings) 					if (response.warnings.hasOwnProperty(module)) {						console.log(module, response.warnings[module]);						if (response.warnings[module].hasOwnProperty("*")) {							warningtext = response.warnings[module]["*"].split("\n");						} else {							warningtext = response.warnings[module].split("\n");						}						for (i = 0; i < warningtext.length; i += 1) {							console.log("API warning in " + module + " module: " + warningtext[i]);						}					}				}				callback(response, query); // this resolves the				// Promise returned by this.request			}			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					|| this.lock) {					// there's another instance running, so forget it				return;			}			this.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.apply(this, item);		};		/* 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; any keys beginning with "_" will not					be passed to the API, but will be kept in the query object					for possible use by the callback				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			Returns: a jQuery Promise that will be resolved (with the same two arguments as passed to onsuccess) when the request has been 				submitted to the server and a response received		 */		this.request = function(query, onsuccess) {			var dfd = new $.Deferred;			if (onsuccess !== undefined) {				dfd.then(onsuccess);			}			if (query.action === undefined) {				console.log(query);				throw "Invalid query: no 'action' parameter.";			}			query.format = 'json';			if (this.settings.maxlag !== 0) {				query.maxlag = this.settings.maxlag;			}			queryqueue.push([query, dfd.resolve]);			if (queryqueue.length === 1) {				// this is the first item on queue, so start processor				process.call(this);			}			return dfd.promise;		};   this.handlerequest = handlerequest;    this.handlefailure = handlefailure;    this.handleresponse = handleresponse;    this.queryqueue = queryqueue;    this.process = process;	};	mw.RnB.Wiki.prototype.defaults = {		api: new mw.Api,		url:  mw.config.get("wgScriptPath") + "/api.php", // OBSOLETE maxlag: 0, // time in seconds write_interval: 10000, // time in microseconds beforeRequest: function (q) { return q; }, uponResponse: function (r) { return r; } };	/* actions that require a POST action */ mw.RnB.Wiki.prototype.postactions = [ "emailcapture", "abusefilterunblockautopromote", "articlefeedback", "articlefeedbackv5-flag-feedback", "articlefeedbackv5", "markashelpful", "moodbar", "feedbackdashboard", "feedbackdashboardresponse", "moodbarsetuseremail", "congresslookup", "stabilize", "review", "reviewactivity", "login", "purge", "rollback", "delete", "undelete", "protect", "block", "unblock", "move", "edit", "upload", "filerevert", "emailuser", "watch", "patrol", "import", "userrights" ];	/* commands within action=query that require POST actions */ mw.RnB.Wiki.prototype.postqueries = { 'list':	["checkuser"], 'prop': [], 'meta': [] }; } (jQuery));