File: JsHttpRequest.js

package info (click to toggle)
frontaccounting 2.2.10-3.1
  • links: PTS
  • area: main
  • in suites: wheezy
  • size: 6,252 kB
  • sloc: php: 64,938; sql: 3,014; sh: 390; makefile: 38
file content (803 lines) | stat: -rw-r--r-- 31,761 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
/**
 * JsHttpRequest: JavaScript "AJAX" data loader
 *
 * @license LGPL
 * @author Dmitry Koterov, http://en.dklab.ru/lib/JsHttpRequest/
 * @version 5.x
 */

// {{{
function JsHttpRequest() {
    // Standard properties.
    var t = this;
    t.onreadystatechange = null;
    t.readyState         = 0;
    t.responseText       = null;
    t.responseXML        = null;
    t.status             = 200;
    t.statusText         = "OK";
    // JavaScript response array/hash
    t.responseJS         = null;

    // Additional properties.
    t.caching            = false;        // need to use caching?
    t.loader             = null;         // loader to use ('form', 'script', 'xml'; null - autodetect)
    t.session_name       = "PHPSESSID";  // set to SID cookie or GET parameter name

    // Internals.
    t._ldObj              = null;  // used loader object
    t._reqHeaders        = [];    // collected request headers
    t._openArgs          = null;  // parameters from open()
    t._errors = {
        inv_form_el:        'Invalid FORM element detected: name=%, tag=%',
        must_be_single_el:  'If used, <form> must be a single HTML element in the list.',
        js_invalid:         'JavaScript code generated by backend is invalid!\n%',
        url_too_long:       'Cannot use so long query with GET request (URL is larger than % bytes)',
        unk_loader:         'Unknown loader: %',
        no_loaders:         'No loaders registered at all, please check JsHttpRequest.LOADERS array',
        no_loader_matched:  'Cannot find a loader which may process the request. Notices are:\n%'
    }
    
    /**
     * Aborts the request. Behaviour of this function for onreadystatechange() 
     * is identical to IE (most universal and common case). E.g., readyState -> 4
     * on abort() after send().
     */
    t.abort = function() { with (this) {
        if (_ldObj && _ldObj.abort) _ldObj.abort();
        _cleanup();
        if (readyState == 0) {
            // start->abort: no change of readyState (IE behaviour)
            return;
        }
        if (readyState == 1 && !_ldObj) {
            // open->abort: no onreadystatechange call, but change readyState to 0 (IE).
            // send->abort: change state to 4 (_ldObj is not null when send() is called)
            readyState = 0;
            return;
        }
        _changeReadyState(4, true); // 4 in IE & FF on abort() call; Opera does not change to 4.
    }}
    
    /**
     * Prepares the object for data loading.
     * You may also pass URLs like "GET url" or "script.GET url".
     */
    t.open = function(method, url, asyncFlag, username, password) { with (this) {
        // Extract methor and loader from the URL (if present).
        if (url.match(/^((\w+)\.)?(GET|POST)\s+(.*)/i)) {
            this.loader = RegExp.$2? RegExp.$2 : null;
            method = RegExp.$3;
            url = RegExp.$4; 
        }
        // Append SID to original URL. Use try...catch for security problems.
        try {
            if (
                document.location.search.match(new RegExp('[&?]' + session_name + '=([^&?]*)'))
                || document.cookie.match(new RegExp('(?:;|^)\\s*' + session_name + '=([^;]*)'))
            ) {
                url += (url.indexOf('?') >= 0? '&' : '?') + session_name + "=" + this.escape(RegExp.$1);
            }
        } catch (e) {}
        // Store open arguments to hash.
        _openArgs = {
            method:     (method || '').toUpperCase(),
            url:        url,
            asyncFlag:  asyncFlag,
            username:   username != null? username : '',
            password:   password != null? password : ''
        }
        _ldObj = null;
        _changeReadyState(1, true); // compatibility with XMLHttpRequest
        return true;
    }}
    
    /**
     * Sends a request to a server.
     */
    t.send = function(content) {
        if (!this.readyState) {
            // send without open or after abort: no action (IE behaviour).
            return;
        }
        this._changeReadyState(1, true); // compatibility with XMLHttpRequest
        this._ldObj = null;
        
        // Prepare to build QUERY_STRING from query hash.
        var queryText = [];
        var queryElem = [];
        if (!this._hash2query(content, null, queryText, queryElem)) return;
    
        // Solve the query hashcode & return on cache hit.
        var hash = null;
        if (this.caching && !queryElem.length) {
            hash = this._openArgs.username + ':' + this._openArgs.password + '@' + this._openArgs.url + '|' + queryText + "#" + this._openArgs.method;
            var cache = JsHttpRequest.CACHE[hash];
            if (cache) {
                this._dataReady(cache[0], cache[1]);
                return false;
            }
        }
    
        // Try all the loaders.
        var loader = (this.loader || '').toLowerCase();
        if (loader && !JsHttpRequest.LOADERS[loader]) return this._error('unk_loader', loader);
        var errors = [];
        var lds = JsHttpRequest.LOADERS;
        for (var tryLoader in lds) {
            var ldr = lds[tryLoader].loader;
            if (!ldr) continue; // exclude possibly derived prototype properties from "for .. in".
            if (loader && tryLoader != loader) continue;
            // Create sending context.
            var ldObj = new ldr(this);
            JsHttpRequest.extend(ldObj, this._openArgs);
            JsHttpRequest.extend(ldObj, {
                queryText:  queryText.join('&'),
                queryElem:  queryElem,
                id:         (new Date().getTime()) + "" + JsHttpRequest.COUNT++,
                hash:       hash,
                span:       null
            });
            var error = ldObj.load();
            if (!error) {
                // Save loading script.
                this._ldObj = ldObj;
                JsHttpRequest.PENDING[ldObj.id] = this;
                return true;
            }
            if (!loader) {
                errors[errors.length] = '- ' + tryLoader.toUpperCase() + ': ' + this._l(error);
            } else {
                return this._error(error);
            }
        }
    
        // If no loader matched, generate error message.
        return tryLoader? this._error('no_loader_matched', errors.join('\n')) : this._error('no_loaders');
    }
    
    /**
     * Returns all response headers (if supported).
     */
    t.getAllResponseHeaders = function() { with (this) {
        return _ldObj && _ldObj.getAllResponseHeaders? _ldObj.getAllResponseHeaders() : [];
    }}

    /**
     * Returns one response header (if supported).
     */
    t.getResponseHeader = function(label) { with (this) {
        return _ldObj && _ldObj.getResponseHeader? _ldObj.getResponseHeader(label) : null;
    }}

    /**
     * Adds a request header to a future query.
     */
    t.setRequestHeader = function(label, value) { with (this) {
        _reqHeaders[_reqHeaders.length] = [label, value];
    }}
    
    //
    // Internal functions.
    //
    
    /**
     * Do all the work when a data is ready.
     */
    t._dataReady = function(text, js) { with (this) {
        if (caching && _ldObj) JsHttpRequest.CACHE[_ldObj.hash] = [text, js];
        responseText = responseXML = text;
        responseJS = js;
        if (js !== null) {
            status = 200;
            statusText = "OK";
        } else {
             // The special value "null" from a backend means Fatal error.
             // User cannot assign null to $_RESULT manually, it is 
             // translated to false to avoid 500 error collisions.
            status = 500;
            statusText = "Internal Server Error";
        }
        _changeReadyState(2);
        _changeReadyState(3);
        _changeReadyState(4);
        _cleanup();
    }}
    
    /**
     * Analog of sprintf(), but translates the first parameter by _errors.
     */
    t._l = function(args) {
        var i = 0, p = 0, msg = this._errors[args[0]];
        // Cannot use replace() with a callback, because it is incompatible with IE5.
        while ((p = msg.indexOf('%', p)) >= 0) {
            var a = args[++i] + "";
            msg = msg.substring(0, p) + a + msg.substring(p + 1, msg.length);
            p += 1 + a.length;
        }
        return msg;
    }

    /** 
     * Called on error.
     */
    t._error = function(msg) {
        msg = this._l(typeof(msg) == 'string'? arguments : msg)
        msg = "JsHttpRequest: " + msg;
        if (!window.Error) {
            // Very old browser...
            throw msg;
        } else if ((new Error(1, 'test')).description == "test") {
            // We MUST (!!!) pass 2 parameters to the Error() constructor for IE5.
            throw new Error(1, msg);
        } else {
            // Mozilla does not support two-parameter call style.
            throw new Error(msg);
        }
    }
    
    /**
     * 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.
     */
    t._hash2query = function(content, prefix, queryText, queryElem) {
        if (prefix == null) prefix = "";
        if((''+typeof(content)).toLowerCase() == 'object') {
            var formAdded = false;
            if (content && content.parentNode && content.parentNode.appendChild && content.tagName && content.tagName.toUpperCase() == 'FORM') {
                content = { form: content };
            }
            for (var k in content) {
                var v = content[k];
                if (v instanceof Function) continue;
                var curPrefix = prefix? prefix + '[' + this.escape(k) + ']' : this.escape(k);
                var isFormElement = v && v.parentNode && v.parentNode.appendChild && v.tagName;
                if (isFormElement) {
                    var tn = v.tagName.toUpperCase();
                    if (tn == 'FORM') {
                        // FORM itself is passed.
                        formAdded = true;
                    } else if (tn == 'INPUT' || tn == 'TEXTAREA' || tn == 'SELECT' || tn == 'BUTTON') {
                        // This is a single form elemenent.
                    } else {
                        return this._error('inv_form_el', (v.name||''), v.tagName);
                    }
                    queryElem[queryElem.length] = { name: curPrefix, e: v };
                } 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;
                    // Convert JS boolean true and false to corresponding PHP values.
                    if (v === true) v = 1; 
                    if (v === false) v = '';
                    queryText[queryText.length] = curPrefix + "=" + this.escape('' + v);
                }
                if (formAdded && queryElem.length > 1) {
                    return this._error('must_be_single_el');
                }
            }
        } else {
            queryText[queryText.length] = content;
        }
        return true;
    }
    
    /**
     * Remove last used script element (clean memory).
     */
    t._cleanup = function() {
        var ldObj = this._ldObj;
        if (!ldObj) return;
        // Mark this loading as aborted.
        JsHttpRequest.PENDING[ldObj.id] = false;
        var span = ldObj.span;
        if (!span) return;
        // Do NOT use iframe.contentWindow.back() - it is incompatible with Opera 9!
        ldObj.span = null;
        var closure = function() {
            span.parentNode.removeChild(span);
        }
        // IE5 crashes on setTimeout(function() {...}, ...) construction! Use tmp variable.
        JsHttpRequest.setTimeout(closure, 50);
    }
    
    /**
     * Change current readyState and call trigger method.
     */
    t._changeReadyState = function(s, reset) { with (this) {
        if (reset) {
            status = statusText = responseJS = null;
            responseText = '';
        }
        readyState = s;
        if (onreadystatechange) onreadystatechange();
    }}
    
    /**
     * JS escape() does not quote '+'.
     */
    t.escape = function(s) {
        return escape(s).replace(new RegExp('\\+','g'), '%2B');
    }
}


