/**

 * JsHttpRequest: JavaScript "AJAX" data loader.

 * (C) 2006 Dmitry Koterov, http://forum.dklab.ru/users/DmitryKoterov/

 *

 * This library is free software; you can redistribute it and/or

 * modify it under the terms of the GNU Lesser General Public

 * License as published by the Free Software Foundation; either

 * version 2.1 of the License, or (at your option) any later version.

 * See http://www.gnu.org/copyleft/lesser.html

 *

 * Do not remove this comment if you want to use script!

 * Не удаляйте данный комментарий, если вы хотите использовать скрипт!

 *

 * This library tries to use XMLHttpRequest (if available), and on 

 * failure - use dynamically created <script> elements. Backend code

 * is the same for both cases. Library also supports file uploading;

 * in this case it uses FORM+IFRAME-based loading.

 *

 * @author Dmitry Koterov 

 * @version 4.16

 */



function JsHttpRequest() { this._construct() }

(function() { // to create local-scope variables

    var COUNT       = 0;

    var PENDING     = {};

    var CACHE       = {};



    // Called by server script on data load. Static.

    JsHttpRequest.dataReady = function(id, text, js) {

        var undef;

        var th = PENDING[id];

        delete PENDING[id];

        if (th) {

            delete th._xmlReq;

            if (th.caching && th.hash) CACHE[th.hash] = [text, js];

            th._dataReady(text, js);

        } else if (th !== false) {

            throw "JsHttpRequest.dataReady(): unknown pending id: " + id;

        }

    }



    // Simple interface for most popular use-case.

    JsHttpRequest.query = function(url, content, onready, nocache) {

        var req = new JsHttpRequest();

        req.caching = !nocache;

        req.onreadystatechange = function() {

            if (req.readyState == 4) {

                onready(req.responseJS, req.responseText);

            }

        }

        req.open(null, url, true);

        req.send(content);

    },

    

    JsHttpRequest.prototype = {

        // Standard properties.

        onreadystatechange: null,

        readyState:         0,

        responseText:       null,

        responseXML:        null,

        status:             200,

        statusText:         "OK",

        // JavaScript response array/hash

        responseJS:         null,



        // Additional properties.

        session_name:       "PHPSESSID",  // set to SID cookie or GET parameter name

        caching:            false,        // need to use caching?

        loader:             null,         // loader to use ('form', 'script', 'xml'; null - autodetect)



        // Internals.

        _span:              null,

        _id:                null,

        _xmlReq:            null,

        _openArg:           null,

        _reqHeaders:        null,

        _maxUrlLen:         2000,



        dummy: function() {}, // empty constant function for ActiveX leak minimization



        abort: function() {

            if (this._xmlReq) {

                this._xmlReq.abort();

                this._xmlReq = null;

            }

            this._cleanupScript();

            this._changeReadyState(4, true); // 4 in IE & FF on abort() call; Opera does not change to 4.

        },



        open: function(method, url, asyncFlag, username, password) {

            // Append SID to original URL.

            var sid = this._getSid();

            if (sid) url += (url.indexOf('?')>=0? '&' : '?') + this.session_name + "=" + this.escape(sid);

            this._openArg = {

                method:     (method||'').toUpperCase(),

                url:        url,

                asyncFlag:  asyncFlag,

                username:   username != null? username : '',

                password:   password != null? password : ''

            };

            this._id = null;

            this._xmlReq = null;

            this._reqHeaders = [];

            this._changeReadyState(1, true); // compatibility with XMLHttpRequest

            return true;

        },

        

        send: function(content) {

            this._changeReadyState(1, true); // compatibility with XMLHttpRequest



            var id = (new Date().getTime()) + "" + COUNT++;

            var url = this._openArg.url; 



            // Prepare to build QUERY_STRING from query hash.

            var queryText = [];

            var queryElem = [];

            if (!this._hash2query(content, null, queryText, queryElem)) return;



            var loader = (this.loader||'').toLowerCase();

            var method = this._openArg.method;

            var xmlReq = null;

            if (queryElem.length && !loader) {

                // Always use form loader if we have at least one form element.

                loader = 'form';

            } else {

                // Try to obtain XML request object.

                xmlReq = this._obtainXmlReq(id, url)

            }



            // Full URL if parameters are passed via GET.

            var fullGetUrl = url + (url.indexOf('?')>=0? '&' : '?') + queryText.join('&');



            // Solve hashcode BEFORE appending ID and check if cache is already present.

            this.hash = null;

            if (this.caching && !queryElem.length) {

                this.hash = fullGetUrl;

                if (CACHE[this.hash]) {

                    var c = CACHE[this.hash];

                    this._dataReady(c[0], c[1]);

                    return false;

                }

            }



            // Detect loader and method. (Yes, lots of code and conditions!)

            var canSetHeaders = xmlReq && (window.ActiveXObject || xmlReq.setRequestHeader); 

            if (!loader) {

                // Auto-detect loader.

                if (xmlReq) {

                    // Can use XMLHttpRequest.

                    loader = 'xml';

                    switch (method) {

                        case "POST":

                            if (!canSetHeaders) {

                                // Use POST method. Pass query in request body.

                                // Opera 8.01 does not support setRequestHeader, so no POST method.

                                loader = 'form';

                            }

                            break;

                        case "GET":

                            // Length of the query is checked later.

                            break;

                        default:

                            // Method is not set: auto-detect method.

                            if (canSetHeaders) {

                                method = 'POST';

                            } else {

                                if (fullGetUrl.length > this._maxUrlLen) {

                                    method = 'POST';

                                    loader = 'form';

                                } else {

                                    method = 'GET';

                                }

                            }

                    }

                } else {

                    // Cannot use XMLHttpRequest.

                    loader = 'script';

                    switch (method) {

                        case "POST":

                            loader = 'form';

                            break;

                        case "GET":

                            // Length of the query is checked later.

                            break;

                        default:

                            if (fullGetUrl.length > this._maxUrlLen) {

                                method = 'POST';

                                loader = 'form';

                            } else {

                                method = 'GET';

                            }

                    }

                }

            } else if (!method) {

                // Loader is pre-defined, but method is not set.

                switch (loader) {

                    case 'form':

                        method = 'POST';

                        break;

                    case 'script':

                        method = 'GET';

                        break;

                    default:

                        if (canSetHeaders) {

                            method = 'POST';

                        } else {

                            method = 'GET';

                        }

                }

            }



            // Correct GET URL.

            var requestBody = null;

            if (method == 'GET') {

                url = fullGetUrl;

                if (url.length > this._maxUrlLen) return this._error('Cannot use so long query (URL is ' + url.length + ' byte(s) length) with GET request.');

            } else if (method == 'POST') {

                requestBody = queryText.join('&');

            } else {

                return this._error('Unknown method: ' + method + '. Only GET and POST are supported.');

            }



            // Append loading ID to URL: a=aaa&b=bbb&<id>

            url = url + (url.indexOf('?')>=0? '&' : '?') + 'JsHttpRequest=' + id + '-' + loader;



            // Save loading script.

            PENDING[id] = this;



            // Send the request.

            switch (loader) {

                case 'xml':

                    // Use XMLHttpRequest.

                    if (!xmlReq) return this._error('Cannot use XMLHttpRequest or ActiveX loader: not supported');

                    if (method == "POST" && !canSetHeaders) return this._error('Cannot use XMLHttpRequest loader or ActiveX loader, POST method: headers setting is not supported');

                    if (queryElem.length) return this._error('Cannot use XMLHttpRequest loader: direct form elements using and uploading are not implemented');

                    this._xmlReq = xmlReq;

                    var a = this._openArg;

                    this._xmlReq.open(method, url, a.asyncFlag, a.username, a.password);

                    if (canSetHeaders) {

                        // Pass pending headers.

                        for (var i=0; i<this._reqHeaders.length; i++)

                            this._xmlReq.setRequestHeader(this._reqHeaders[i][0], this._reqHeaders[i][1]);

                        // Set non-default Content-type. We cannot use 

                        // "application/x-www-form-urlencoded" here, because 

                        // in PHP variable HTTP_RAW_POST_DATA is accessible only when 

                        // enctype is not default (e.g., "application/octet-stream" 

                        // is a good start). We parse POST data manually in backend 

                        // library code.

                        this._xmlReq.setRequestHeader('Content-Type', 'application/octet-stream');

                    }

                    // Send the request.

                    return this._xmlReq.send(requestBody);



                case 'script':

                    // Create <script> element and run it.

                    if (method != 'GET') return this._error('Cannot use SCRIPT loader: it supports only GET method');

                    if (queryElem.length) return this._error('Cannot use SCRIPT loader: direct form elements using and uploading are not implemented');

                    this._obtainScript(id, url);

                    return true;



                case 'form':

                    // Create & submit FORM.

                    if (!this._obtainForm(id, url, method, queryText, queryElem)) return null;

                    return true;



                default:

                    return this._error('Unknown loader: ' + loader);

            }

        },



        getAllResponseHeaders: function() {

            if (this._xmlReq) return this._xmlReq.getAllResponseHeaders();

            return '';

        },

            

        getResponseHeader: function(label) {

            if (this._xmlReq) return this._xmlReq.getResponseHeader(label);

            return '';

        },



        setRequestHeader: function(label, value) {

            // Collect headers.

            this._reqHeaders[this._reqHeaders.length] = [label, value];

        },





        //

        // Internal functions.

        //



        // Constructor.

        _construct: function() {},



        // Do all work when data is ready.

        _dataReady: function(text, js) { with (this) {

            if (text !== null || js !== null) {

                status = 4;

                responseText = responseXML = text;

                responseJS = js;

            } else {

                status = 500;

                responseText = responseXML = responseJS = null;

            }

            _changeReadyState(2);

            _changeReadyState(3);

            _changeReadyState(4);

            _cleanupScript();

        }},



        // Called on error.

        _error: function(msg) {

            throw (window.Error? new Error(msg) : msg);

        },



        // Create new XMLHttpRequest object.

        _obtainXmlReq: function(id, url) {

            // If url.domain specified and differ from current, cannot use XMLHttpRequest!

            // XMLHttpRequest (and MS ActiveX'es) cannot work with different domains.

            var p = url.match(new RegExp('^([a-z]+)://([^/]+)(.*)', 'i'));

            if (p) {

                if (p[2].toLowerCase() == document.location.hostname.toLowerCase()) {

                    url = p[3];

                } else {

                    return null;

                }

            }

            

            // Try to use built-in loaders.

            var req = null;

            if (window.XMLHttpRequest) {

                try { req = new XMLHttpRequest() } catch(e) {}

            } else if (window.ActiveXObject) {

                try { req = new ActiveXObject("Microsoft.XMLHTTP") } catch(e) {}

                if (!req) try { req = new ActiveXObject("Msxml2.XMLHTTP") } catch (e) {}

            }

            if (req) {

                var th = this;

                req.onreadystatechange = function() { 

                    if (req.readyState == 4) {

                        // Avoid memory leak by removing closure.

                        req.onreadystatechange = th.dummy;

                        th.status = null;

                        try { 

                            // In case of abort() call, req.status is unavailable and generates exception.

                            // But req.readyState equals to 4 in this case. Stupid behaviour. :-(

                            th.status = req.status;

                            th.responseText = req.responseText;

                        } catch (e) {}

                        if (!th.status) return;

                        var funcRequestBody = null;

                        try {

                            // Prepare generator function & catch syntax errors on this stage.

                            eval('funcRequestBody = function() {\n' + th.responseText + '\n}');

                        } catch (e) {

                            return th._error("JavaScript code generated by backend is invalid!\n" + th.responseText)

                        }

                        // Call associated dataReady() outside try-catch block 

                        // to pass excaptions in onreadystatechange in usual manner.

                        funcRequestBody();

                    }

                };

                this._id = id;

            }

            return req;

        },



        // Create new script element and start loading.

        _obtainScript: function(id, href) { with (document) {

            // Oh shit! Damned stupid fucked Opera 7.23 does not allow to create SCRIPT 

            // element over createElement (in HEAD or BODY section or in nested SPAN - 

            // no matter): it is created deadly, and does not respons on href assignment.

            // So - always create SPAN.

            var span = createElement('SPAN');

            span.style.display = 'none';

            body.insertBefore(span, body.lastChild);

            span.innerHTML = 'Text for stupid IE.<s'+'cript></' + 'script>';

            setTimeout(function() {

                var s = span.getElementsByTagName('script')[0];

                s.language = 'JavaScript';

                if (s.setAttribute) s.setAttribute('src', href); else s.src = href;

            }, 10);

            this._id = id;

            this._span = span;

        }},



        // Create & submit form.

        _obtainForm: function(id, url, method, queryText, queryElem) {

            // In case of GET method - split real query string.

            if (method == 'GET') {

                queryText = url.split('?', 2)[1].split('&');

                url = url.split('?', 2)[0];

            }



            // Create invisible IFRAME with temporary form (form is used on empty queryElem).

            var div = document.createElement('DIV');

            div.id = 'jshr_d_' + id;

            div.style.position = 'absolute';

            div.style.visibility = 'hidden';

            div.innerHTML = 

                '<form enctype="multipart/form-data"></form>' + // stupid IE, MUST use innerHTML assignment :-(

                '<iframe src="javascript:\'\'" name="jshr_i_' + id + '" style="width:0px; height:0px; overflow:hidden; border:none"></iframe>';

            var form = div.getElementsByTagName('FORM')[0];

            var iframe = div.getElementsByTagName('IFRAME')[0];



            // Check if all form elements belong to same form.

            if (queryElem.length) {

                // If we have at least one form element, we use its form as POST container.

                form = queryElem[0][1].form;

                var foundFile = false;

                for (var i = 0; i < queryElem.length; i++) {

                    var e = queryElem[i][1];

                    if (!e.form) {

                        return this._error('Element "' + e.name + '" do not belongs to any form!');

                    }

                    if (e.form != form) {

                        return this._error('Element "' + e.name + '" belongs to different form. All elements must belong to the same form!');

                    }

                    foundFile = foundFile || (e.tagName.toLowerCase() == 'input' && (e.type||'').toLowerCase() == 'file');

                }

                var et = "multipart/form-data";

                if (form.enctype != et && foundFile) {

                    return this._error('Attribute "enctype" of elements\' form must be "' + et + '" (for IE), "' + form.enctype + '" given.');

                }

            }



            // Temporary disable ALL form elements in 'form' (including custom!).

            for (var i = 0; i < form.elements.length; i++) {

                var e = form.elements[i];

                if (e.name != null) {

                    e.jshrSaveName = e.name;

                    e.name = '';

                }

            }



            // Insert hidden fields to the form.

            var tmpE = [];

            for (var i=0; i<queryText.length; i++) {

                var pair = queryText[i].split('=', 2);

                var e = document.createElement('INPUT');

                e.type = 'hidden';

                e.name = unescape(pair[0]);

                e.value = pair[1] != null? unescape(pair[1]) : '';

                form.appendChild(e);

                tmpE[tmpE.length] = e;

            }



            // Enable custom form elements back & change their names.

            for (var i = 0; i < queryElem.length; i++) queryElem[i][1].name = queryElem[i][0];



            // Insert generated form inside the document.

            // Be careful: don't forget to close FORM container in document body!

            document.body.insertBefore(div, document.body.lastChild);

            this._span = div;



            // Temporary modify form attributes, submit form, restore attributes back.

            var sv = {};

            sv.enctype  = form.enctype;  form.enctype = "multipart/form-data";

            sv.action   = form.action;   form.action = url;

            sv.method   = form.method;   form.method = method;

            sv.target   = form.target;   form.target = iframe.name;

            sv.onsubmit = form.onsubmit; form.onsubmit = null;

            form.submit();

            for (var i in sv) form[i] = sv[i];

            

            // Remove generated temporary hidden elements from form.

            for (var i = 0; i < tmpE.length; i++) tmpE[i].parentNode.removeChild(tmpE[i]);



            // Enable all disabled elements back.

            for (var i = 0; i < form.elements.length; i++) {

                var e = form.elements[i];

                if (e.jshrSaveName != null) {

                    e.name = e.jshrSaveName;

                    e.jshrSaveName = null;

                }

            }

        },



        // Remove last used script element (clean memory).

        _cleanupScript: function() {

            var span = this._span;

            if (span) {

                this._span = null;

                setTimeout(function() {

                    // without setTimeout - crash in IE 5.0!

                    span.parentNode.removeChild(span);

                }, 50);

            }

            if (this._id) {

                // Mark this loading as aborted.

                PENDING[this._id] = false;

            }

            return false;

        },



        // Convert hash to QUERY_STRING.

        // If next value is scalar or hash, push it to queryText.

        // If next value is form element, push [name, element] to queryElem.

        _hash2query: function(content, prefix, queryText, queryElem) {

            if (prefix == null) prefix = "";

            if (content instanceof Object) {

                for (var k in content) {

                    var v = content[k];

                    if (v instanceof Function) continue;

                    var curPrefix = prefix? prefix+'['+this.escape(k)+']' : this.escape(k);

                    if (this._isFormElement(v)) {

                        var tn = v.tagName.toLowerCase();

                        if (tn == 'form') {

                            // This is FORM itself. Add all its elements.

                            for (var i=0; i<v.elements.length; i++) {

                                var e = v.elements[i];

                                if (e.name) queryElem[queryElem.length] = [e.name, e];

                            }

                        } else if (tn == 'input' || tn == 'textarea' || tn == 'select') {

                            // This is a single form elemenent.

                            queryElem[queryElem.length] = [curPrefix, v];

                        } else {

                            return this._error('Invalid FORM element detected: name=' + (e.name||'') + ', tag=' + e.tagName);

                        }

                    } else if (v instanceof Object) {

                        this._hash2query(v, curPrefix, queryText, queryElem);

                    } else {

                        // We MUST skip NULL values, because there is no method

                        // to pass NULL's via GET or POST request in PHP.

                        if (v === null) continue;

                        queryText[queryText.length] = curPrefix + "=" + this.escape('' + v);

                    }

                }

            } else {

                queryText[queryText.length] = content;

            }

            return true;

        },



        // Return true if e is any form element of FORM itself.

        _isFormElement: function(e) {

            // Fast & dirty method.

            return e && e.ownerDocument && e.parentNode && e.parentNode.appendChild && e.tagName;

        },



        // Return value of SID based on QUERY_STRING or cookie

        // (PHP compatible sessions).

        _getSid: function() {

            var m = document.location.search.match(new RegExp('[&?]'+this.session_name+'=([^&?]*)'));

            var sid = null;

            if (m) {

                sid = m[1];

            } else {

                var m = document.cookie.match(new RegExp('(;|^)\\s*'+this.session_name+'=([^;]*)'));

                if (m) sid = m[2];

            }

            return sid;

        },



        // Change current readyState and call trigger method.

        _changeReadyState: function(s, reset) { with (this) {

            if (reset) {

                status = statusText = responseJS = null;

                responseText = '';

            }

            readyState = s;

            if (onreadystatechange) onreadystatechange();

        }},



        // Stupid JS escape() does not quote '+'.

        escape: function(s) {

            return escape(s).replace(new RegExp('\\+','g'), '%2B');

        }

    }

})();

