PHP Classes

File: JsHttpRequest.js

Recommend this page to a friend!
  Classes of Dmitry Koterov   JsHttpRequest   JsHttpRequest.js   Download  
File: JsHttpRequest.js
Role: Auxiliary data
Content type: text/plain
Description: The JS backend class.
Class: JsHttpRequest
Process regular and file upload AJAX requests
Author: By
Last change:
Date: 17 years ago
Size: 26,459 bytes
 

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