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