User:Bawolff/mwapilib.js

//Depends on ajax.js /*global sajax_init_object */ //Depends on some config vars defined by mediawiki /*global wgServer wgScriptPath */ //Depends on wikibits.js /*global jsMsg*/

/*jslint bitwise: true, browser: true, eqeqeq: true, immed: true, newcap: true, nomen: true, plusplus: false, regexp: true, undef: true */

/***************************** NOTE: Try user:Bawolff/mwapilib2.js - its much better designed.

/* The purpose of this js file is to create a bunch of functions useful for communicating with the Mediawiki API.

Please note: as with all my edits, this page is PD (unless someone else edits it in which case its cc-by 2.5). so feel free to do with it as you please

There are examples of usage at User:Bawolff/mwapilib. There is some real live examples at:
 * mediawiki:Gadget-easyPeerReview.js
 * User:Bawolff/sandbox/powerFlag (code at mediawiki:Common.js/User:Bawolff/sandbox/powerFlag)

If you use it somewhere, please feel free to add to this list

//So it doesn't conflict with other var names

if (!window.Bawolff) { var Bawolff = {}; } Bawolff.mwapi = {};

/* This is the constructor for a request object. It wraps up the ajax request to api.php in a bunch of prettiness.

This has one optional paramter, an object containing request details. The information can also be specified later

Call as, for example: var foo = new Bawolff.mwapi.Request; or: var foo = new Bawolff.mwapi.Request({action:"query", prop: "info", titles: "Main Page"});

Bawolff.mwapi.Request = function(req, method) { if (false) { throw new Error("API is not enabled. Please contact your wiki webmaster"); }   if (req) { this.req = req; }   else { this.req = {}; }   if (method) { this.setMethod(method); }   else { this.setMethod("GET"); }   this.ajax = null; //defined later }

/* Takes two strings, first is the property name, second is its value. The second property can also optionally be an array, in which case it is turned into a pipe delimited string. If name exists already, it is overridden

For example: var foo = new Bawolff.mwapi.Request; foo.setParam("action", "query"); foo.setParam("titles", ["Main Page", "Wikinews:Sandbox"]);

Bawolff.mwapi.Request.prototype.setParam = function(name, value) { this.req[name] = value; }

//return value of a paramter Bawolff.mwapi.Request.prototype.getParam = function(name) { return this.req[name]; }

//Replace all parameters with an associtive array (aka object) of new params Bawolff.mwapi.Request.prototype.replaceParam = function(value) { this.req = value; }

//Set the method too use. Some api functions don't work with GET. All will work //with post. Heads not allowed here as this pretty little wrapper doesn't give you the //headers. Bawolff.mwapi.Request.prototype.setMethod = function (method) { if (method.match(/^get$/i)) { this.method = "GET"; } else if (method.match(/^post$/i)) { this.method = "POST"; }   else { throw new Error("Invalid Method: " + method + ". Must be one of GET or POST"); } }

Bawolff.mwapi.Request.prototype.toString = function { return "AJAX MW-API wrapper object. Action: " + this.req.action; }

//See Bawolff.mwapi.List below //this is for if you have a chain of api async requests //that should go off one after each other (FIFO) //say foo is an instance of Bawolff.mwapi.Request //and asynQueue is an instance of Bawolff.mwapi.List or Bawolff.mwapi.AyncQueue //do foo.delaySend(asyncQueue, callbackFunc); //and than asyncQueue.start will send out the requests one after another in order. /********
 * example:

//echo is an example call back func used. var echo = function (a, b, c) {alert(a.getElementsByTagName('page')[0].getAttribute('title')+b+c)};

var asyncQueue = new Bawolff.mwapi.List; var foo = new Bawolff.mwapi.Request({action:"query", prop: "info", titles: "Main Page"}); foo.delaySend(asyncQueue, echo); var bar = new Bawolff.mwapi.Request({action:"query", prop: "info", titles: "WN:WC"}); bar.delaySend(asyncQueue, echo); asyncQueue.start; //start sending them one after another.



Bawolff.mwapi.Request.prototype.delaySend = function(callList, callback, errCallback) { //is this a bad circular ref? this.callList = callList; var reqObj = this; callList.add(function {reqObj.send(callback, errCallback);}); }

/*Sending function. This is where gruntwork is done. Takes a minimun on 1 argument, a callback function. If a second argument is supplied it is treated as an error callback, and will be called if something bad happens This wrapper always uses async requests. The callback will be sent two arguments, an xmldocument object with the response to the request, and the ajax object. (Normally you only need the first.) For example:

var callback = function(resp) {....} requestObj.send(callback);

If error is ommited, errors will be dumped to the sitenotice.

Bawolff.mwapi.Request.prototype.send = function(callback, errorfunc) {

this.setParam("format", "xml");

//create the xmlHTTPRequest this.ajax = new XMLHttpRequest; if (!this.ajax) { throw new Error("Client does not support xmlHTTPRequest; (aka does not support ajax)"); }

var i; //general index variable var requestString = ""; for (i in this.req) { if (this.req.hasOwnProperty(i)) { requestString += encodeURIComponent(i) + "=" if (this.req[i] instanceof Bawolff.LazyVar) { //this is for if it is using a global variable that changes between when request object //was created and when it was run. See Bawolff.LazyVar requestString += encodeURIComponent(this.req[i].get); } else { requestString += encodeURIComponent(this.req[i]); }           requestString += "&"; }   }    if (requestString === "") { throw new Error("No request specified. (there's nothing to ask the api.)"); }   requestString = requestString.substring(0, requestString.length - 1);

var uri; var dataToSend; if (this.method === "POST") { uri = mw.config.get('wgServer') + mw.config.get('wgScriptPath') + "/api.php"; dataToSend = requestString; }   else { //aka GET uri = mw.config.get('wgServer') + mw.config.get('wgScriptPath') + "/api.php?" + requestString; dataToSend = null; }

this.ajax.open(this.method, uri, true);

if (!errorfunc) { errorfunc = function(e) { if (e.name === 'NS_ERROR_NOT_AVAILABLE') return; //weird firefox error i can't track down. var msg = "A local JS function experienced an error on an API request. Please leave a note at User_talk:Bawolff. Details: "; msg += e.name + ": " + e.message; msg += " <"; msg += e.lineNumber + ":"; msg += e.fileName + ">"; if (e.name === 'badtoken') msg += ' token=' + Bawolff.mwapi.edit_token + ';'; msg += ' browser=' + navigator.userAgent; mw.notify(document.createTextNode(msg)); }   }

this.ajax.onreadystatechange = Bawolff.mwapi.Request.makeCallback(callback, errorfunc, this.ajax, this.callList); /* circular reference?*/

