Incorrect Journal Balance (sales invoice) when many decimals in tax and price.
[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             if ($src != NULL) {
270                 // First correct all 2-byte entities.
271                 $s = preg_replace('/%(?!5B)(?!5D)([0-9a-f]{2})/si', '%u00\\1', $src);
272                 // Now we can use standard parse_str() with no worry!
273                 $data = null;
274                 parse_str($s, $data);
275                 $GLOBALS[$dst] = $this->_ucs2EntitiesDecode($data);
276             }
277         }
278         $GLOBALS['HTTP_GET_VARS'] = $_GET; // deprecated vars
279         $GLOBALS['HTTP_POST_VARS'] = $_POST;
280         $_REQUEST = 
281             (isset($_COOKIE)? $_COOKIE : array()) + 
282             (isset($_POST)? $_POST : array()) + 
283             (isset($_GET)? $_GET : array());
284         if (ini_get('register_globals')) {
285             // TODO?
286         }
287     }
288
289
290     /**
291      * Called in case of error too!
292      */
293     function _obHandler($text)
294     {
295         unset($this->_emergBuffer); // free a piece of memory for memory_limit error
296         unset($GLOBALS['JsHttpRequest_Active']);
297         
298         // Check for error & fetch a resulting data.
299         $wasFatalError = false;
300         if (preg_match_all("/{$this->_uniqHash}(.*?){$this->_uniqHash}/sx", $text, $m)) {
301             // Display_errors:
302             // 1. disabled manually after the library initialization, or
303             // 2. was initially disabled and is not changed
304             $needRemoveErrorMessages = !ini_get('display_errors') || (!$this->_prevDisplayErrors && ini_get('display_errors') == $this->_magic);
305             foreach ($m[0] as $error) {
306                 if (preg_match('/\bFatal error(<.*?>)?:/i', $error)) {
307                     $wasFatalError = true;
308                 }
309                 if ($needRemoveErrorMessages) {
310                     $text = str_replace($error, '', $text); // strip the whole error message
311                 } else {
312                     $text = str_replace($this->_uniqHash, '', $text);
313                 }
314             }
315         }
316         if ($wasFatalError) {
317             // On fatal errors - force "null" result. This is needed, because $_RESULT
318             // may not be fully completed at the moment of the error.
319             $this->RESULT = null;
320         } else {
321             // Read the result from globals if not set directly.
322             if (!isset($this->RESULT)) {
323                 global $_RESULT;
324                 $this->RESULT = $_RESULT;
325             }
326             // Avoid manual NULLs in the result (very important!).
327             if ($this->RESULT === null) {
328                 $this->RESULT = false;
329             }
330         }
331         
332         // Note that 500 error is generated when a PHP error occurred.
333         $status = $this->RESULT === null? 500 : 200;
334         $result = array(
335             'id'   => $this->ID,
336             'js'   => $this->RESULT,  // null always means a fatal error...
337             'text' => $text,          // ...independent on $text!!!
338         );
339         $encoding = $this->SCRIPT_ENCODING;
340         $text = null; // to be on a safe side
341         
342         // Try to use very fast json_encode: 3-4 times faster than a manual encoding.
343         if (function_exists('array_walk_recursive') && function_exists('json_encode') && $this->_unicodeConvMethod) {
344             $this->_nonAsciiChars = join("", array_map('chr', range(128, 255)));
345             $this->_toUtfFailed = false;
346             $resultUtf8 = $result;
347             array_walk_recursive($resultUtf8, array(&$this, '_toUtf8_callback'), $this->SCRIPT_ENCODING);
348             if (!$this->_toUtfFailed) {
349                 // If some key contains non-ASCII character, convert everything manually.
350                 $text = json_encode($resultUtf8);
351                 $encoding = "UTF-8";
352             }
353         }
354         
355         // On failure, use manual encoding.
356         if ($text === null) {
357             $text = $this->php2js($result);
358         }
359
360         if ($this->LOADER != "xml") {
361             // In non-XML mode we cannot use plain JSON. So - wrap with JS function call.
362             // If top.JsHttpRequestGlobal is not defined, loading is aborted and 
363             // iframe is removed, so - do not call dataReady().
364             $text = "" 
365                 . ($this->LOADER == "form"? 'top && top.JsHttpRequestGlobal && top.JsHttpRequestGlobal' : 'JsHttpRequest') 
366                 . ".dataReady(" . $text . ")\n"
367                 . "";
368             if ($this->LOADER == "form") {
369                 $text = '<script type="text/javascript" language="JavaScript"><!--' . "\n$text" . '//--></script>';
370             }
371             
372             // Always return 200 code in non-XML mode (else SCRIPT does not work in FF).
373             // For XML mode, 500 code is okay.
374             $status = 200;
375         }
376
377         // Status header. To be safe, display it only in error mode. In case of success 
378         // termination, do not modify the status (""HTTP/1.1 ..." header seems to be not
379         // too cross-platform).
380         if ($this->RESULT === null) {
381             if (php_sapi_name() == "cgi") {
382                 header("Status: $status");
383             } else {
384                 header("HTTP/1.1 $status");
385             }
386         }
387
388         // In XMLHttpRequest mode we must return text/plain - damned stupid Opera 8.0. :(
389         $ctype = !empty($this->_contentTypes[$this->LOADER])? $this->_contentTypes[$this->LOADER] : $this->_contentTypes[''];
390         header("Content-type: $ctype; charset=$encoding");
391
392         return $text;
393     }
394
395
396     /**
397      * Internal function, used in array_walk_recursive() before json_encode() call.
398      * If a key contains non-ASCII characters, this function sets $this->_toUtfFailed = true,
399      * becaues array_walk_recursive() cannot modify array keys.
400      */
401     function _toUtf8_callback(&$v, $k, $fromEnc)
402     {
403         if ($v === null || is_bool($v)) return;
404         if ($this->_toUtfFailed || !is_scalar($v) || strpbrk($k, $this->_nonAsciiChars) !== false) {
405             $this->_toUtfFailed = true;
406         } else {
407             $v = $this->_unicodeConv($fromEnc, 'UTF-8', $v);
408         }
409     }
410     
411
412     /**
413      * Decode all %uXXXX entities in string or array (recurrent).
414      * String must not contain %XX entities - they are ignored!
415      */
416     function _ucs2EntitiesDecode($data)
417     {
418         if (is_array($data)) {
419             $d = array();
420             foreach ($data as $k=>$v) {
421                 $d[$this->_ucs2EntitiesDecode($k)] = $this->_ucs2EntitiesDecode($v);
422             }
423             return $d;
424         } else {
425             if (strpos($data, '%u') !== false) { // improve speed
426                 $data = preg_replace_callback('/%u([0-9A-F]{1,4})/si', array(&$this, '_ucs2EntitiesDecodeCallback'), $data);
427             }
428             return $data;
429         }
430     }
431
432
433     /**
434      * Decode one %uXXXX entity (RE callback).
435      */
436     function _ucs2EntitiesDecodeCallback($p)
437     {
438         $hex = $p[1];
439         $dec = hexdec($hex);
440         if ($dec === "38" && $this->SCRIPT_DECODE_MODE == 'entities') {
441             // Process "&" separately in "entities" decode mode.
442             $c = "&amp;";
443         } else {
444             if ($this->_unicodeConvMethod) {
445                 $c = @$this->_unicodeConv('UCS-2BE', $this->SCRIPT_ENCODING, pack('n', $dec));
446             } else {
447                 $c = $this->_decUcs2Decode($dec, $this->SCRIPT_ENCODING);
448             }
449             if (!strlen($c)) {
450                 if ($this->SCRIPT_DECODE_MODE == 'entities') {
451                     $c = '&#' . $dec . ';';
452                 } else {
453                     $c = '?';
454                 }
455             }
456         }
457         return $c;
458     }
459
460
461     /**
462      * Wrapper for iconv() or mb_convert_encoding() functions.
463      * This function will generate fatal error if none of these functons available!
464      * 
465      * @see iconv()
466      */
467     function _unicodeConv($fromEnc, $toEnc, $v)
468     {
469         if ($this->_unicodeConvMethod == 'iconv') {
470             return iconv($fromEnc, $toEnc, $v);
471         } 
472         return mb_convert_encoding($v, $toEnc, $fromEnc);
473     }
474
475
476     /**
477      * If there is no ICONV, try to decode 1-byte characters and UTF-8 manually
478      * (for most popular charsets only).
479      */
480      
481     /**
482      * Convert from UCS-2BE decimal to $toEnc.
483      */
484     function _decUcs2Decode($code, $toEnc)
485     {
486         // Little speedup by using array_flip($this->_encTables) and later hash access.
487         static $flippedTable = null;
488         if ($code < 128) return chr($code);
489         
490         if (isset($this->_encTables[$toEnc])) {
491             if (!$flippedTable) $flippedTable = array_flip($this->_encTables[$toEnc]);
492             if (isset($flippedTable[$code])) return chr(128 + $flippedTable[$code]);
493         } else if ($toEnc == 'utf-8' || $toEnc == 'utf8') {
494             // UTF-8 conversion rules: http://www.cl.cam.ac.uk/~mgk25/unicode.html
495             if ($code < 0x800) {
496                 return chr(0xC0 + ($code >> 6)) . 
497                        chr(0x80 + ($code & 0x3F));
498             } else { // if ($code <= 0xFFFF) -- it is almost always so for UCS2-BE
499                 return chr(0xE0 + ($code >> 12)) .
500                        chr(0x80 + (0x3F & ($code >> 6))) .
501                        chr(0x80 + ($code & 0x3F));
502             }
503         }
504         
505         return "";
506     }
507     
508
509     /**
510      * UCS-2BE -> 1-byte encodings (from #128).
511      */
512     var $_encTables = array(
513         'windows-1251' => array(
514             0x0402, 0x0403, 0x201A, 0x0453, 0x201E, 0x2026, 0x2020, 0x2021,
515             0x20AC, 0x2030, 0x0409, 0x2039, 0x040A, 0x040C, 0x040B, 0x040F,
516             0x0452, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014,
517             0x0098, 0x2122, 0x0459, 0x203A, 0x045A, 0x045C, 0x045B, 0x045F,
518             0x00A0, 0x040E, 0x045E, 0x0408, 0x00A4, 0x0490, 0x00A6, 0x00A7,
519             0x0401, 0x00A9, 0x0404, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x0407,
520             0x00B0, 0x00B1, 0x0406, 0x0456, 0x0491, 0x00B5, 0x00B6, 0x00B7,
521             0x0451, 0x2116, 0x0454, 0x00BB, 0x0458, 0x0405, 0x0455, 0x0457,
522             0x0410, 0x0411, 0x0412, 0x0413, 0x0414, 0x0415, 0x0416, 0x0417,
523             0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041D, 0x041E, 0x041F,
524             0x0420, 0x0421, 0x0422, 0x0423, 0x0424, 0x0425, 0x0426, 0x0427,
525             0x0428, 0x0429, 0x042A, 0x042B, 0x042C, 0x042D, 0x042E, 0x042F,
526             0x0430, 0x0431, 0x0432, 0x0433, 0x0434, 0x0435, 0x0436, 0x0437,
527             0x0438, 0x0439, 0x043A, 0x043B, 0x043C, 0x043D, 0x043E, 0x043F,
528             0x0440, 0x0441, 0x0442, 0x0443, 0x0444, 0x0445, 0x0446, 0x0447,
529             0x0448, 0x0449, 0x044A, 0x044B, 0x044C, 0x044D, 0x044E, 0x044F,
530         ),
531         'koi8-r' => array(
532             0x2500, 0x2502, 0x250C, 0x2510, 0x2514, 0x2518, 0x251C, 0x2524,
533             0x252C, 0x2534, 0x253C, 0x2580, 0x2584, 0x2588, 0x258C, 0x2590,
534             0x2591, 0x2592, 0x2593, 0x2320, 0x25A0, 0x2219, 0x221A, 0x2248,
535             0x2264, 0x2265, 0x00A0, 0x2321, 0x00B0, 0x00B2, 0x00B7, 0x00F7,
536             0x2550, 0x2551, 0x2552, 0x0451, 0x2553, 0x2554, 0x2555, 0x2556,
537             0x2557, 0x2558, 0x2559, 0x255A, 0x255B, 0x255C, 0x255d, 0x255E,
538             0x255F, 0x2560, 0x2561, 0x0401, 0x2562, 0x2563, 0x2564, 0x2565,
539             0x2566, 0x2567, 0x2568, 0x2569, 0x256A, 0x256B, 0x256C, 0x00A9,
540             0x044E, 0x0430, 0x0431, 0x0446, 0x0434, 0x0435, 0x0444, 0x0433,
541             0x0445, 0x0438, 0x0439, 0x043A, 0x043B, 0x043C, 0x043d, 0x043E,
542             0x043F, 0x044F, 0x0440, 0x0441, 0x0442, 0x0443, 0x0436, 0x0432,
543             0x044C, 0x044B, 0x0437, 0x0448, 0x044d, 0x0449, 0x0447, 0x044A,
544             0x042E, 0x0410, 0x0411, 0x0426, 0x0414, 0x0415, 0x0424, 0x0413,
545             0x0425, 0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041d, 0x041E,
546             0x041F, 0x042F, 0x0420, 0x0421, 0x0422, 0x0423, 0x0416, 0x0412,
547             0x042C, 0x042B, 0x0417, 0x0428, 0x042d, 0x0429, 0x0427, 0x042A      
548         ),
549     );
550 }