PHP Classes
Icontem

File: JsHttpRequest.js


  Search   All class groups All class groups   Latest entries Latest entries   Top 10 charts Top 10 charts   Newsletter Newsletter   Blog Blog   Forums Forums   Help FAQ Help FAQ  
  Login   Register  
Recommend this page to a friend! ReTweet ReTweet Stumble It! Stumble It! Bookmark in del.icio.us Bookmark in del.icio.us
  Classes of Dmitry Koterov  >  JsHttpRequest  >  JsHttpRequest.js  
File: JsHttpRequest.js
Role: Auxiliary data
Content type: text/plain
Description: The JS backend class.
Class: JsHttpRequest
Process regular and file upload AJAX requests
 

Contents

Class file image Download
/**
 * 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.19
 */

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 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');

            var ifname = 'jshr_i_' + id;
            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="' + ifname + '" id="' + ifname + '" style="width:0px; height:0px; overflow:hidden; border:none"></iframe>'
            var form = div.childNodes[0];
            
            // Check if all form elements belong to same form.
            if (queryElem.length) {
                form = queryElem[0].e;
                if (form.tagName.toUpperCase() == 'FORM') {
                    // Whole FORM sending.
                    queryElem = [];
                } else {
                    // If we have at least one form element, we use its form as POST container.
                    form = queryElem[0].e.form;
                    // Validate all elements.
                    for (var i = 0; i < queryElem.length; i++) {
                        var e = queryElem[i].e;
                        if (!e.form) {
                            return this._error('Element "' + e.name + '" does not belong 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!');
                        }
                    }
                }
                
                // Check enctype of the form.
                var need = "multipart/form-data";
                var given = form.attributes.encType || form.attributes.enctype || form.enctype;
                if (given != need) {
                    return this._error('Attribute "enctype" of elements\' form must be "' + need + '" (for IE), "' + given + '" given.');
                }
            }
            
            // 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;

            // Run submit with delay - for old Opera: it needs time to create IFRAME.
            var th = this;
            setTimeout(function() {
                // Insert hidden fields to the form.
                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);
                }
                
                TODO: iterate over ALL form elements, disable ALL elements except
                specified in queryElem. Then enable them back. Test on IE5.
    
                // Change names of along user-passed form elements.
                for (var i = 0; i < queryElem.length; i++) {
                    var qe = queryElem[i];
                    qe.svName = qe.e.name; // save old name
                    qe.e.name = qe.name;
                }
    
                // Temporary modify form attributes, submit form, restore attributes back.
                var sv = th._setAttributes(
                    form, 
                    {
                        'action':   url,
                        'method':   method,
                        'onsubmit': null,
                        'target':   ifname
                    }
                );
                form.submit();
                th._setAttributes(form, sv);

                // Remove generated temporary hidden elements from form.
                for (var i = 0; i < queryText.length; i++) {
                    form.lastChild.parentNode.removeChild(form.lastChild);
                }

                // Enable all disabled elements back.
                for (var i = 0; i < queryElem.length; i++) {
                    queryElem[i].e.name = queryElem[i].svName;
                }
            }, 10);
        },

        // 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) {
                var formAdded = false;
                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.toUpperCase();
                        if (tn == 'INPUT' || tn == 'TEXTAREA' || tn == 'SELECT' || tn == 'FORM') {
                            // This is a single form elemenent.
                            if (tn == 'FORM') formAdded = true;
                            queryElem[queryElem.length] = { name: curPrefix, e: 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);
                    }
                    if (formAdded && queryElem.length > 1) {
                        return this._error('If used, <form> must be single HTML element in the list.');
                    }
                }
            } 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.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();
        }},
        
        // Set attributes for specified node. Return old attributes.
        _setAttributes: function(e, attr) {
            var sv = {};
            var form = e;
            // This strange algorythm is needed, because form may  contain element 
            // with name like 'action'. In IE for such attribute will be returned
            // form element node, not form action. Workaround: copy all attributes
            // to new empty form and work with it, then copy them back. This is
            // THE ONLY working algorythm since a lot of bugs in IE5.0 (e.g. 
            // with e.attributes property: causes IE crash).
            if (e.mergeAttributes) {
                var form = document.createElement('form');
                form.mergeAttributes(e, false);
            }
            for (var k in attr) {
                sv[k] = form.getAttribute(k);
                form.setAttribute(k, attr[k]);
            }
            if (e.mergeAttributes) {
                e.mergeAttributes(form, false);
            }
            return sv;
        },

        // Stupid JS escape() does not quote '+'.
        escape: function(s) {
            return escape(s).replace(new RegExp('\\+','g'), '%2B');
        }
    }
})();

 
  Advertise on this site Advertise on this site   Site map Site map   Statistics Statistics   Site tips Site tips   Privacy policy Privacy policy   Contact Contact  

For more information send a message to :
info at phpclasses dot org.
Copyright (c) Icontem 1999-2009 PHP Classes - PHP Class Scripts
  PHP Book Reviews - Reviews of books and other products