if (this.method === "POST") { this.ajax.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");

}   this.ajax.setRequestHeader( 'Api-User-Agent', 'https://en.wikinews.org/w/index.php?title=User:Bawolff/mwapilib.js' ); this.ajax.send(dataToSend);

} /*Makes a custom callback. Not meant to be used outside of this file. All three variables required. first two are functions, third is reference to ajax object. The functions are passed first the responseXML or error for the error one, and the second argument is the xmlHTTPRequest object. Bawolff.mwapi.Request.makeCallback = function (callback, errorCallback, ajax, callList) { return (function {       if (ajax.readyState !== 4) {            return;        }        try {            //Check for http errors            if (ajax.status != 200) { /*Intentional != just in case */                throw new Error("Error: " + ajax.status + ajax.statusText);            }

//check for errors raised by API: var APIerror = ajax.responseXML.getElementsByTagName("error"); if (APIerror.length !== 0) { var err = new Error(APIerror[0].getAttribute("info")); err.name = APIerror[0].getAttribute("code"); err.doc = APIerror; throw err; }           callback(ajax.responseXML, ajax); if (callList instanceof Bawolff.mwapi.List) { callList.next; //do the next function in line

}       }

catch (e) { errorCallback(e, ajax); }   });

}

/************************/ /* Do stuff that uses this. First off, page retrival/editing // http://en.wikinews.org/w/api.php?action=query&prop=revisions&titles=API|Main%20Page&rvprop=content|ids //Creates a Bawolff.mwapi.Request for obtaining pages, and calls it //This is NOT a constructor. //returns the request object (most times you won't need it) //Argument is title of page (string), or array of pages to retrieve. //and callback function, which takes an object of {pagename:, ...}

//Delay is optional and must be a Bawolff.mwapi.AsyncQueue or Bawolff.mwapi.List //See docs on those classes for details /* example: callback = function (pages) {some func} Bawolff.mwapi.getPage("Main Page", callback); Bawolff.mwapi.getPage = function(list, callback, delay, followRedirect) { var req, callbackWrapper; if (list instanceof Array) { list = list.join("|"); //turn into string }

//wraps arround callback to make content pretty callbackWrapper = function(xml) { var pages, resp, i;

resp = {}; pages = xml.getElementsByTagName('page'); for (i = 0;i < pages.length;i++) { if (_hasAttribute(pages[i], 'missing')) { throw new Error('Could not retrieve page "' + pages[i].getAttribute('title') + '" as it does not exist.'); }           if (_hasAttribute(pages[i], 'invalid')) { throw new Error('Could not retrieve page "' + pages[i].getAttribute('title') + '" because that is not a valid page name.'); }           if (pages[i].firstChild.firstChild.normalize) pages[i].firstChild.firstChild.normalize; //wtf i don't even know. (firefox bug?) resp[pages[i].getAttribute('title')] = pages[i].firstChild.firstChild.firstChild.data; resp[i] = pages[i].firstChild.firstChild.firstChild.data; }

callback(resp); //callback is inside this functions scope }

req = new Bawolff.mwapi.Request({action: "query", prop: "revisions", titles: list, rvprop: "content|ids"}); if (followRedirect) { req.setParam('redirects', 'true'); }   if (delay) { req.delaySend(delay, callbackWrapper); }   else { req.send(callbackWrapper); }   return req; }

//Creates a Bawolff.mwapi.Request for obtaining edit token //This is NOT a constructor. //returns mwapi.Request object (probably not needed) //Argument is callback function [takes 1 arg that is token, 2nd optional arg is timestamp] //2nd arg is [optional] pagename (string, not array) //http://en.wikinews.org/w/api.php?action=query&prop=info&titles=d&intoken=edit //third arg is optional. must be instance of Bawolff.mwapi.AsyncQueue see docs on that class for details /* example: callback = function (editoken) {some func} Bawolff.mwapi.getToken(callback); Bawolff.mwapi.getToken = function(callback, page, delay) { var req, callbackWrapper; if (!page) { page = "Wikinews:Sandbox"; //arbitrary. doesn't matter as editokens are same for all pages for specific user in specific session }

//wraps arround callback to make content pretty callbackWrapper = function(xml) { var pages, token, time, i;

pages = xml.getElementsByTagName('tokens'); if (_hasAttribute(pages[0], "csrftoken")) { token = pages[0].getAttribute('csrftoken'); // Different api module. Hopefully nobody cared about this. //time = pages[0].getAttribute('starttimestamp'); }       else { throw new Error("Couldn't obtain edit-token from page. Maybe pagename is invalid?"); }       Bawolff.mwapi.edit_token = token; //have a default since it stays constant. callback(token); //callback is inside this function's scope }

req = new Bawolff.mwapi.Request({action: "query", meta: "tokens", type: "csrf"}); if (delay) { req.delaySend(delay, callbackWrapper); }   else { req.send(callbackWrapper); }   return req; }

//Edits a page using a Bawolff.mwapi.Request object. /*arg is argument object. it contains: { //incomplete, but works. Use With Caution (as its not finished yet) Bawolff.mwapi.edit = function(arg, callback, errCallback, delay) { var token, requestOpt, req, callbackWrapper; if (!arg.page) { throw new Error("Nothing to edit"); }   if (!callback) { callback = function {return true;} }   if (!arg.summary) { arg.summary = "Edited page using user:Bawolff/mwapilib.js"; }   if (!arg.section) { arg.section = ""; }
 * content: "Text to replace article (string)",
 * token: "edit token (string [optional, will figure out itself if not supplied])",
 * page: "pagename to edit (string)",
 * time: "timestamp (from edit token) (string. [optional. prevents edit conflicts])",
 * section "section number. 0 for top, new for new (string). blank for whole page",
 * minor: boolean, for minor edit
 * createonly: error if non-existant
 * summary: "edit summary (string)"}
 * callback. (function) passed true on success, false on some failures (CAPTCHA). Second argument is xml response.
 * errCallback (function) passed errors on failure. (page deleted in meantime, spam filter, etc)
 * delay [optional] Add to the queue specified by delay so its executed later. must be a Bawolff.mwapi.AsyncQueue object
 * Returns the Bawolff.mwapi.Request object if the edit token is given as an arg, or cached
 * Returns boolean true if it has to fetch edit token from server
 * Moral of this story, if you depend on returned object, specify the edit token.

//wraps arround callback to make content pretty callbackWrapper = function(xml) { var status, res;

status = xml.getElementsByTagName('edit'); if (_hasAttribute(status[0], "result")) { if (status[0].getAttribute("result") === "Success") { res = true; }           else { res = false; }       }        else { throw new Error("Couldn't figure out if edit succeeded or failed [something bad happened]"); }

callback(res, xml); //callback is inside this function's scope }

//Start building request object

requestOpt = {action: "edit", title: arg.page, text: arg.content, summary: arg.summary}; if (arg.section) { requestOpt.section = arg.section; }   if (arg.minor) { requestOpt.minor = arg.minor; }   if (arg.createonly) { requestOpt.createonly = arg.createonly; }

//Figure out the editoken. fixme: this is ugly //first try arg if (arg.token) { token = arg.token; }   else if (Bawolff.mwapi.edit_token) { //Try cached value token = Bawolff.mwapi.edit_token; }

if (!token) { //if we still don't have it, get it       var tokenCallbackAndEdit = function (token) { requestOpt.token = token; req = new Bawolff.mwapi.Request(requestOpt, "POST"); req.send(callbackWrapper, errCallback); }       Bawolff.mwapi.getToken(tokenCallbackAndEdit, arg.page); return true; }   else { //we have the token requestOpt.token = token; req = new Bawolff.mwapi.Request(requestOpt, "POST");

if (delay) { req.delaySend(delay, callbackWrapper, errCallback); }   else { req.send(callbackWrapper, errCallback); }       return req; } } /* */

