[0004212] Work Order Entry: fixed error when voided WO refence is reused.
[fa-stable.git] / includes / JsHttpRequest.php
1 <?php
2 /**
3  * JsHttpRequest: PHP backend for JavaScript DHTML loader.
4  * (C) Dmitry Koterov, http://en.dklab.ru
5  *
6  * This library is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU Lesser General Public
8  * License as published by the Free Software Foundation; either
9  * version 2.1 of the License, or (at your option) any later version.
10  * See http://www.gnu.org/copyleft/lesser.html
11  *
12  * Do not remove this comment if you want to use the script!
13  * Íå óäàëÿéòå äàííûé êîììåíòàðèé, åñëè âû õîòèòå èñïîëüçîâàòü ñêðèïò!
14  *
15  * This backend library also supports POST requests additionally to GET.
16  *
17  * @author Dmitry Koterov 
18  * @version 5.x
19  */
20
21 class JsHttpRequest
22 {
23     var $SCRIPT_ENCODING = "windows-1251";
24     var $SCRIPT_DECODE_MODE = '';
25     var $LOADER = null;
26     var $ID = null;    
27     var $RESULT = null;
28     
29     // Internal; uniq value.
30     var $_uniqHash;
31     // Magic number for display_error checking.
32     var $_magic = 14623;
33     // Previous display_errors value.
34     var $_prevDisplayErrors = null;    
35     // Internal: response content-type depending on loader type.
36     var $_contentTypes = array(
37         "script" => "text/javascript",
38         "xml"    => "text/plain", // In XMLHttpRequest mode we must return text/plain - stupid Opera 8.0. :(
39         "form"   => "text/html",
40         ""       => "text/plain", // for unknown loader
41     );
42     // Internal: conversion to UTF-8 JSON cancelled because of non-ascii key.
43     var $_toUtfFailed = false;
44     // Internal: list of characters 128...255 (for strpbrk() ASCII check).
45     var $_nonAsciiChars = '';
46     // Which Unicode conversion function is available?
47     var $_unicodeConvMethod = null;
48     // Emergency memory buffer to be freed on memory_limit error.
49     var $_emergBuffer = null;
50
51     
52     /**
53      * Constructor.
54      * 
55      * Create new JsHttpRequest backend object and attach it
56      * to script output buffer. As a result - script will always return
57      * correct JavaScript code, even in case of fatal errors.
58      *
59      * QUERY_STRING is in form of: PHPSESSID=<sid>&a=aaa&b=bbb&JsHttpRequest=<id>-<loader>
60      * where <id> is a request ID, <loader> is a loader name, <sid> - a session ID (if present), 
61      * PHPSESSID - session parameter name (by default = "PHPSESSID").
62      * 
63      * If an object is created WITHOUT an active AJAX query, it is simply marked as
64      * non-active. Use statuc method isActive() to check.
65      */
66     function __construct($enc)
67     {
68         global $JsHttpRequest_Active;
69         
70         // To be on a safe side - do not allow to drop reference counter on ob processing.
71         $GLOBALS['_RESULT'] =& $this->RESULT; 
72         
73         // Parse QUERY_STRING.
74         if (array_key_exists('QUERY_STRING', $_SERVER) && preg_match('/^(.*)(?:&|^)JsHttpRequest=(?:(\d+)-)?([^&]+)((?:&|$).*)$/s', @$_SERVER['QUERY_STRING'], $m)) {
75             $this->ID = $m[2];
76             $this->LOADER = strtolower($m[3]);
77             $_SERVER['QUERY_STRING'] = preg_replace('/^&+|&+$/s', '', preg_replace('/(^|&)'.session_name().'=[^&]*&?/s', '&', $m[1] . $m[4]));
78             unset(
79                 $_GET['JsHttpRequest'],
80                 $_REQUEST['JsHttpRequest'],
81                 $_GET[session_name()],
82                 $_POST[session_name()],
83                 $_REQUEST[session_name()]
84             );
85             // Detect Unicode conversion method.
86             $this->_unicodeConvMethod = function_exists('mb_convert_encoding')? 'mb' : (function_exists('iconv')? 'iconv' : null);
87     
88             // Fill an emergency buffer. We erase it at the first line of OB processor
89             // to free some memory. This memory may be used on memory_limit error.
90             $this->_emergBuffer = str_repeat('a', 1024 * 200);
91
92             // Intercept fatal errors via display_errors (seems it is the only way).     
93             $this->_uniqHash = md5('JsHttpRequest' . microtime() . getmypid());
94             $this->_prevDisplayErrors = ini_get('display_errors');
95             ini_set('display_errors', $this->_magic); //
96             ini_set('error_prepend_string', $this->_uniqHash . ini_get('error_prepend_string'));
97             ini_set('error_append_string',  ini_get('error_append_string') . $this->_uniqHash);
98             if (function_exists('xdebug_disable')) xdebug_disable(); // else Fatal errors are not catched
99
100             // Start OB handling early.
101             ob_start(array(&$this, "_obHandler"));
102             $JsHttpRequest_Active = true;
103     
104             // Set up the encoding.
105             $this->setEncoding($enc);
106     
107             // Check if headers are already sent (see Content-Type library usage).
108             // If true - generate a debug message and exit.
109             $file = $line = null;
110             $headersSent = version_compare(PHP_VERSION, "4.3.0") < 0? headers_sent() : headers_sent($file, $line);
111             if ($headersSent) {
112                 trigger_error(
113                     "HTTP headers are already sent" . ($line !== null? " in $file on line $line" : " somewhere in the script") . ". "
114                     . "Possibly you have an extra space (or a newline) before the first line of the script or any library. "
115                     . "Please note that JsHttpRequest uses its own Content-Type header and fails if "
116                     . "this header cannot be set. See header() function documentation for more details",
117                     E_USER_ERROR
118                 );
119                 exit();
120             }
121         } else {
122             $this->ID = 0;
123             $this->LOADER = 'unknown';
124             $JsHttpRequest_Active = false;
125         }
126     }
127     
128
129     /**
130      * Static function.
131      * Returns true if JsHttpRequest output processor is currently active.
132      * 
133      * @return boolean    True if the library is active, false otherwise.
134      */
135     function isActive()
136     {
137         return !empty($GLOBALS['JsHttpRequest_Active']);
138     }
139     
140
141     /**
142      * string getJsCode()
143      * 
144      * Return JavaScript part of the library.
145      */
146     function getJsCode()
147     {
148         return file_get_contents(dirname(__FILE__) . '/JsHttpRequest.js');
149     }
150
151
152     /**
153      * void setEncoding(string $encoding)
154      * 
155      * Set an active script encoding & correct QUERY_STRING according to it.
156      * Examples:
157      *   "windows-1251"          - set plain encoding (non-windows characters, 
158      *                             e.g. hieroglyphs, are totally ignored)
159      *   "windows-1251 entities" - set windows encoding, BUT additionally replace:
160      *                             "&"         ->  "&amp;" 
161      *                             hieroglyph  ->  &#XXXX; entity
162      */
163     function setEncoding($enc)
164     {
165         // Parse an encoding.
166         preg_match('/^(\S*)(?:\s+(\S*))$/', $enc, $p);
167         $this->SCRIPT_ENCODING    = strtolower(!empty($p[1])? $p[1] : $enc);
168         $this->SCRIPT_DECODE_MODE = !empty($p[2])? $p[2] : '';
169         // Manually parse QUERY_STRING because of damned Unicode's %uXXXX.
170         $this->_correctSuperglobals();
171     }
172
173     
174     /**
175      * string quoteInput(string $input)
176      * 
177      * Quote a string according to the input decoding mode.
178      * If entities are used (see setEncoding()), no '&' character is quoted,
179      * only '"', '>' and '<' (we presume that '&' is already quoted by
180      * an input reader function).
181      *
182      * Use this function INSTEAD of htmlspecialchars() for $_GET data 
183      * in your scripts.
184      */
185     function quoteInput($s)
186     {
187         if ($this->SCRIPT_DECODE_MODE == 'entities')
188             return str_replace(array('"', '<', '>'), array('&quot;', '&lt;', '&gt;'), $s);
189         else
190             return html_specials_encode($s);
191     }
192     
193
194     /**
195      * Convert a PHP scalar, array or hash to JS scalar/array/hash. This function is 
196      * an analog of json_encode(), but it can work with a non-UTF8 input and does not 
197      * analyze the passed data. Output format must be fully JSON compatible.
198      * 
199      * @param mixed $a   Any structure to convert to JS.
200      * @return string    JavaScript equivalent structure.
201      */
202     function php2js($a=false)
203     {
204         if (is_null($a)) return 'null';
205         if ($a === false) return 'false';
206         if ($a === true) return 'true';
207         if (is_scalar($a)) {
208             if (is_float($a)) {
209                 // Always use "." for floats.
210                 $a = str_replace(",", ".", strval($a));
211             }
212             // All scalars are converted to strings to avoid indeterminism.
213             // PHP's "1" and 1 are equal for all PHP operators, but 
214             // JS's "1" and 1 are not. So if we pass "1" or 1 from the PHP backend,
215             // we should get the same result in the JS frontend (string).
216             // Character replacements for JSON.
217             static $jsonReplaces = array(
218                 array("\\", "/", "\n", "\t", "\r", "\b", "\f", '"'),
219                 array('\\\\', '\\/', '\\n', '\\t', '\\r', '\\b', '\\f', '\"')
220             );
221             return '"' . str_replace($jsonReplaces[0], $jsonReplaces[1], $a) . '"';
222         }
223         $isList = true;
224         for ($i = 0, reset($a); $i < count($a); $i++, next($a)) {
225             if (key($a) !== $i) { 
226                 $isList = false; 
227                 break; 
228             }
229         }
230         $result = array();
231         if ($isList) {
232             foreach ($a as $v) {
233                 $result[] = $this->php2js($v);
234             }
235             return '[ ' . join(', ', $result) . ' ]';
236         } else {
237             foreach ($a as $k => $v) {
238                 $result[] = $this->php2js($k) . ': ' . $this->php2js($v);
239             }
240             return '{ ' . join(', ', $result) . ' }';
241         }
242     }
243     
244         
245     /**
246      * Internal methods.
247      */
248
249     /**
250      * Parse & decode QUERY_STRING.
251      */
252     function _correctSuperglobals()
253     {
254         // In case of FORM loader we may go to nirvana, everything is already parsed by PHP.
255         if ($this->LOADER == 'form') return;
256         
257         // ATTENTION!!!
258         // HTTP_RAW_POST_DATA is only accessible when Content-Type of POST request
259         // is NOT default "application/x-www-form-urlencoded"!!!
260         // Library frontend sets "application/octet-stream" for that purpose,
261         // see JavaScript code. In PHP 5.2.2.HTTP_RAW_POST_DATA is not set sometimes; 
262         // in such cases - read the POST data manually from the STDIN stream.
263         $rawPost = strcasecmp($_SERVER['REQUEST_METHOD'], 'POST') == 0? (isset($GLOBALS['HTTP_RAW_POST_DATA'])? $GLOBALS['HTTP_RAW_POST_DATA'] : @file_get_contents("php://input")) : null;
264         $source = array(
265             '_GET' => !empty($_SERVER['QUERY_STRING'])? $_SERVER['QUERY_STRING'] : null, 
266             '_POST'=> $rawPost,
267         );
268         foreach ($source as $dst=>$src) {
269             // First correct all 2-byte entities.
270             $s = preg_replace('/%(?!5B)(?!5D)([0-9a-f]{2})/si', '%u00\\1', $src);
271             // Now we can use standard parse_str() with no worry!
272             $data = null;
273             parse_str($s, $data);
274             $GLOBALS[$dst] = $this->_ucs2EntitiesDecode($data);
275         }
276         $GLOBALS['HTTP_GET_VARS'] = $_GET; // deprecated vars
277         $GLOBALS['HTTP_POST_VARS'] = $_POST;
278         $_REQUEST = 
279             (isset($_COOKIE)? $_COOKIE : array()) + 
280             (isset($_POST)? $_POST : array()) + 
281             (isset($_GET)? $_GET : array());
282         if (ini_get('register_globals')) {
283             // TODO?
284         }
285     }
286
287
288     /**
289      * Called in case of error too!
290      */
291     function _obHandler($text)
292     {
293         unset($this->_emergBuffer); // free a piece of memory for memory_limit error
294         unset($GLOBALS['JsHttpRequest_Active']);
295         
296         // Check for error & fetch a resulting data.
297         $wasFatalError = false;
298         if (preg_match_all("/{$this->_uniqHash}(.*?){$this->_uniqHash}/sx", $text, $m)) {
299             // Display_errors:
300             // 1. disabled manually after the library initialization, or
301             // 2. was initially disabled and is not changed
302             $needRemoveErrorMessages = !ini_get('display_errors') || (!$this->_prevDisplayErrors && ini_get('display_errors') == $this->_magic);
303             foreach ($m[0] as $error) {
304                 if (preg_match('/\bFatal error(<.*?>)?:/i', $error)) {
305                     $wasFatalError = true;
306                 }
307                 if ($needRemoveErrorMessages) {
308                     $text = str_replace($error, '', $text); // strip the whole error message
309                 } else {
310                     $text = str_replace($this->_uniqHash, '', $text);
311                 }
312             }
313         }
314         if ($wasFatalError) {
315             // On fatal errors - force "null" result. This is needed, because $_RESULT
316             // may not be fully completed at the moment of the error.
317             $this->RESULT = null;
318         } else {
319             // Read the result from globals if not set directly.
320             if (!isset($this->RESULT)) {
321                 global $_RESULT;
322                 $this->RESULT = $_RESULT;
323             }
324             // Avoid manual NULLs in the result (very important!).
325             if ($this->RESULT === null) {
326                 $this->RESULT = false;
327             }
328         }
329         
330         // Note that 500 error is generated when a PHP error occurred.
331         $status = $this->RESULT === null? 500 : 200;
332         $result = array(
333             'id'   => $this->ID,
334             'js'   => $this->RESULT,  // null always means a fatal error...
335             'text' => $text,          // ...independent on $text!!!
336         );
337         $encoding = $this->SCRIPT_ENCODING;
338         $text = null; // to be on a safe side
339         
340         // Try to use very fast json_encode: 3-4 times faster than a manual encoding.
341         if (function_exists('array_walk_recursive') && function_exists('json_encode') && $this->_unicodeConvMethod) {
342             $this->_nonAsciiChars = join("", array_map('chr', range(128, 255)));
343             $this->_toUtfFailed = false;
344             $resultUtf8 = $result;
345             array_walk_recursive($resultUtf8, array(&$this, '_toUtf8_callback'), $this->SCRIPT_ENCODING);
346             if (!$this->_toUtfFailed) {
347                 // If some key contains non-ASCII character, convert everything manually.
348                 $text = json_encode($resultUtf8);
349                 $encoding = "UTF-8";
350             }
351         }
352         
353         // On failure, use manual encoding.
354         if ($text === null) {
355             $text = $this->php2js($result);
356         }
357
358         if ($this->LOADER != "xml") {
359             // In non-XML mode we cannot use plain JSON. So - wrap with JS function call.
360             // If top.JsHttpRequestGlobal is not defined, loading is aborted and 
361             // iframe is removed, so - do not call dataReady().
362             $text = "" 
363                 . ($this->LOADER == "form"? 'top && top.JsHttpRequestGlobal && top.JsHttpRequestGlobal' : 'JsHttpRequest') 
364                 . ".dataReady(" . $text . ")\n"
365                 . "";
366             if ($this->LOADER == "form") {
367                 $text = '<script type="text/javascript" language="JavaScript"><!--' . "\n$text" . '//--></script>';
368             }
369             
370             // Always return 200 code in non-XML mode (else SCRIPT does not work in FF).
371             // For XML mode, 500 code is okay.
372             $status = 200;
373         }
374
375         // Status header. To be safe, display it only in error mode. In case of success 
376         // termination, do not modify the status (""HTTP/1.1 ..." header seems to be not
377         // too cross-platform).
378         if ($this->RESULT === null) {
379             if (php_sapi_name() == "cgi") {
380                 header("Status: $status");
381             } else {
382                 header("HTTP/1.1 $status");
383             }
384         }
385
386         // In XMLHttpRequest mode we must return text/plain - damned stupid Opera 8.0. :(
387         $ctype = !empty($this->_contentTypes[$this->LOADER])? $this->_contentTypes[$this->LOADER] : $this->_contentTypes[''];
388         header("Content-type: $ctype; charset=$encoding");
389
390         return $text;
391     }
392
393
394     /**
395      * Internal function, used in array_walk_recursive() before json_encode() call.
396      * If a key contains non-ASCII characters, this function sets $this->_toUtfFailed = true,
397      * becaues array_walk_recursive() cannot modify array keys.
398      */
399     function _toUtf8_callback(&$v, $k, $fromEnc)
400     {
401         if ($v === null || is_bool($v)) return;
402         if ($this->_toUtfFailed || !is_scalar($v) || strpbrk($k, $this->_nonAsciiChars) !== false) {
403             $this->_toUtfFailed = true;
404         } else {
405             $v = $this->_unicodeConv($fromEnc, 'UTF-8', $v);
406         }
407     }
408     
409
410     /**
411      * Decode all %uXXXX entities in string or array (recurrent).
412      * String must not contain %XX entities - they are ignored!
413      */
414     function _ucs2EntitiesDecode($data)
415     {
416         if (is_array($data)) {
417             $d = array();
418             foreach ($data as $k=>$v) {
419                 $d[$this->_ucs2EntitiesDecode($k)] = $this->_ucs2EntitiesDecode($v);
420             }
421             return $d;
422         } else {
423             if (strpos($data, '%u') !== false) { // improve speed
424                 $data = preg_replace_callback('/%u([0-9A-F]{1,4})/si', array(&$this, '_ucs2EntitiesDecodeCallback'), $data);
425             }
426             return $data;
427         }
428     }
429
430
431     /**
432      * Decode one %uXXXX entity (RE callback).
433      */
434     function _ucs2EntitiesDecodeCallback($p)
435     {
436         $hex = $p[1];
437         $dec = hexdec($hex);
438         if ($dec === "38" && $this->SCRIPT_DECODE_MODE == 'entities') {
439             // Process "&" separately in "entities" decode mode.
440             $c = "&amp;";
441         } else {
442             if ($this->_unicodeConvMethod) {
443                 $c = @$this->_unicodeConv('UCS-2BE', $this->SCRIPT_ENCODING, pack('n', $dec));
444             } else {
445                 $c = $this->_decUcs2Decode($dec, $this->SCRIPT_ENCODING);
446             }
447             if (!strlen($c)) {
448                 if ($this->SCRIPT_DECODE_MODE == 'entities') {
449                     $c = '&#' . $dec . ';';
450                 } else {
451                     $c = '?';
452                 }
453             }
454         }
455         return $c;
456     }
457
458
459     /**
460      * Wrapper for iconv() or mb_convert_encoding() functions.
461      * This function will generate fatal error if none of these functons available!
462      * 
463      * @see iconv()
464      */
465     function _unicodeConv($fromEnc, $toEnc, $v)
466     {
467         if ($this->_unicodeConvMethod == 'iconv') {
468             return iconv($fromEnc, $toEnc, $v);
469         } 
470         return mb_convert_encoding($v, $toEnc, $fromEnc);
471     }
472
473
474     /**
475      * If there is no ICONV, try to decode 1-byte characters and UTF-8 manually
476      * (for most popular charsets only).
477      */
478      
479     /**
480      * Convert from UCS-2BE decimal to $toEnc.
481      */
482     function _decUcs2Decode($code, $toEnc)
483     {
484         // Little speedup by using array_flip($this->_encTables) and later hash access.
485         static $flippedTable = null;
486         if ($code < 128) return chr($code);
487         
488         if (isset($this->_encTables[$toEnc])) {
489             if (!$flippedTable) $flippedTable = array_flip($this->_encTables[$toEnc]);
490             if (isset($flippedTable[$code])) return chr(128 + $flippedTable[$code]);
491         } else if ($toEnc == 'utf-8' || $toEnc == 'utf8') {
492             // UTF-8 conversion rules: http://www.cl.cam.ac.uk/~mgk25/unicode.html
493             if ($code < 0x800) {
494                 return chr(0xC0 + ($code >> 6)) . 
495                        chr(0x80 + ($code & 0x3F));
496             } else { // if ($code <= 0xFFFF) -- it is almost always so for UCS2-BE
497                 return chr(0xE0 + ($code >> 12)) .
498                        chr(0x80 + (0x3F & ($code >> 6))) .
499                        chr(0x80 + ($code & 0x3F));
500             }
501         }
502         
503         return "";
504     }
505     
506
507     /**
508      * UCS-2BE -> 1-byte encodings (from #128).
509      */
510     var $_encTables = array(
511         'windows-1251' => array(
512             0x0402, 0x0403, 0x201A, 0x0453, 0x201E, 0x2026, 0x2020, 0x2021,
513             0x20AC, 0x2030, 0x0409, 0x2039, 0x040A, 0x040C, 0x040B, 0x040F,
514             0x0452, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014,
515             0x0098, 0x2122, 0x0459, 0x203A, 0x045A, 0x045C, 0x045B, 0x045F,
516             0x00A0, 0x040E, 0x045E, 0x0408, 0x00A4, 0x0490, 0x00A6, 0x00A7,
517             0x0401, 0x00A9, 0x0404, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x0407,
518             0x00B0, 0x00B1, 0x0406, 0x0456, 0x0491, 0x00B5, 0x00B6, 0x00B7,
519             0x0451, 0x2116, 0x0454, 0x00BB, 0x0458, 0x0405, 0x0455, 0x0457,
520             0x0410, 0x0411, 0x0412, 0x0413, 0x0414, 0x0415, 0x0416, 0x0417,
521             0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041D, 0x041E, 0x041F,
522             0x0420, 0x0421, 0x0422, 0x0423, 0x0424, 0x0425, 0x0426, 0x0427,
523             0x0428, 0x0429, 0x042A, 0x042B, 0x042C, 0x042D, 0x042E, 0x042F,
524             0x0430, 0x0431, 0x0432, 0x0433, 0x0434, 0x0435, 0x0436, 0x0437,
525             0x0438, 0x0439, 0x043A, 0x043B, 0x043C, 0x043D, 0x043E, 0x043F,
526             0x0440, 0x0441, 0x0442, 0x0443, 0x0444, 0x0445, 0x0446, 0x0447,
527             0x0448, 0x0449, 0x044A, 0x044B, 0x044C, 0x044D, 0x044E, 0x044F,
528         ),
529         'koi8-r' => array(
530             0x2500, 0x2502, 0x250C, 0x2510, 0x2514, 0x2518, 0x251C, 0x2524,
531             0x252C, 0x2534, 0x253C, 0x2580, 0x2584, 0x2588, 0x258C, 0x2590,
532             0x2591, 0x2592, 0x2593, 0x2320, 0x25A0, 0x2219, 0x221A, 0x2248,
533             0x2264, 0x2265, 0x00A0, 0x2321, 0x00B0, 0x00B2, 0x00B7, 0x00F7,
534             0x2550, 0x2551, 0x2552, 0x0451, 0x2553, 0x2554, 0x2555, 0x2556,
535             0x2557, 0x2558, 0x2559, 0x255A, 0x255B, 0x255C, 0x255d, 0x255E,
536             0x255F, 0x2560, 0x2561, 0x0401, 0x2562, 0x2563, 0x2564, 0x2565,
537             0x2566, 0x2567, 0x2568, 0x2569, 0x256A, 0x256B, 0x256C, 0x00A9,
538             0x044E, 0x0430, 0x0431, 0x0446, 0x0434, 0x0435, 0x0444, 0x0433,
539             0x0445, 0x0438, 0x0439, 0x043A, 0x043B, 0x043C, 0x043d, 0x043E,
540             0x043F, 0x044F, 0x0440, 0x0441, 0x0442, 0x0443, 0x0436, 0x0432,
541             0x044C, 0x044B, 0x0437, 0x0448, 0x044d, 0x0449, 0x0447, 0x044A,
542             0x042E, 0x0410, 0x0411, 0x0426, 0x0414, 0x0415, 0x0424, 0x0413,
543             0x0425, 0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041d, 0x041E,
544             0x041F, 0x042F, 0x0420, 0x0421, 0x0422, 0x0423, 0x0416, 0x0412,
545             0x042C, 0x042B, 0x0417, 0x0428, 0x042d, 0x0429, 0x0427, 0x042A      
546         ),
547     );
548 }