1/**
2 * JsHttpRequest: JavaScript "AJAX" data loader
3 *
4 * @license LGPL
5 * @author Dmitry Koterov, http://en.dklab.ru/lib/JsHttpRequest/
6 * @version 5.x $Id$
7 */
8
9// {{{
10function JsHttpRequest() {
11    // Standard properties.
12    var t = this;
13    t.onreadystatechange = null;
14    t.readyState         = 0;
15    t.responseText       = null;
16    t.responseXML        = null;
17    t.status             = 200;
18    t.statusText         = "OK";
19    // JavaScript response array/hash
20    t.responseJS         = null;
21
22    // Additional properties.
23    t.caching            = false;        // need to use caching?
24    t.loader             = null;         // loader to use ('form', 'script', 'xml'; null - autodetect)
25    t.session_name       = "PHPSESSID";  // set to SID cookie or GET parameter name
26
27    // Internals.
28    t._ldObj              = null;  // used loader object
29    t._reqHeaders        = [];    // collected request headers
30    t._openArgs          = null;  // parameters from open()
31    t._errors = {
32        inv_form_el:        'Invalid FORM element detected: name=%, tag=%',
33        must_be_single_el:  'If used, <form> must be a single HTML element in the list.',
34        js_invalid:         'JavaScript code generated by backend is invalid!\n%',
35        url_too_long:       'Cannot use so long query with GET request (URL is larger than % bytes)',
36        unk_loader:         'Unknown loader: %',
37        no_loaders:         'No loaders registered at all, please check JsHttpRequest.LOADERS array',
38        no_loader_matched:  'Cannot find a loader which may process the request. Notices are:\n%',
39        no_headers:         'Method setRequestHeader() cannot work together with the % loader.'
40    }
41
42    /**
43     * Aborts the request. Behaviour of this function for onreadystatechange()
44     * is identical to IE (most universal and common case). E.g., readyState -> 4
45     * on abort() after send().
46     */
47    t.abort = function() { with (this) {
48        if (_ldObj && _ldObj.abort) _ldObj.abort();
49        _cleanup();
50        if (readyState == 0) {
51            // start->abort: no change of readyState (IE behaviour)
52            return;
53        }
54        if (readyState == 1 && !_ldObj) {
55            // open->abort: no onreadystatechange call, but change readyState to 0 (IE).
56            // send->abort: change state to 4 (_ldObj is not null when send() is called)
57            readyState = 0;
58            return;
59        }
60        _changeReadyState(4, true); // 4 in IE & FF on abort() call; Opera does not change to 4.
61    }}
62
63    /**
64     * Prepares the object for data loading.
65     */
66    t.open = function(method, url, asyncFlag, username, password) { with (this){
67        // Append SID to original URL. Use try...catch for security problems.
68        try {
69            if (
70                document.location.search.match(new RegExp('[&?]' + session_name + '=([^&?]*)'))
71                || document.cookie.match(new RegExp('(?:;|^)\\s*' + session_name + '=([^;]*)'))
72            ) {
73                url += (url.indexOf('?') >= 0? '&' : '?') + session_name + "=" + this.escape(RegExp.$1);
74            }
75        } catch (e) {}
76        // Store open arguments to hash.
77        _openArgs = {
78            method:     (method || '').toUpperCase(),
79            url:        url,
80            asyncFlag:  asyncFlag,
81            username:   username != null? username : '',
82            password:   password != null? password : ''
83        }
84        _ldObj = null;
85        _changeReadyState(1, true); // compatibility with XMLHttpRequest
86        return true;
87    }}
88
89    /**
90     * Sends a request to a server.
91     */
92    t.send = function(content) {
93        if (!this.readyState) {
94            // send without open or after abort: no action (IE behaviour).
95            return;
96        }
97        this._changeReadyState(1, true); // compatibility with XMLHttpRequest
98        this._ldObj = null;
99
100        // Prepare to build QUERY_STRING from query hash.
101        var queryText = [];
102        var queryElem = [];
103        if (!this._hash2query(content, null, queryText, queryElem)) return;
104
105        // Solve the query hashcode & return on cache hit.
106        var hash = null;
107        if (this.caching && !queryElem.length) {
108            hash = this._openArgs.username + ':' + this._openArgs.password + '@' + this._openArgs.url + '|' + queryText + "#" + this._openArgs.method;
109            var cache = JsHttpRequest.CACHE[hash];
110            if (cache) {
111                this._dataReady(cache[0], cache[1]);
112                return false;
113            }
114        }
115
116        // Try all the loaders.
117        var loader = (this.loader || '').toLowerCase();
118        if (loader && !JsHttpRequest.LOADERS[loader]) return this._error('unk_loader', loader);
119        var errors = [];
120        var lds = JsHttpRequest.LOADERS;
121        for (var tryLoader in lds) {
122            var ldr = lds[tryLoader].loader;
123            if (!ldr) continue; // exclude possibly derived prototype properties from "for .. in".
124            if (loader && tryLoader != loader) continue;
125            // Create sending context.
126            var ldObj = new ldr(this);
127            JsHttpRequest.extend(ldObj, this._openArgs);
128            JsHttpRequest.extend(ldObj, {
129                queryText:  queryText.join('&'),
130                queryElem:  queryElem,
131                id:         (new Date().getTime()) + "" + JsHttpRequest.COUNT++,
132                hash:       hash,
133                span:       null
134            });
135            var error = ldObj.load();
136            if (!error) {
137                // Save loading script.
138                this._ldObj = ldObj;
139                JsHttpRequest.PENDING[ldObj.id] = this;
140                return true;
141            }
142            if (!loader) {
143                errors[errors.length] = '- ' + tryLoader.toUpperCase() + ': ' + this._l(error);
144            } else {
145                return this._error(error);
146            }
147        }
148
149        // If no loader matched, generate error message.
150        return tryLoader? this._error('no_loader_matched', errors.join('\n')) : this._error('no_loaders');
151    }
152
153    /**
154     * Returns all response headers (if supported).
155     */
156    t.getAllResponseHeaders = function() { with (this) {
157        return _ldObj && _ldObj.getAllResponseHeaders? _ldObj.getAllResponseHeaders() : [];
158    }}
159
160    /**
161     * Returns one response header (if supported).
162     */
163    t.getResponseHeader = function(label) { with (this) {
164        return _ldObj && _ldObj.getResponseHeader? _ldObj.getResponseHeader() : [];
165    }}
166
167    /**
168     * Adds a request header to a future query.
169     */
170    t.setRequestHeader = function(label, value) { with (this) {
171        _reqHeaders[_reqHeaders.length] = [label, value];
172    }}
173
174    //
175    // Internal functions.
176    //
177
178    /**
179     * Do all the work when a data is ready.
180     */
181    t._dataReady = function(text, js) { with (this) {
182        if (caching && _ldObj) JsHttpRequest.CACHE[_ldObj.hash] = [text, js];
183        if (text !== null || js !== null) {
184            status = 4;
185            responseText = responseXML = text;
186            responseJS = js;
187        } else {
188            status = 500;
189            responseText = responseXML = responseJS = null;
190        }
191        _changeReadyState(2);
192        _changeReadyState(3);
193        _changeReadyState(4);
194        _cleanup();
195    }}
196
197    /**
198     * Analog of sprintf(), but translates the first parameter by _errors.
199     */
200    t._l = function(args) {
201        var i = 0, p = 0, msg = this._errors[args[0]];
202        // Cannot use replace() with a callback, because it is incompatible with IE5.
203        while ((p = msg.indexOf('%', p)) >= 0) {
204            var a = args[++i] + "";
205            msg = msg.substring(0, p) + a + msg.substring(p + 1, msg.length);
206            p += 1 + a.length;
207        }
208        return msg;
209    }
210
211    /**
212     * Called on error.
213     */
214    t._error = function(msg) {
215        msg = this._l(typeof(msg) == 'string'? arguments : msg)
216        msg = "JsHttpRequest: " + msg;
217        if (!window.Error) {
218            // Very old browser...
219            throw msg;
220        } else if ((new Error(1, 'test')).description == "test") {
221            // We MUST (!!!) pass 2 parameters to the Error() constructor for IE5.
222            throw new Error(1, msg);
223        } else {
224            // Mozilla does not support two-parameter call style.
225            throw new Error(msg);
226        }
227    }
228
229    /**
230     * Convert hash to QUERY_STRING.
231     * If next value is scalar or hash, push it to queryText.
232     * If next value is form element, push [name, element] to queryElem.
233     */
234    t._hash2query = function(content, prefix, queryText, queryElem) {
235        if (prefix == null) prefix = "";
236        if((''+typeof(content)).toLowerCase() == 'object') {
237            var formAdded = false;
238            if (content && content.parentNode && content.parentNode.appendChild && content.tagName && content.tagName.toUpperCase() == 'FORM') {
239                content = { form: content };
240            }
241            for (var k in content) {
242                var v = content[k];
243                if (v instanceof Function) continue;
244                var curPrefix = prefix? prefix + '[' + this.escape(k) + ']' : this.escape(k);
245                var isFormElement = v && v.parentNode && v.parentNode.appendChild && v.tagName;
246                if (isFormElement) {
247                    var tn = v.tagName.toUpperCase();
248                    if (tn == 'FORM') {
249                        // FORM itself is passed.
250                        formAdded = true;
251                    } else if (tn == 'INPUT' || tn == 'TEXTAREA' || tn == 'SELECT') {
252                        // This is a single form elemenent.
253                    } else {
254                        return this._error('inv_form_el', (v.name||''), v.tagName);
255                    }
256                    queryElem[queryElem.length] = { name: curPrefix, e: v };
257                } else if (v instanceof Object) {
258                    this._hash2query(v, curPrefix, queryText, queryElem);
259                } else {
260                    // We MUST skip NULL values, because there is no method
261                    // to pass NULL's via GET or POST request in PHP.
262                    if (v === null) continue;
263                    queryText[queryText.length] = curPrefix + "=" + this.escape('' + v);
264                }
265                if (formAdded && queryElem.length > 1) {
266                    return this._error('must_be_single_el');
267                }
268            }
269        } else {
270            queryText[queryText.length] = content;
271        }
272        return true;
273    }
274
275    /**
276     * Remove last used script element (clean memory).
277     */
278    t._cleanup = function() {
279        var ldObj = this._ldObj;
280        if (!ldObj) return;
281        // Mark this loading as aborted.
282        JsHttpRequest.PENDING[ldObj.id] = false;
283        var span = ldObj.span;
284        if (!span) return;
285        ldObj.span = null;
286        var closure = function() {
287            span.parentNode.removeChild(span);
288        }
289        // IE5 crashes on setTimeout(function() {...}, ...) construction! Use tmp variable.
290        JsHttpRequest.setTimeout(closure, 50);
291    }
292
293    /**
294     * Change current readyState and call trigger method.
295     */
296    t._changeReadyState = function(s, reset) { with (this) {
297        if (reset) {
298            status = statusText = responseJS = null;
299            responseText = '';
300        }
301        readyState = s;
302        if (onreadystatechange) onreadystatechange();
303    }}
304
305    /**
306     * JS escape() does not quote '+'.
307     */
308    t.escape = function(s) {
309        return escape(s).replace(new RegExp('\\+','g'), '%2B');
310    }
311}
312
313
314// Global library variables.
315JsHttpRequest.COUNT = 0;              // unique ID; used while loading IDs generation
316JsHttpRequest.MAX_URL_LEN = 2000;     // maximum URL length
317JsHttpRequest.CACHE = {};             // cached data
318JsHttpRequest.PENDING = {};           // pending loadings
319JsHttpRequest.LOADERS = {};           // list of supported data loaders (filled at the bottom of the file)
320JsHttpRequest._dummy = function() {}; // avoid memory leaks
321
322
323/**
324 * These functions are dirty hacks for IE 5.0 which does not increment a
325 * reference counter for an object passed via setTimeout(). So, if this
326 * object (closure function) is out of scope at the moment of timeout
327 * applying, IE 5.0 crashes.
328 */
329
330/**
331 * Timeout wrappers storage. Used to avoid zeroing of referece counts in IE 5.0.
332 * Please note that you MUST write "window.setTimeout", not "setTimeout", else
333 * IE 5.0 crashes again. Strange, very strange...
334 */
335JsHttpRequest.TIMEOUTS = { s: window.setTimeout, c: window.clearTimeout };
336
337/**
338 * Wrapper for IE5 buggy setTimeout.
339 * Use this function instead of a usual setTimeout().
340 */
341JsHttpRequest.setTimeout = function(func, dt) {
342    // Always save inside the window object before a call (for FF)!
343    window.JsHttpRequest_tmp = JsHttpRequest.TIMEOUTS.s;
344    if (typeof(func) == "string") {
345        id = window.JsHttpRequest_tmp(func, dt);
346    } else {
347        var id = null;
348        var mediator = function() {
349            func();
350            delete JsHttpRequest.TIMEOUTS[id]; // remove circular reference
351        }
352        id = window.JsHttpRequest_tmp(mediator, dt);
353        // Store a reference to the mediator function to the global array
354        // (reference count >= 1); use timeout ID as an array key;
355        JsHttpRequest.TIMEOUTS[id] = mediator;
356    }
357    window.JsHttpRequest_tmp = null; // no delete() in IE5 for window
358    return id;
359}
360
361/**
362 * Complimental wrapper for clearTimeout.
363 * Use this function instead of usual clearTimeout().
364 */
365JsHttpRequest.clearTimeout = function(id) {
366    window.JsHttpRequest_tmp = JsHttpRequest.TIMEOUTS.c;
367    delete JsHttpRequest.TIMEOUTS[id]; // remove circular reference
368    var r = window.JsHttpRequest_tmp(id);
369    window.JsHttpRequest_tmp = null; // no delete() in IE5 for window
370    return r;
371}
372
373
374/**
375 * Global static function.
376 * Simple interface for most popular use-cases.
377 * You may also pass URLs like "GET url" or "script.GET url".
378 */
379JsHttpRequest.query = function(url, content, onready, nocache) {
380    var req = new this();
381    req.caching = !nocache;
382    req.onreadystatechange = function() {
383        if (req.readyState == 4) {
384            onready(req.responseJS, req.responseText);
385        }
386    }
387    var method = null;
388    if (url.match(/^((\w+)\.)?(GET|POST)\s+(.*)/i)) {
389        req.loader = RegExp.$2? RegExp.$2 : null;
390        method = RegExp.$3;
391        url = RegExp.$4;
392    }
393    req.open(method, url, true);
394    req.send(content);
395}
396
397
398/**
399 * Global static function.
400 * Called by server backend script on data load.
401 */
402JsHttpRequest.dataReady = function(d) {
403    var th = this.PENDING[d.id];
404    delete this.PENDING[d.id];
405    if (th) {
406        th._dataReady(d.text, d.js);
407    } else if (th !== false) {
408        throw "dataReady(): unknown pending id: " + d.id;
409    }
410}
411
412
413// Adds all the properties of src to dest.
414JsHttpRequest.extend = function(dest, src) {
415    for (var k in src) dest[k] = src[k];
416}
417
418/**
419 * Each loader has the following properties which must be initialized:
420 * - method
421 * - url
422 * - asyncFlag (ignored)
423 * - username
424 * - password
425 * - queryText (string)
426 * - queryElem (array)
427 * - id
428 * - hash
429 * - span
430 */
431
432// }}}
433
434// {{{ xml
435// Loader: XMLHttpRequest or ActiveX.
436// [+] GET and POST methods are supported.
437// [+] Most native and memory-cheap method.
438// [+] Backend data can be browser-cached.
439// [-] Cannot work in IE without ActiveX.
440// [-] No support for loading from different domains.
441// [-] No uploading support.
442//
443JsHttpRequest.LOADERS.xml = { loader: function(req) {
444    JsHttpRequest.extend(req._errors, {
445        xml_no:          'Cannot use XMLHttpRequest or ActiveX loader: not supported',
446        xml_no_diffdom:  'Cannot use XMLHttpRequest to load data from different domain %',
447        xml_no_headers:  'Cannot use XMLHttpRequest loader or ActiveX loader, POST method: headers setting is not supported, needed to work with encodings correctly',
448        xml_no_form_upl: 'Cannot use XMLHttpRequest loader: direct form elements using and uploading are not implemented'
449    });
450
451    this.load = function() {
452        if (this.queryElem.length) return ['xml_no_form_upl'];
453
454        // XMLHttpRequest (and MS ActiveX'es) cannot work with different domains.
455        if (this.url.match(new RegExp('^([a-z]+)://([^\\/]+)(.*)', 'i'))) {
456            if (RegExp.$2.toLowerCase() == document.location.hostname.toLowerCase()) {
457                this.url = RegExp.$3;
458            } else {
459                return ['xml_no_diffdom', RegExp.$2];
460            }
461        }
462
463        // Try to obtain a loader.
464        var xr = null;
465        if (window.XMLHttpRequest) {
466            try { xr = new XMLHttpRequest() } catch(e) {}
467        } else if (window.ActiveXObject) {
468            try { xr = new ActiveXObject("Microsoft.XMLHTTP") } catch(e) {}
469            if (!xr) try { xr = new ActiveXObject("Msxml2.XMLHTTP") } catch (e) {}
470        }
471        if (!xr) return ['xml_no'];
472
473        // Loading method detection. We cannot POST if we cannot set "octet-stream"
474        // header, because we need to process the encoded data in the backend manually.
475        var canSetHeaders = window.ActiveXObject || xr.setRequestHeader;
476        if (!this.method) this.method = canSetHeaders? 'POST' : 'GET';
477
478        // Build & validate the full URL.
479        if (this.method == 'GET') {
480            if (this.queryText) this.url += (this.url.indexOf('?') >= 0? '&' : '?') + this.queryText;
481            this.queryText = '';
482            if (this.url.length > JsHttpRequest.MAX_URL_LEN) return ['url_too_long', JsHttpRequest.MAX_URL_LEN];
483        } else if (this.method == 'POST' && !canSetHeaders) {
484            return ['xml_no_headers'];
485        }
486
487        // Add ID to the url if we need to disable the cache.
488        this.url += (this.url.indexOf('?') >= 0? '&' : '?') + 'JsHttpRequest=' + (req.caching? '0' : this.id) + '-xml';
489
490        // Assign the result handler.
491        var id = this.id;
492        xr.onreadystatechange = function() {
493            if (xr.readyState != 4) return;
494            // Avoid memory leak by removing the closure.
495            xr.onreadystatechange = JsHttpRequest._dummy;
496            req.status = null;
497            try {
498                // In case of abort() call, xr.status is unavailable and generates exception.
499                // But xr.readyState equals to 4 in this case. Stupid behaviour. :-(
500                req.status = xr.status;
501                req.responseText = xr.responseText;
502            } catch (e) {}
503            if (!req.status) return;
504            try {
505                // Prepare generator function & catch syntax errors on this stage.
506                eval('JsHttpRequest._tmp = function(id) { var d = ' + req.responseText + '; d.id = id; JsHttpRequest.dataReady(d); }');
507            } catch (e) {
508                // Note that FF 2.0 does not throw any error from onreadystatechange handler.
509                return req._error('js_invalid', req.responseText)
510            }
511            // Call associated dataReady() outside the try-catch block
512            // to pass exceptions in onreadystatechange in usual manner.
513            JsHttpRequest._tmp(id);
514            JsHttpRequest._tmp = null;
515        };
516
517        // Open & send the request.
518        xr.open(this.method, this.url, true, this.username, this.password);
519        if (canSetHeaders) {
520            // Pass pending headers.
521            for (var i = 0; i < req._reqHeaders.length; i++) {
522                xr.setRequestHeader(req._reqHeaders[i][0], req._reqHeaders[i][1]);
523            }
524            // Set non-default Content-type. We cannot use
525            // "application/x-www-form-urlencoded" here, because
526            // in PHP variable HTTP_RAW_POST_DATA is accessible only when
527            // enctype is not default (e.g., "application/octet-stream"
528            // is a good start). We parse POST data manually in backend
529            // library code. Note that Safari sets by default "x-www-form-urlencoded"
530            // header, but FF sets "text/xml" by default.
531            xr.setRequestHeader('Content-Type', 'application/octet-stream');
532        }
533        xr.send(this.queryText);
534
535        // No SPAN is used for this loader.
536        this.span = null;
537        this.xr = xr; // save for later usage on abort()
538
539        // Success.
540        return null;
541    }
542
543    // Override req.getAllResponseHeaders method.
544    this.getAllResponseHeaders = function() {
545        return this.xr.getAllResponseHeaders();
546    }
547
548    // Override req.getResponseHeader method.
549    this.getResponseHeader = function(label) {
550        return this.xr.getResponseHeader(label);
551    }
552
553    this.abort = function() {
554        this.xr.abort();
555        this.xr = null;
556    }
557}}
558// }}}
559
560
561// {{{ script
562// Loader: SCRIPT tag.
563// [+] Most cross-browser.
564// [+] Supports loading from different domains.
565// [-] Only GET method is supported.
566// [-] No uploading support.
567// [-] Backend data cannot be browser-cached.
568//
569JsHttpRequest.LOADERS.script = { loader: function(req) {
570    JsHttpRequest.extend(req._errors, {
571        script_only_get:   'Cannot use SCRIPT loader: it supports only GET method',
572        script_no_form:    'Cannot use SCRIPT loader: direct form elements using and uploading are not implemented'
573    })
574
575    this.load = function() {
576        // Move GET parameters to the URL itself.
577        if (this.queryText) this.url += (this.url.indexOf('?') >= 0? '&' : '?') + this.queryText;
578        this.url += (this.url.indexOf('?') >= 0? '&' : '?') + 'JsHttpRequest=' + this.id + '-' + 'script';
579        this.queryText = '';
580
581        if (!this.method) this.method = 'GET';
582        if (this.method !== 'GET') return ['script_only_get'];
583        if (this.queryElem.length) return ['script_no_form'];
584        if (this.url.length > JsHttpRequest.MAX_URL_LEN) return ['url_too_long', JsHttpRequest.MAX_URL_LEN];
585        if (req._reqHeaders.length) return ['no_headers', 'SCRIPT'];
586
587        var th = this, d = document, s = null, b = d.body;
588        if (!window.opera) {
589            // Safari, IE, FF, Opera 7.20.
590            this.span = s = d.createElement('SCRIPT');
591            var closure = function() {
592                s.language = 'JavaScript';
593                if (s.setAttribute) s.setAttribute('src', th.url); else s.src = th.url;
594                b.insertBefore(s, b.lastChild);
595            }
596        } else {
597            // Oh shit! Damned stupid Opera 7.23 does not allow to create SCRIPT
598            // element over createElement (in HEAD or BODY section or in nested SPAN -
599            // no matter): it is created deadly, and does not response the href assignment.
600            // So - always create SPAN.
601            this.span = s = d.createElement('SPAN');
602            s.style.display = 'none';
603            b.insertBefore(s, b.lastChild);
604            s.innerHTML = 'Workaround for IE.<s'+'cript></' + 'script>';
605            var closure = function() {
606                s = s.getElementsByTagName('SCRIPT')[0]; // get with timeout!
607                s.language = 'JavaScript';
608                if (s.setAttribute) s.setAttribute('src', th.url); else s.src = th.url;
609            }
610        }
611        JsHttpRequest.setTimeout(closure, 10);
612
613        // Success.
614        return null;
615    }
616}}
617// }}}
618
619
620// {{{ form
621// Loader: FORM & IFRAME.
622// [+] Supports file uploading.
623// [+] GET and POST methods are supported.
624// [+] Supports loading from different domains.
625// [-] Uses a lot of system resources.
626// [-] Backend data cannot be browser-cached.
627// [-] Pollutes browser history on some old browsers.
628//
629JsHttpRequest.LOADERS.form = { loader: function(req) {
630    JsHttpRequest.extend(req._errors, {
631        form_el_not_belong:  'Element "%" does not belong to any form!',
632        form_el_belong_diff: 'Element "%" belongs to a different form. All elements must belong to the same form!',
633        form_el_inv_enctype: 'Attribute "enctype" of the form must be "%" (for IE), "%" given.'
634    })
635
636    this.load = function() {
637        var th = this;
638
639        if (!th.method) th.method = 'POST';
640        th.url += (th.url.indexOf('?') >= 0? '&' : '?') + 'JsHttpRequest=' + th.id + '-' + 'form';
641
642        if (req._reqHeaders.length) return ['no_headers', 'FORM'];
643
644        // If GET, build full URL. Then copy QUERY_STRING to queryText.
645        if (th.method == 'GET') {
646            if (th.queryText) th.url += (th.url.indexOf('?') >= 0? '&' : '?') + th.queryText;
647            if (th.url.length > JsHttpRequest.MAX_URL_LEN) return ['url_too_long', JsHttpRequest.MAX_URL_LEN];
648            var p = th.url.split('?', 2);
649            th.url = p[0];
650            th.queryText = p[1] || '';
651        }
652
653        // Check if all form elements belong to same form.
654        var form = null;
655        var wholeFormSending = false;
656        if (th.queryElem.length) {
657            if (th.queryElem[0].e.tagName.toUpperCase() == 'FORM') {
658                // Whole FORM sending.
659                form = th.queryElem[0].e;
660                wholeFormSending = true;
661                th.queryElem = [];
662            } else {
663                // If we have at least one form element, we use its FORM as a POST container.
664                form = th.queryElem[0].e.form;
665                // Validate all the elements.
666                for (var i = 0; i < th.queryElem.length; i++) {
667                    var e = th.queryElem[i].e;
668                    if (!e.form) {
669                        return ['form_el_not_belong', e.name];
670                    }
671                    if (e.form != form) {
672                        return ['form_el_belong_diff', e.name];
673                    }
674                }
675            }
676
677            // Check enctype of the form.
678            if (th.method == 'POST') {
679                var need = "multipart/form-data";
680                var given = (form.attributes.encType && form.attributes.encType.nodeValue) || (form.attributes.enctype && form.attributes.enctype.value) || form.enctype;
681                if (given != need) {
682                    return ['form_el_inv_enctype', need, given];
683                }
684            }
685        }
686
687        // Create invisible IFRAME with temporary form (form is used on empty queryElem).
688        // We ALWAYS create th IFRAME in the document of the form - for Opera 7.20.
689        var d = form && (form.ownerDocument || form.document) || document;
690        var ifname = 'jshr_i_' + th.id;
691        var s = th.span = d.createElement('DIV');
692        s.style.position = 'absolute';
693        s.style.display = 'none';
694        s.style.visibility = 'hidden';
695        s.innerHTML =
696            (form? '' : '<form' + (th.method == 'POST'? ' enctype="multipart/form-data" method="post"' : '') + '></form>') + // stupid IE, MUST use innerHTML assignment :-(
697            '<iframe name="' + ifname + '" id="' + ifname + '" style="width:0px; height:0px; overflow:hidden; border:none"></iframe>'
698        if (!form) {
699            form = th.span.firstChild;
700        }
701
702        // Insert generated form inside the document.
703        // Be careful: don't forget to close FORM container in document body!
704        d.body.insertBefore(s, d.body.lastChild);
705
706        // Function to safely set the form attributes. Parameter attr is NOT a hash
707        // but an array, because "for ... in" may badly iterate over derived attributes.
708        var setAttributes = function(e, attr) {
709            var sv = [];
710            var form = e;
711            // This strange algorythm is needed, because form may  contain element
712            // with name like 'action'. In IE for such attribute will be returned
713            // form element node, not form action. Workaround: copy all attributes
714            // to new empty form and work with it, then copy them back. This is
715            // THE ONLY working algorythm since a lot of bugs in IE5.0 (e.g.
716            // with e.attributes property: causes IE crash).
717            if (e.mergeAttributes) {
718                var form = d.createElement('form');
719                form.mergeAttributes(e, false);
720            }
721            for (var i = 0; i < attr.length; i++) {
722                var k = attr[i][0], v = attr[i][1];
723                // TODO: http://forum.dklab.ru/viewtopic.php?p=129059#129059
724                sv[sv.length] = [k, form.getAttribute(k)];
725                form.setAttribute(k, v);
726            }
727            if (e.mergeAttributes) {
728                e.mergeAttributes(form, false);
729            }
730            return sv;
731        }
732
733        // Run submit with delay - for old Opera: it needs some time to create IFRAME.
734        var closure = function() {
735            // Save JsHttpRequest object to new IFRAME.
736            top.JsHttpRequestGlobal = JsHttpRequest;
737
738            // Disable ALL the form elements.
739            var savedNames = [];
740            if (!wholeFormSending) {
741                for (var i = 0, n = form.elements.length; i < n; i++) {
742                    savedNames[i] = form.elements[i].name;
743                    form.elements[i].name = '';
744                }
745            }
746
747            // Insert hidden fields to the form.
748            var qt = th.queryText.split('&');
749            for (var i = qt.length - 1; i >= 0; i--) {
750                var pair = qt[i].split('=', 2);
751                var e = d.createElement('INPUT');
752                e.type = 'hidden';
753                e.name = unescape(pair[0]);
754                e.value = pair[1] != null? unescape(pair[1]) : '';
755                form.appendChild(e);
756            }
757
758
759            // Change names of along user-passed form elements.
760            for (var i = 0; i < th.queryElem.length; i++) {
761                th.queryElem[i].e.name = th.queryElem[i].name;
762            }
763
764            // Temporary modify form attributes, submit form, restore attributes back.
765            var sv = setAttributes(
766                form,
767                [
768                    ['action',   th.url],
769                    ['method',   th.method],
770                    ['onsubmit', null],
771                    ['target',   ifname]
772                ]
773            );
774            form.submit();
775            setAttributes(form, sv);
776
777            // Remove generated temporary hidden elements from the top of the form.
778            for (var i = 0; i < qt.length; i++) {
779                // Use "form.firstChild.parentNode", not "form", or IE5 crashes!
780                form.lastChild.parentNode.removeChild(form.lastChild);
781            }
782            // Enable all disabled elements back.
783            if (!wholeFormSending) {
784                for (var i = 0, n = form.elements.length; i < n; i++) {
785                    form.elements[i].name = savedNames[i];
786                }
787            }
788        }
789        JsHttpRequest.setTimeout(closure, 100);
790
791        // Success.
792        return null;
793    }
794}}
795// }}}
796