//appends to a page using a Bawolff.mwapi.Request object. /*arg is argument object. it contains: { //Warning: I havn't really tested this method very well. Bawolff.mwapi.append = function(arg, callback, errCallback, delay) { var token, req, callbackWrapper; if (!arg.page) { throw new Error("Nothing to edit"); }   if (!callback) { callback = function {return true;} }   if (!arg.summary) { arg.summary = "appended to page using user:Bawolff/mwapilib.js"; }
 * content: "Text to append to article (string)",
 * token: "edit token (string [optional, will figure out itself if not suplied])",
 * page: "pagename to edit (string)",
 * summary: "edit summary (string)"}
 * callback. (function) passed true on success, false on some failures (CAPTCHA). Second argument is xml response.
 * errCallback (function) passed errors on failure. (page deleted in meantime, spam filter, etc)
 * Returns the Bawolff.mwapi.Request object if the edit token is given as an arg, or cached
 * Returns boolean true if it has to fetch edit token from server.
 * Moral of this story, if you depend on returned object, specify the edit token.

//wraps arround callback to make content pretty callbackWrapper = function(xml) { var status, res;

status = xml.getElementsByTagName('edit'); if (_hasAttribute(status[0], "result")) { if (status[0].getAttribute("result") === "Success") { res = true; }           else { res = false; }       }        else { throw new Error("Couldn't figure out if edit succeeded or failed [something bad happened]"); }

