User:R'n'B/wrappi.js
Appearance
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump. This code will be executed when previewing this page. |
![]() | Documentation for this user script can be added at User:R'n'B/wrappi. |
/* 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));