// Global library variables.
JsHttpRequest.COUNT = 0;              // unique ID; used while loading IDs generation
JsHttpRequest.MAX_URL_LEN = 2000;     // maximum URL length
JsHttpRequest.CACHE = {};             // cached data
JsHttpRequest.PENDING = {};           // pending loadings
JsHttpRequest.LOADERS = {};           // list of supported data loaders (filled at the bottom of the file)
JsHttpRequest._dummy = function() {}; // avoid memory leaks


/**
 * These functions are dirty hacks for IE 5.0 which does not increment a
 * reference counter for an object passed via setTimeout(). So, if this 
 * object (closure function) is out of scope at the moment of timeout 
 * applying, IE 5.0 crashes. 
 */

/**
 * Timeout wrappers storage. Used to avoid zeroing of referece counts in IE 5.0.
 * Please note that you MUST write "window.setTimeout", not "setTimeout", else
 * IE 5.0 crashes again. Strange, very strange...
 */
JsHttpRequest.TIMEOUTS = { s: window.setTimeout, c: window.clearTimeout };

/**
 * Wrapper for IE5 buggy setTimeout.
 * Use this function instead of a usual setTimeout().
 */
JsHttpRequest.setTimeout = function(func, dt) {
    // Always save inside the window object before a call (for FF)!
    window.JsHttpRequest_tmp = JsHttpRequest.TIMEOUTS.s; 
    if (typeof(func) == "string") {
        id = window.JsHttpRequest_tmp(func, dt);
    } else {
        var id = null;
        var mediator = function() {
            func();
            delete JsHttpRequest.TIMEOUTS[id]; // remove circular reference
        }
        id = window.JsHttpRequest_tmp(mediator, dt);
        // Store a reference to the mediator function to the global array
        // (reference count >= 1); use timeout ID as an array key;
        JsHttpRequest.TIMEOUTS[id] = mediator;
    }
    window.JsHttpRequest_tmp = null; // no delete() in IE5 for window
    return id;
}

