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