User:Bawolff/mwapilib.js

From Wikinews, the free news source you can write!
Jump to navigation Jump to search

Note: After saving, you may have to bypass your browser's cache to see the changes. Mozilla / Firefox / Safari: hold down Shift while clicking Reload, or press Ctrl-Shift-R (Cmd-Shift-R on Apple Mac); IE: hold Ctrl while clicking Refresh, or press Ctrl-F5; Konqueror: simply click the Reload button, or press F5; Opera users may need to completely clear their cache in Tools→Preferences. — More skins

//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: <content>, ...}

//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: {
** 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.
*/
//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 = "";
    }

    //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: {
** 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.
*/
//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]]''";
    }

    //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: <revision id>, token: <edit token [optional]>, level: <flag_accuracy level, [optional, defaults to 1, can also be called flag_accuracy]>, comment: <comment [optional]>}
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;
 }
}