callback(res, xml); //callback is inside this function's scope }

//Figure out the editoken. fixme: this is ugly //first try arg if (arg.token) { token = arg.token; }   else if (Bawolff.mwapi.edit_token) { //Try cached value token = Bawolff.mwapi.edit_token; }

if (!token) { //if we still don't have it, get it       var tokenCallbackAndEdit = function (token) { req = new Bawolff.mwapi.Request({action: "edit", title: arg.page, appendtext: arg.content, token: token, summary: arg.summary}, "POST"); req.send(callbackWrapper, errCallback); }       Bawolff.mwapi.getToken(tokenCallbackAndEdit, arg.page); return true; }   else { //we have the token req = new Bawolff.mwapi.Request({action: "edit", title: arg.page, appendtext: arg.content, token: token, summary: arg.summary}, "POST"); if (delay) { req.delaySend(delay, callbackWrapper, errCallback); }   else { req.send(callbackWrapper, errCallback); }       return req; } }

Bawolff.mwapi.sight = function(arg, callback, errCallback, delay) { /******** This function sights a paticular revision. first arg is an object with the following structure: {revid:, token: , level: , comment: } errCallback, and callback is optional, delay is an optional Bawolff.mwapi.AsyncQueue object if you want to use delay, but not errCallback, use undefined for the errCallback parameter.

level is quality level you want to flag to. 0 is normal, 1 is sighted 2 is reviewed. Wikinews uses 0 and 1.

callback is passed two arguments. First is boolean to indicate success. It will (always?) return true. Second is xml response body. Will be considered successful even if the revision was already flagged, unless your trying to re-unflag something errCallback is called on most errors. It is also called if you try to set flag_accuracy to 0 on something that is already set to 0 (and is passed a cryptic error message ``$1''. See https://bugzilla.wikimedia.org/show_bug.cgi?id=19545   var token, requestOpt, req;

if (!arg) { throw new Error("Need to specify what to sight (first argument missing)"); }       if (!arg.revid) { throw new Error("Please specify which revision you want to flag."); }   if (false) { throw new Error("Requires writeAPI in order to flag pages"); }   if (!callback) { callback = function {return true;} }   if (!arg.comment) { arg.comment = "Flagged page using user:Bawolff/mwapilib.js"; }   if (arg.level === undefined) { //could be numeric 0 which is == to false if (arg.flag_accuracy !== undefined) { arg.level = arg.flag_accuracy; }       else { arg.level = "1"; }   }

//wraps around callback to make content pretty callbackWrapper = function(xml) { var status, res;

status = xml.getElementsByTagName('review'); if (_hasAttribute(status[0], "result")) { if (status[0].getAttribute("result") === "Success") { res = true; }           else { res = false; }       }        else { throw new Error("Couldn't figure out if review succeeded or failed [something bad happened]"); }

callback(res, xml); //callback is inside this function's scope }

//Start building request object //api.php?action=review&revid=12345&token=123AB&flag_accuracy=1&comment=Ok requestOpt = {action: "review", revid: arg.revid, flag_accuracy: arg.level, comment: arg.comment};

//Figure out the editoken. fixme: this is ugly //first try arg if (arg.token) { token = arg.token; }   else if (Bawolff.mwapi.edit_token) { //Try cached value token = Bawolff.mwapi.edit_token; }

if (!token) { //if we still don't have it, get it       var tokenCallbackAndEdit = function (token) { requestOpt.token = token; req = new Bawolff.mwapi.Request(requestOpt, "POST"); if (delay) { req.delaySend(delay, callbackWrapper); }           else { req.send(callbackWrapper); }       }        Bawolff.mwapi.getToken(tokenCallbackAndEdit); return true; }   else { //we have the token requestOpt.token = token; req = new Bawolff.mwapi.Request(requestOpt, "POST");

if (delay) { req.delaySend(delay, callbackWrapper); }   else { req.send(callbackWrapper); }       return req; } }

/**** Takes a date in the form that api sends it (aka 2009-03-23T04:43:58Z ) and returns a native js Date object

depends on Bawolff.mwapi.parseAPIDate.regex and Bawolff.mwapi.parseAPIDate.func to do grunt work.



Bawolff.mwapi.parseAPIDate = function (datestring) { // + sign converts to number return new Date(+datestring.replace(Bawolff.mwapi.parseAPIDate.regex, Bawolff.mwapi.parseAPIDate.func));

}

Bawolff.mwapi.parseAPIDate.regex = /^([0-9]{4})-([01][0-9])-([0-3][0-9])T([0-2][0-9]):([0-5][0-9]):([0-5][0-9])Z$/; Bawolff.mwapi.parseAPIDate.func = function (str, year, month, day, hour, min, sec) { //Since JS and server form differ on month 0.

month = parseInt(month, 10); month--;

return Date.UTC(year, month, day, hour, min, sec);

}

//This could probably use a better implementation //idea is to have a queue of async ajax requests to complete

//See Bawolff.mwapi.Request.prototype.delaySend for instructions on how to use. Bawolff.mwapi.List = function { this.list = []; this.isStarted = false; } Bawolff.mwapi.List.prototype.add = function (f) { this.list[this.list.length] = f;

} //this should be called by ajax callbacks Bawolff.mwapi.List.prototype.next = function { if (this.list.length !== 0) { (this.list.shift); } } Bawolff.mwapi.List.prototype.start = function { if (!this.isStarted) { this.isStarted = true; this.next; } } Bawolff.mwapi.List.prototype.toString = function { return "mwapi ajax request queue. [" + (this.isStarted ? "started; " : "") + this.list.length + " items]"; }

Bawolff.mwapi.AsyncQueue = Bawolff.mwapi.List; //alias. List is a stupid name.

//Lazy var. This provides a wrapper to allow a variable name to be expanded at a specific time //to ease passing arround variables between async ajax calls.

//note this is called Bawolff.LazyVar, not Bawolff.mwapi.LazyVar

Bawolff.LazyVar = function(name) { this.fullName = name; this.name = name.split("."); } //Note this takes one optional paramter, a number. // 0 (or undefined) for do all checks // 1 for check only if var exists // 2 for check it exists and is not an empty string. Bawolff.LazyVar.prototype.get = function(skipChecks) { var newName = window[this.name[0]]; for (var i = 1; i < this.name.length; i++) { newName = newName[this.name[i]]; }   if (!skipChecks) { skipChecks = 0; }   if (skipChecks < 2) { if (newName === undefined) { throw new Error("Could not find variable: " + this.fullName); }   }    if (skipChecks < 1) { if (newName === "") { throw new Error("Variable is empty string: " + this.fullName); }   }    return newName; }

Bawolff.LazyVar.prototype.toString = Bawolff.LazyVar.prototype.get; /*Bawolff.LazyVar.prototype.toString = function { return "Lazy variable: " + this.fullName; }*/

var _hasAttribute = function(elm, attribute) { //IE does not element hasAttribute method of XML Dom elements try { return elm.hasAttribute(attribute); } catch(e) { return elm.getAttribute(attribute) !== null; } }