Jump to content

User:R'n'B/wrappi.js

From Wikipedia, the free encyclopedia
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-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));