/**
 * Complimental wrapper for clearTimeout. 
 * Use this function instead of usual clearTimeout().
 */
JsHttpRequest.clearTimeout = function(id) {
    window.JsHttpRequest_tmp = JsHttpRequest.TIMEOUTS.c;
    delete JsHttpRequest.TIMEOUTS[id]; // remove circular reference
    var r = window.JsHttpRequest_tmp(id);
    window.JsHttpRequest_tmp = null; // no delete() in IE5 for window
    return r;
}


/**
 * Global static function.
 * Simple interface for most popular use-cases.
 * You may also pass URLs like "GET url" or "script.GET url".
 */
JsHttpRequest.query = function(url, content, onready, nocache) {
    var req = new this();
    req.caching = !nocache;
    req.onreadystatechange = function() {
        if (req.readyState == 4) {
            onready(req.responseJS, req.responseText);
        }
    }
    req.open(null, url, true);
    req.send(content);
}


/**
 * Global static function.
 * Called by server backend script on data load.
 */
JsHttpRequest.dataReady = function(d) {
    var th = this.PENDING[d.id];
    delete this.PENDING[d.id];
    if (th) {
        th._dataReady(d.text, d.js);
    } else if (th !== false) {
        throw "dataReady(): unknown pending id: " + d.id;
    }
}


