Jump to content

User:R'n'B/wrappi.js

From Wikipedia, the free encyclopedia
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.
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));