Moving 2.0 development version to main trunk.
[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 $Id$
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 JsHttpRequest($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 (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
99             // Start OB handling early.
100             ob_start(array(&$this, "_obHandler"));
101             $JsHttpRequest_Active = true;
102     
103             // Set up the encoding.
104             $this->setEncoding($enc);
105     
106             // Check if headers are already sent (see Content-Type library usage).
107             // If true - generate a debug message and exit.
108             $file = $line = null;
109             $headersSent = version_compare(PHP_VERSION, "4.3.0") < 0? headers_sent() : headers_sent($file, $line);
110             if ($headersSent) {
111                 trigger_error(
112                     "HTTP headers are already sent" . ($line !== null? " in $file on line $line" : " somewhere in the script") . ". "
113                     . "Possibly you have an extra space (or a newline) before the first line of the script or any library. "
114                     . "Please note that JsHttpRequest uses its own Content-Type header and fails if "
115                     . "this header cannot be set. See header() function documentation for more details",
116                     E_USER_ERROR
117                 );
118                 exit();
119             }
120         } else {
121             $this->ID = 0;
122             $this->LOADER = 'unknown';
123             $JsHttpRequest_Active = false;
124         }
125     }
126     
127
128     /**
129      * Static function.
130      * Returns true if JsHttpRequest output processor is currently active.
131      * 
132      * @return boolean    True if the library is active, false otherwise.
133      */
134     function isActive()
135     {
136         return !empty($GLOBALS['JsHttpRequest_Active']);
137     }
138     
139
140     /**
141      * string getJsCode()
142      * 
143      * Return JavaScript part of the library.
144      */
145     function getJsCode()
146     {
147         return file_get_contents(dirname(__FILE__) . '/JsHttpRequest.js');
148     }
149
150
151     /**
152      * void setEncoding(string $encoding)
153      * 
154      * Set an active script encoding & correct QUERY_STRING according to it.
155      * Examples:
156      *   "windows-1251"          - set plain encoding (non-windows characters, 
157      *                             e.g. hieroglyphs, are totally ignored)
158      *   "windows-1251 entities" - set windows encoding, BUT additionally replace:
159      *                             "&"         ->  "&amp;" 
160      *                             hieroglyph  ->  &#XXXX; entity
161      */
162     function setEncoding($enc)
163     {
164         // Parse an encoding.
165         preg_match('/^(\S*)(?:\s+(\S*))$/', $enc, $p);
166         $this->SCRIPT_ENCODING    = strtolower(!empty($p[1])? $p[1] : $enc);
167         $this->SCRIPT_DECODE_MODE = !empty($p[2])? $p[2] : '';
168         // Manually parse QUERY_STRING because of damned Unicode's %uXXXX.
169         $this->_correctSuperglobals();
170     }
171
172     
173     /**
174      * string quoteInput(string $input)
175      * 
176      * Quote a string according to the input decoding mode.
177      * If entities are used (see setEncoding()), no '&' character is quoted,
178      * only '"', '>' and '<' (we presume that '&' is already quoted by
179      * an input reader function).
180      *
181      * Use this function INSTEAD of htmlspecialchars() for $_GET data 
182      * in your scripts.
183      */
184     function quoteInput($s)
185     {
186         if ($this->SCRIPT_DECODE_MODE == 'entities')
187             return str_replace(array('"', '<', '>'), array('&quot;', '&lt;', '&gt;'), $s);
188         else
189             return htmlspecialchars($s);
190     }
191     
192
193     /**
194      * Convert a PHP scalar, array or hash to JS scalar/array/hash. This function is 
195      * an analog of json_encode(), but it can work with a non-UTF8 input and does not 
196      * analyze the passed data. Output format must be fully JSON compatible.
197      * 
198      * @param mixed $a   Any structure to convert to JS.
199      * @return string    JavaScript equivalent structure.
200      */
201     function php2js($a=false)
202     {
203         if (is_null($a)) return 'null';
204         if ($a === false) return 'false';
205         if ($a === true) return 'true';
206         if (is_scalar($a)) {
207             if (is_float($a)) {
208                 // Always use "." for floats.
209                 $a = str_replace(",", ".", strval($a));
210             }
211             // All scalars are converted to strings to avoid indeterminism.
212             // PHP's "1" and 1 are equal for all PHP operators, but 
213             // JS's "1" and 1 are not. So if we pass "1" or 1 from the PHP backend,
214             // we should get the same result in the JS frontend (string).
215             // Character replacements for JSON.
216             static $jsonReplaces = array(
217                 array("\\", "/", "\n", "\t", "\r", "\b", "\f", '"'),
218                 array('\\\\', '\\/', '\\n', '\\t', '\\r', '\\b', '\\f', '\"')
219             );
220             return '"' . str_replace($jsonReplaces[0], $jsonReplaces[1], $a) . '"';
221         }
222         $isList = true;
223         for ($i = 0, reset($a); $i < count($a); $i++, next($a)) {
224             if (key($a) !== $i) { 
225                 $isList = false; 
226                 break; 
227             }
228         }
229         $result = array();
230         if ($isList) {
231             foreach ($a as $v) {
232                 $result[] = JsHttpRequest::php2js($v);
233             }
234             return '[ ' . join(', ', $result) . ' ]';
235         } else {
236             foreach ($a as $k => $v) {
237                 $result[] = JsHttpRequest::php2js($k) . ': ' . JsHttpRequest::php2js($v);
238             }
239             return '{ ' . join(', ', $result) . ' }';
240         }
241     }
242     
243         
244     /**
245      * Internal methods.
246      */
247
248     /**
249      * Parse & decode QUERY_STRING.
250      */
251     function _correctSuperglobals()
252     {
253         // In case of FORM loader we may go to nirvana, everything is already parsed by PHP.
254         if ($this->LOADER == 'form') return;
255         
256         // ATTENTION!!!
257         // HTTP_RAW_POST_DATA is only accessible when Content-Type of POST request
258         // is NOT default "application/x-www-form-urlencoded"!!!
259         // Library frontend sets "application/octet-stream" for that purpose,
260         // see JavaScript code. In PHP 5.2.2.HTTP_RAW_POST_DATA is not set sometimes; 
261         // in such cases - read the POST data manually from the STDIN stream.
262         $rawPost = strcasecmp($_SERVER['REQUEST_METHOD'], 'POST') == 0? (isset($GLOBALS['HTTP_RAW_POST_DATA'])? $GLOBALS['HTTP_RAW_POST_DATA'] : @file_get_contents("php://input")) : null;
263         $source = array(
264             '_GET' => !empty($_SERVER['QUERY_STRING'])? $_SERVER['QUERY_STRING'] : null, 
265             '_POST'=> $rawPost,
266         );
267         foreach ($source as $dst=>$src) {
268             // First correct all 2-byte entities.
269             $s = preg_replace('/%(?!5B)(?!5D)([0-9a-f]{2})/si', '%u00\\1', $src);
270             // Now we can use standard parse_str() with no worry!
271             $data = null;
272             parse_str($s, $data);
273             $GLOBALS[$dst] = $this->_ucs2EntitiesDecode($data);
274         }
275         $GLOBALS['HTTP_GET_VARS'] = $_GET; // deprecated vars
276         $GLOBALS['HTTP_POST_VARS'] = $_POST;
277         $_REQUEST = 
278             (isset($_COOKIE)? $_COOKIE : array()) + 
279             (isset($_POST)? $_POST : array()) + 
280             (isset($_GET)? $_GET : array());
281         if (ini_get('register_globals')) {
282             // TODO?
283         }
284     }
285
286
287     /**
288      * Called in case of error too!
289      */
290     function _obHandler($text)
291     {
292         unset($this->_emergBuffer); // free a piece of memory for memory_limit error
293         unset($GLOBALS['JsHttpRequest_Active']);
294         
295         // Check for error & fetch a resulting data.
296         if (preg_match("/{$this->_uniqHash}(.*?){$this->_uniqHash}/sx", $text, $m)) {
297             if (!ini_get('display_errors') || (!$this->_prevDisplayErrors && ini_get('display_errors') == $this->_magic)) {
298                 // Display_errors:
299                 // 1. disabled manually after the library initialization, or
300                 // 2. was initially disabled and is not changed
301                 $text = str_replace($m[0], '', $text); // strip whole error message
302             } else {
303                 $text = str_replace($this->_uniqHash, '', $text);
304             }
305         }
306         if ($m && preg_match('/\bFatal error(<.*?>)?:/i', $m[1])) {
307             // On fatal errors - force null result (generate 500 error).
308             $this->RESULT = null;
309         } else {
310             // Make a resulting hash.
311             if (!isset($this->RESULT)) {
312                 global $_RESULT;
313                 $this->RESULT = $_RESULT;
314             }
315         }
316         
317         $result = array(
318             'id'   => $this->ID,
319             'js'   => $this->RESULT,
320             'text' => $text,
321         );
322         $text = null;
323         $encoding = $this->SCRIPT_ENCODING;
324         $status = $this->RESULT !== null? 200 : 500;
325
326         // Try to use very fast json_encode: 3-4 times faster than a manual encoding.
327         if (function_exists('array_walk_recursive') && function_exists('json_encode') && $this->_unicodeConvMethod) {
328             $this->_nonAsciiChars = join("", array_map('chr', range(128, 255)));
329             $this->_toUtfFailed = false;
330             $resultUtf8 = $result;
331             array_walk_recursive($resultUtf8, array(&$this, '_toUtf8_callback'), $this->SCRIPT_ENCODING);
332             if (!$this->_toUtfFailed) {
333                 // If some key contains non-ASCII character, convert everything manually.
334                 $text = json_encode($resultUtf8);
335                 $encoding = "UTF-8";
336             }
337         }
338         
339         // On failure, use manual encoding.
340         if ($text === null) {
341             $text = $this->php2js($result);
342         }
343
344         if ($this->LOADER != "xml") {
345             // In non-XML mode we cannot use plain JSON. So - wrap with JS function call.
346             // If top.JsHttpRequestGlobal is not defined, loading is aborted and 
347             // iframe is removed, so - do not call dataReady().
348             $text = "" 
349                 . ($this->LOADER == "form"? 'top && top.JsHttpRequestGlobal && top.JsHttpRequestGlobal' : 'JsHttpRequest') 
350                 . ".dataReady(" . $text . ")\n"
351                 . "";
352             if ($this->LOADER == "form") {
353                 $text = '<script type="text/javascript" language="JavaScript"><!--' . "\n$text" . '//--></script>';
354             }
355             
356             // Always return 200 code in non-XML mode (else SCRIPT does not work in FF).
357             // For XML mode, 500 code is okay.
358             $status = 200;
359         }
360
361         // Status header. To be safe, display it only in error mode. In case of success 
362         // termination, do not modify the status (""HTTP/1.1 ..." header seems to be not
363         // too cross-platform).
364         if ($this->RESULT === null) {
365             if (php_sapi_name() == "cgi") {
366                 header("Status: $status");
367             } else {
368                 header("HTTP/1.1 $status");
369             }
370         }
371
372         // In XMLHttpRequest mode we must return text/plain - damned stupid Opera 8.0. :(
373         $ctype = !empty($this->_contentTypes[$this->LOADER])? $this->_contentTypes[$this->LOADER] : $this->_contentTypes[''];
374         header("Content-type: $ctype; charset=$encoding");
375
376         return $text;
377     }
378
379
380     /**
381      * Internal function, used in array_walk_recursive() before json_encode() call.
382      * If a key contains non-ASCII characters, this function sets $this->_toUtfFailed = true,
383      * becaues array_walk_recursive() cannot modify array keys.
384      */
385     function _toUtf8_callback(&$v, $k, $fromEnc)
386     {
387         if ($v === null || is_bool($v)) return;
388         if ($this->_toUtfFailed || !is_scalar($v) || strpbrk($k, $this->_nonAsciiChars) !== false) {
389             $this->_toUtfFailed = true;
390         } else {
391             $v = $this->_unicodeConv($fromEnc, 'UTF-8', $v);
392         }
393     }
394     
395
396     /**
397      * Decode all %uXXXX entities in string or array (recurrent).
398      * String must not contain %XX entities - they are ignored!
399      */
400     function _ucs2EntitiesDecode($data)
401     {
402         if (is_array($data)) {
403             $d = array();
404             foreach ($data as $k=>$v) {
405                 $d[$this->_ucs2EntitiesDecode($k)] = $this->_ucs2EntitiesDecode($v);
406             }
407             return $d;
408         } else {
409             if (strpos($data, '%u') !== false) { // improve speed
410                 $data = preg_replace_callback('/%u([0-9A-F]{1,4})/si', array(&$this, '_ucs2EntitiesDecodeCallback'), $data);
411             }
412             return $data;
413         }
414     }
415
416
417     /**
418      * Decode one %uXXXX entity (RE callback).
419      */
420     function _ucs2EntitiesDecodeCallback($p)
421     {
422         $hex = $p[1];
423         $dec = hexdec($hex);
424         if ($dec === "38" && $this->SCRIPT_DECODE_MODE == 'entities') {
425             // Process "&" separately in "entities" decode mode.
426             $c = "&amp;";
427         } else {
428             if ($this->_unicodeConvMethod) {
429                 $c = @$this->_unicodeConv('UCS-2BE', $this->SCRIPT_ENCODING, pack('n', $dec));
430             } else {
431                 $c = $this->_decUcs2Decode($dec, $this->SCRIPT_ENCODING);
432             }
433             if (!strlen($c)) {
434                 if ($this->SCRIPT_DECODE_MODE == 'entities') {
435                     $c = '&#' . $dec . ';';
436                 } else {
437                     $c = '?';
438                 }
439             }
440         }
441         return $c;
442     }
443
444
445     /**
446      * Wrapper for iconv() or mb_convert_encoding() functions.
447      * This function will generate fatal error if none of these functons available!
448      * 
449      * @see iconv()
450      */
451     function _unicodeConv($fromEnc, $toEnc, $v)
452     {
453         if ($this->_unicodeConvMethod == 'iconv') {
454             return iconv($fromEnc, $toEnc, $v);
455         } 
456         return mb_convert_encoding($v, $toEnc, $fromEnc);
457     }
458
459
460     /**
461      * If there is no ICONV, try to decode 1-byte characters manually
462      * (for most popular charsets only).
463      */
464      
465     /**
466      * Convert from UCS-2BE decimal to $toEnc.
467      */
468     function _decUcs2Decode($code, $toEnc)
469     {
470         if ($code < 128) return chr($code);
471         if (isset($this->_encTables[$toEnc])) {
472             // TODO: possible speedup by using array_flip($this->_encTables) and later hash access in the constructor.
473             $p = array_search($code, $this->_encTables[$toEnc]);
474             if ($p !== false) return chr(128 + $p);
475         }
476         return "";
477     }
478     
479
480     /**
481      * UCS-2BE -> 1-byte encodings (from #128).
482      */
483     var $_encTables = array(
484         'windows-1251' => array(
485             0x0402, 0x0403, 0x201A, 0x0453, 0x201E, 0x2026, 0x2020, 0x2021,
486             0x20AC, 0x2030, 0x0409, 0x2039, 0x040A, 0x040C, 0x040B, 0x040F,
487             0x0452, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014,
488             0x0098, 0x2122, 0x0459, 0x203A, 0x045A, 0x045C, 0x045B, 0x045F,
489             0x00A0, 0x040E, 0x045E, 0x0408, 0x00A4, 0x0490, 0x00A6, 0x00A7,
490             0x0401, 0x00A9, 0x0404, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x0407,
491             0x00B0, 0x00B1, 0x0406, 0x0456, 0x0491, 0x00B5, 0x00B6, 0x00B7,
492             0x0451, 0x2116, 0x0454, 0x00BB, 0x0458, 0x0405, 0x0455, 0x0457,
493             0x0410, 0x0411, 0x0412, 0x0413, 0x0414, 0x0415, 0x0416, 0x0417,
494             0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041D, 0x041E, 0x041F,
495             0x0420, 0x0421, 0x0422, 0x0423, 0x0424, 0x0425, 0x0426, 0x0427,
496             0x0428, 0x0429, 0x042A, 0x042B, 0x042C, 0x042D, 0x042E, 0x042F,
497             0x0430, 0x0431, 0x0432, 0x0433, 0x0434, 0x0435, 0x0436, 0x0437,
498             0x0438, 0x0439, 0x043A, 0x043B, 0x043C, 0x043D, 0x043E, 0x043F,
499             0x0440, 0x0441, 0x0442, 0x0443, 0x0444, 0x0445, 0x0446, 0x0447,
500             0x0448, 0x0449, 0x044A, 0x044B, 0x044C, 0x044D, 0x044E, 0x044F,
501         ),
502         'koi8-r' => array(
503             0x2500, 0x2502, 0x250C, 0x2510, 0x2514, 0x2518, 0x251C, 0x2524,
504             0x252C, 0x2534, 0x253C, 0x2580, 0x2584, 0x2588, 0x258C, 0x2590,
505             0x2591, 0x2592, 0x2593, 0x2320, 0x25A0, 0x2219, 0x221A, 0x2248,
506             0x2264, 0x2265, 0x00A0, 0x2321, 0x00B0, 0x00B2, 0x00B7, 0x00F7,
507             0x2550, 0x2551, 0x2552, 0x0451, 0x2553, 0x2554, 0x2555, 0x2556,
508             0x2557, 0x2558, 0x2559, 0x255A, 0x255B, 0x255C, 0x255d, 0x255E,
509             0x255F, 0x2560, 0x2561, 0x0401, 0x2562, 0x2563, 0x2564, 0x2565,
510             0x2566, 0x2567, 0x2568, 0x2569, 0x256A, 0x256B, 0x256C, 0x00A9,
511             0x044E, 0x0430, 0x0431, 0x0446, 0x0434, 0x0435, 0x0444, 0x0433,
512             0x0445, 0x0438, 0x0439, 0x043A, 0x043B, 0x043C, 0x043d, 0x043E,
513             0x043F, 0x044F, 0x0440, 0x0441, 0x0442, 0x0443, 0x0436, 0x0432,
514             0x044C, 0x044B, 0x0437, 0x0448, 0x044d, 0x0449, 0x0447, 0x044A,
515             0x042E, 0x0410, 0x0411, 0x0426, 0x0414, 0x0415, 0x0424, 0x0413,
516             0x0425, 0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041d, 0x041E,
517             0x041F, 0x042F, 0x0420, 0x0421, 0x0422, 0x0423, 0x0416, 0x0412,
518             0x042C, 0x042B, 0x0417, 0x0428, 0x042d, 0x0429, 0x0427, 0x042A      
519         ),
520     );
521 }