// Adds all the properties of src to dest.
JsHttpRequest.extend = function(dest, src) {
    for (var k in src) dest[k] = src[k];
}

/**
 * Each loader has the following properties which must be initialized:
 * - method
 * - url
 * - asyncFlag (ignored)
 * - username
 * - password
 * - queryText (string)
 * - queryElem (array)
 * - id
 * - hash
 * - span
 */ 
 
// }}}

// {{{ xml
// Loader: XMLHttpRequest or ActiveX.
// [+] GET and POST methods are supported.
// [+] Most native and memory-cheap method.
// [+] Backend data can be browser-cached.
// [-] Cannot work in IE without ActiveX. 
// [-] No support for loading from different domains.
// [-] No uploading support.
//
JsHttpRequest.LOADERS.xml = { loader: function(req) {
    JsHttpRequest.extend(req._errors, {
        xml_no:          'Cannot use XMLHttpRequest or ActiveX loader: not supported',
        xml_no_diffdom:  'Cannot use XMLHttpRequest to load data from different domain %',
        xml_no_headers:  'Cannot use XMLHttpRequest loader or ActiveX loader, POST method: headers setting is not supported, needed to work with encodings correctly',
        xml_no_form_upl: 'Cannot use XMLHttpRequest loader: direct form elements using and uploading are not implemented'
    });
    
    this.load = function() {
        if (this.queryElem.length) return ['xml_no_form_upl'];
        
        // XMLHttpRequest (and MS ActiveX'es) cannot work with different domains.
        if (this.url.match(new RegExp('^([a-z]+://[^\\/]+)(.*)((:[0-9]*)+)', 'i'))) {
        	// We MUST also check if protocols matched: cannot send from HTTP 
        	// to HTTPS and vice versa.
            if (RegExp.$1.toLowerCase() != document.location.protocol + '//' + document.location.hostname.toLowerCase()) {
                return ['xml_no_diffdom', RegExp.$1];
            }
        }
        
        // Try to obtain a loader.
        var xr = null;
        if (window.XMLHttpRequest) {
            try { xr = new XMLHttpRequest() } catch(e) {}
        } else if (window.ActiveXObject) {
            try { xr = new ActiveXObject("Microsoft.XMLHTTP") } catch(e) {}
            if (!xr) try { xr = new ActiveXObject("Msxml2.XMLHTTP") } catch (e) {}
        }
        if (!xr) return ['xml_no'];
        
        // Loading method detection. We cannot POST if we cannot set "octet-stream" 
        // header, because we need to process the encoded data in the backend manually.
        var canSetHeaders = window.ActiveXObject || xr.setRequestHeader;
        if (!this.method) this.method = canSetHeaders && this.queryText.length? 'POST' : 'GET';
        
        // Build & validate the full URL.
        if (this.method == 'GET') {
            if (this.queryText) this.url += (this.url.indexOf('?') >= 0? '&' : '?') + this.queryText;
            this.queryText = '';
            if (this.url.length > JsHttpRequest.MAX_URL_LEN) return ['url_too_long', JsHttpRequest.MAX_URL_LEN];
        } else if (this.method == 'POST' && !canSetHeaders) {
            return ['xml_no_headers'];
        }
        
        // Add ID to the url if we need to disable the cache.
        this.url += (this.url.indexOf('?') >= 0? '&' : '?') + 'JsHttpRequest=' + (req.caching? '0' : this.id) + '-xml';        
        
        // Assign the result handler.
        var id = this.id;
        xr.onreadystatechange = function() { 
            if (xr.readyState != 4) return;
            // Avoid memory leak by removing the closure.
            xr.onreadystatechange = JsHttpRequest._dummy;
            req.status = null;
            try { 
                // In case of abort() call, xr.status is unavailable and generates exception.
                // But xr.readyState equals to 4 in this case. Stupid behaviour. :-(
                req.status = xr.status;
                req.responseText = xr.responseText;
            } catch (e) {}
            if (!req.status) return;
            try {
                 // Damned Opera returned empty responseText when Status is not 200.
                 var rtext = req.responseText || '{ js: null, text: null }';
                // Prepare generator function & catch syntax errors on this stage.
                eval('JsHttpRequest._tmp = function(id) { var d = ' + rtext + '; d.id = id; JsHttpRequest.dataReady(d); }');
            } catch (e) {
                // Note that FF 2.0 does not throw any error from onreadystatechange handler.
                return req._error('js_invalid', req.responseText)
            }
            // Call associated dataReady() outside the try-catch block 
            // to pass exceptions in onreadystatechange in usual manner.
            JsHttpRequest._tmp(id);
            JsHttpRequest._tmp = null;
        };

        // Open & send the request.
        xr.open(this.method, this.url, true, this.username, this.password);
        if (canSetHeaders) {
            // Pass pending headers.
            for (var i = 0; i < req._reqHeaders.length; i++) {
                xr.setRequestHeader(req._reqHeaders[i][0], req._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. Note that Safari sets by default "x-www-form-urlencoded"
            // header, but FF sets "text/xml" by default.
            xr.setRequestHeader('Content-Type', 'application/octet-stream');
        }
        xr.send(this.queryText);
        
        // No SPAN is used for this loader.
        this.span = null;
        this.xr = xr; // save for later usage on abort()
        
        // Success.
        return null;
    }
    
    // Override req.getAllResponseHeaders method.
    this.getAllResponseHeaders = function() {
        return this.xr.getAllResponseHeaders();
    }
    
    // Override req.getResponseHeader method.
    this.getResponseHeader = function(label) {
        return this.xr.getResponseHeader(label);
    }

    this.abort = function() {
        this.xr.abort();
        this.xr = null;
    }
}}
// }}}


// {{{ script
// Loader: SCRIPT tag.
// [+] Most cross-browser. 
// [+] Supports loading from different domains.
// [-] Only GET method is supported.
// [-] No uploading support.
// [-] Backend data cannot be browser-cached.
//
JsHttpRequest.LOADERS.script = { loader: function(req) {
    JsHttpRequest.extend(req._errors, {
        script_only_get:   'Cannot use SCRIPT loader: it supports only GET method',
        script_no_form:    'Cannot use SCRIPT loader: direct form elements using and uploading are not implemented'
    })
    
    this.load = function() {
        // Move GET parameters to the URL itself.
        if (this.queryText) this.url += (this.url.indexOf('?') >= 0? '&' : '?') + this.queryText;
        this.url += (this.url.indexOf('?') >= 0? '&' : '?') + 'JsHttpRequest=' + this.id + '-' + 'script';        
        this.queryText = '';
        
        if (!this.method) this.method = 'GET';
        if (this.method !== 'GET') return ['script_only_get'];
        if (this.queryElem.length) return ['script_no_form'];
        if (this.url.length > JsHttpRequest.MAX_URL_LEN) return ['url_too_long', JsHttpRequest.MAX_URL_LEN];

        var th = this, d = document, s = null, b = d.body;
        if (!window.opera) {
            // Safari, IE, FF, Opera 7.20.
            this.span = s = d.createElement('SCRIPT');
            var closure = function() {
                s.language = 'JavaScript';
                if (s.setAttribute) s.setAttribute('src', th.url); else s.src = th.url;
                b.insertBefore(s, b.lastChild);
            }
        } else {
            // Oh shit! Damned stupid 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 response the href assignment.
            // So - always create SPAN.
            this.span = s = d.createElement('SPAN');
            s.style.display = 'none';
            b.insertBefore(s, b.lastChild);
            s.innerHTML = 'Workaround for IE.<s'+'cript></' + 'script>';
            var closure = function() {
                s = s.getElementsByTagName('SCRIPT')[0]; // get with timeout!
                s.language = 'JavaScript';
                if (s.setAttribute) s.setAttribute('src', th.url); else s.src = th.url;
            }
        }
        JsHttpRequest.setTimeout(closure, 10);
        
        // Success.
        return null;
    }
}}
// }}}


// {{{ form
// Loader: FORM & IFRAME.
// [+] Supports file uploading.
// [+] GET and POST methods are supported.
// [+] Supports loading from different domains.
// [-] Uses a lot of system resources.
// [-] Backend data cannot be browser-cached.
// [-] Pollutes browser history on some old browsers.
//
JsHttpRequest.LOADERS.form = { loader: function(req) {
    JsHttpRequest.extend(req._errors, {
        form_el_not_belong:  'Element "%" does not belong to any form!',
        form_el_belong_diff: 'Element "%" belongs to a different form. All elements must belong to the same form!',
        form_el_inv_enctype: 'Attribute "enctype" of the form must be "%" (for IE), "%" given.'
    })
    
    this.load = function() {
        var th = this;
     
        if (!th.method) th.method = 'POST';
        th.url += (th.url.indexOf('?') >= 0? '&' : '?') + 'JsHttpRequest=' + th.id + '-' + 'form';
        
        // If GET, build full URL. Then copy QUERY_STRING to queryText.
        if (th.method == 'GET') {
            if (th.queryText) th.url += (th.url.indexOf('?') >= 0? '&' : '?') + th.queryText;
            if (th.url.length > JsHttpRequest.MAX_URL_LEN) return ['url_too_long', JsHttpRequest.MAX_URL_LEN];
            var p = th.url.split('?', 2);
            th.url = p[0];
            th.queryText = p[1] || '';
        }

        // Check if all form elements belong to same form.
        var form = null;
        var wholeFormSending = false;
        if (th.queryElem.length) {
            if (th.queryElem[0].e.tagName.toUpperCase() == 'FORM') {
                // Whole FORM sending.
                form = th.queryElem[0].e;
                wholeFormSending = true;
                th.queryElem = [];
            } else {
                // If we have at least one form element, we use its FORM as a POST container.
                form = th.queryElem[0].e.form;
                // Validate all the elements.
                for (var i = 0; i < th.queryElem.length; i++) {
                    var e = th.queryElem[i].e;
                    if (!e.form) {
                        return ['form_el_not_belong', e.name];
                    }
                    if (e.form != form) {
                        return ['form_el_belong_diff', e.name];
                    }
                }
            }
            
            // Check enctype of the form.
            if (th.method == 'POST') {
                var need = "multipart/form-data";
                var given = (form.attributes.encType && form.attributes.encType.nodeValue) || (form.attributes.enctype && form.attributes.enctype.value) || form.enctype;
                if (given != need) {
                    return ['form_el_inv_enctype', need, given];
                }
            }
        }

        // Create invisible IFRAME with temporary form (form is used on empty queryElem).
        // We ALWAYS create th IFRAME in the document of the form - for Opera 7.20.
        var d = form && (form.ownerDocument || form.document) || document;
        var ifname = 'jshr_i_' + th.id;
        var s = th.span = d.createElement('DIV');
        s.style.position = 'absolute';
        s.style.display = 'none';
        s.style.visibility = 'hidden';
        s.innerHTML = 
            (form? '' : '<form' + (th.method == 'POST'? ' enctype="multipart/form-data" method="post"' : '') + '></form>') + // stupid IE, MUST use innerHTML assignment :-(
            '<iframe name="' + ifname + '" id="' + ifname + '" style="width:0px; height:0px; overflow:hidden; border:none"></iframe>'
        if (!form) {
            form = th.span.firstChild;
        }

        // Insert generated form inside the document.
        // Be careful: don't forget to close FORM container in document body!
        d.body.insertBefore(s, d.body.lastChild);

        // Function to safely set the form attributes. Parameter attr is NOT a hash 
        // but an array, because "for ... in" may badly iterate over derived attributes.
        var 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 = d.createElement('form');
                form.mergeAttributes(e, false);
            }
            for (var i = 0; i < attr.length; i++) {
                var k = attr[i][0], v = attr[i][1];
                // TODO: http://forum.dklab.ru/viewtopic.php?p=129059#129059
                sv[sv.length] = [k, form.getAttribute(k)];
                form.setAttribute(k, v);
            }
            if (e.mergeAttributes) {
                e.mergeAttributes(form, false);
            }
            return sv;
        }

        // Run submit with delay - for old Opera: it needs some time to create IFRAME.
        var closure = function() {
            // Save JsHttpRequest object to new IFRAME.
            top.JsHttpRequestGlobal = JsHttpRequest;
            
            // Disable ALL the form elements.
            var savedNames = [];
            if (!wholeFormSending) {
                for (var i = 0, n = form.elements.length; i < n; i++) {
                    savedNames[i] = form.elements[i].name;
                    form.elements[i].name = '';
                }
            }

            // Insert hidden fields to the form.
            var qt = th.queryText.split('&');
            for (var i = qt.length - 1; i >= 0; i--) {
                var pair = qt[i].split('=', 2);
                var e = d.createElement('INPUT');
                e.type = 'hidden';
                e.name = unescape(pair[0]);
                e.value = pair[1] != null? unescape(pair[1]) : '';
                form.appendChild(e);
            }


            // Change names of along user-passed form elements.
            for (var i = 0; i < th.queryElem.length; i++) {
                th.queryElem[i].e.name = th.queryElem[i].name;
            }

            // Temporary modify form attributes, submit form, restore attributes back.
            var sv = setAttributes(
                form, 
                [
                    ['action',   th.url],
                    ['method',   th.method],
                    ['onsubmit', null],
                    ['target',   ifname]
                ]
            );
            form.submit();
            setAttributes(form, sv);

            // Remove generated temporary hidden elements from the top of the form.
            for (var i = 0; i < qt.length; i++) {
                // Use "form.firstChild.parentNode", not "form", or IE5 crashes!
                form.lastChild.parentNode.removeChild(form.lastChild);
            }
            // Enable all disabled elements back.
            if (!wholeFormSending) {
                for (var i = 0, n = form.elements.length; i < n; i++) {
                    form.elements[i].name = savedNames[i];
                }
            }
        }
        JsHttpRequest.setTimeout(closure, 100);

        // Success.
        return null;
    }    
}}
// }}}