3 * JsHttpRequest: PHP backend for JavaScript DHTML loader.
4 * (C) Dmitry Koterov, http://en.dklab.ru
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
12 * Do not remove this comment if you want to use the script!
13 * Íå óäàëÿéòå äàííûé êîììåíòàðèé, åñëè âû õîòèòå èñïîëüçîâàòü ñêðèïò!
15 * This backend library also supports POST requests additionally to GET.
17 * @author Dmitry Koterov
23 var $SCRIPT_ENCODING = "windows-1251";
24 var $SCRIPT_DECODE_MODE = '';
29 // Internal; uniq value.
31 // Magic number for display_error checking.
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
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;
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.
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").
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.
66 function __construct($enc)
68 global $JsHttpRequest_Active;
70 // To be on a safe side - do not allow to drop reference counter on ob processing.
71 $GLOBALS['_RESULT'] =& $this->RESULT;
73 // Parse QUERY_STRING.
74 if (array_key_exists('QUERY_STRING', $_SERVER) && preg_match('/^(.*)(?:&|^)JsHttpRequest=(?:(\d+)-)?([^&]+)((?:&|$).*)$/s', @$_SERVER['QUERY_STRING'], $m)) {
76 $this->LOADER = strtolower($m[3]);
77 $_SERVER['QUERY_STRING'] = preg_replace('/^&+|&+$/s', '', preg_replace('/(^|&)'.session_name().'=[^&]*&?/s', '&', $m[1] . $m[4]));
79 $_GET['JsHttpRequest'],
80 $_REQUEST['JsHttpRequest'],
81 $_GET[session_name()],
82 $_POST[session_name()],
83 $_REQUEST[session_name()]
85 // Detect Unicode conversion method.
86 $this->_unicodeConvMethod = function_exists('mb_convert_encoding')? 'mb' : (function_exists('iconv')? 'iconv' : null);
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);
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
100 // Start OB handling early.
101 ob_start(array(&$this, "_obHandler"));
102 $JsHttpRequest_Active = true;
104 // Set up the encoding.
105 $this->setEncoding($enc);
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);
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",
123 $this->LOADER = 'unknown';
124 $JsHttpRequest_Active = false;
131 * Returns true if JsHttpRequest output processor is currently active.
133 * @return boolean True if the library is active, false otherwise.
137 return !empty($GLOBALS['JsHttpRequest_Active']);
144 * Return JavaScript part of the library.
148 return file_get_contents(dirname(__FILE__) . '/JsHttpRequest.js');
153 * void setEncoding(string $encoding)
155 * Set an active script encoding & correct QUERY_STRING according to it.
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:
161 * hieroglyph -> &#XXXX; entity
163 function setEncoding($enc)
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();
175 * string quoteInput(string $input)
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).
182 * Use this function INSTEAD of htmlspecialchars() for $_GET data
185 function quoteInput($s)
187 if ($this->SCRIPT_DECODE_MODE == 'entities')
188 return str_replace(array('"', '<', '>'), array('"', '<', '>'), $s);
190 return html_specials_encode($s);
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.
199 * @param mixed $a Any structure to convert to JS.
200 * @return string JavaScript equivalent structure.
202 function php2js($a=false)
204 if (is_null($a)) return 'null';
205 if ($a === false) return 'false';
206 if ($a === true) return 'true';
209 // Always use "." for floats.
210 $a = str_replace(",", ".", strval($a));
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', '\"')
221 return '"' . str_replace($jsonReplaces[0], $jsonReplaces[1], $a) . '"';
224 for ($i = 0, reset($a); $i < count($a); $i++, next($a)) {
225 if (key($a) !== $i) {
233 $result[] = $this->php2js($v);
235 return '[ ' . join(', ', $result) . ' ]';
237 foreach ($a as $k => $v) {
238 $result[] = $this->php2js($k) . ': ' . $this->php2js($v);
240 return '{ ' . join(', ', $result) . ' }';
250 * Parse & decode QUERY_STRING.
252 function _correctSuperglobals()
254 // In case of FORM loader we may go to nirvana, everything is already parsed by PHP.
255 if ($this->LOADER == 'form') return;
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;
265 '_GET' => !empty($_SERVER['QUERY_STRING'])? $_SERVER['QUERY_STRING'] : null,
268 foreach ($source as $dst=>$src) {
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!
274 parse_str($s, $data);
275 $GLOBALS[$dst] = $this->_ucs2EntitiesDecode($data);
278 $GLOBALS['HTTP_GET_VARS'] = $_GET; // deprecated vars
279 $GLOBALS['HTTP_POST_VARS'] = $_POST;
281 (isset($_COOKIE)? $_COOKIE : array()) +
282 (isset($_POST)? $_POST : array()) +
283 (isset($_GET)? $_GET : array());
284 if (ini_get('register_globals')) {
291 * Called in case of error too!
293 function _obHandler($text)
295 unset($this->_emergBuffer); // free a piece of memory for memory_limit error
296 unset($GLOBALS['JsHttpRequest_Active']);
298 // Check for error & fetch a resulting data.
299 $wasFatalError = false;
300 if (preg_match_all("/{$this->_uniqHash}(.*?){$this->_uniqHash}/sx", $text, $m)) {
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;
309 if ($needRemoveErrorMessages) {
310 $text = str_replace($error, '', $text); // strip the whole error message
312 $text = str_replace($this->_uniqHash, '', $text);
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;
321 // Read the result from globals if not set directly.
322 if (!isset($this->RESULT)) {
324 $this->RESULT = $_RESULT;
326 // Avoid manual NULLs in the result (very important!).
327 if ($this->RESULT === null) {
328 $this->RESULT = false;
332 // Note that 500 error is generated when a PHP error occurred.
333 $status = $this->RESULT === null? 500 : 200;
336 'js' => $this->RESULT, // null always means a fatal error...
337 'text' => $text, // ...independent on $text!!!
339 $encoding = $this->SCRIPT_ENCODING;
340 $text = null; // to be on a safe side
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);
355 // On failure, use manual encoding.
356 if ($text === null) {
357 $text = $this->php2js($result);
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().
365 . ($this->LOADER == "form"? 'top && top.JsHttpRequestGlobal && top.JsHttpRequestGlobal' : 'JsHttpRequest')
366 . ".dataReady(" . $text . ")\n"
368 if ($this->LOADER == "form") {
369 $text = '<script type="text/javascript" language="JavaScript"><!--' . "\n$text" . '//--></script>';
372 // Always return 200 code in non-XML mode (else SCRIPT does not work in FF).
373 // For XML mode, 500 code is okay.
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");
384 header("HTTP/1.1 $status");
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");
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.
401 function _toUtf8_callback(&$v, $k, $fromEnc)
403 if ($v === null || is_bool($v)) return;
404 if ($this->_toUtfFailed || !is_scalar($v) || strpbrk($k, $this->_nonAsciiChars) !== false) {
405 $this->_toUtfFailed = true;
407 $v = $this->_unicodeConv($fromEnc, 'UTF-8', $v);
413 * Decode all %uXXXX entities in string or array (recurrent).
414 * String must not contain %XX entities - they are ignored!
416 function _ucs2EntitiesDecode($data)
418 if (is_array($data)) {
420 foreach ($data as $k=>$v) {
421 $d[$this->_ucs2EntitiesDecode($k)] = $this->_ucs2EntitiesDecode($v);
425 if (strpos($data, '%u') !== false) { // improve speed
426 $data = preg_replace_callback('/%u([0-9A-F]{1,4})/si', array(&$this, '_ucs2EntitiesDecodeCallback'), $data);
434 * Decode one %uXXXX entity (RE callback).
436 function _ucs2EntitiesDecodeCallback($p)
440 if ($dec === "38" && $this->SCRIPT_DECODE_MODE == 'entities') {
441 // Process "&" separately in "entities" decode mode.
444 if ($this->_unicodeConvMethod) {
445 $c = @$this->_unicodeConv('UCS-2BE', $this->SCRIPT_ENCODING, pack('n', $dec));
447 $c = $this->_decUcs2Decode($dec, $this->SCRIPT_ENCODING);
450 if ($this->SCRIPT_DECODE_MODE == 'entities') {
451 $c = '&#' . $dec . ';';
462 * Wrapper for iconv() or mb_convert_encoding() functions.
463 * This function will generate fatal error if none of these functons available!
467 function _unicodeConv($fromEnc, $toEnc, $v)
469 if ($this->_unicodeConvMethod == 'iconv') {
470 return iconv($fromEnc, $toEnc, $v);
472 return mb_convert_encoding($v, $toEnc, $fromEnc);
477 * If there is no ICONV, try to decode 1-byte characters and UTF-8 manually
478 * (for most popular charsets only).
482 * Convert from UCS-2BE decimal to $toEnc.
484 function _decUcs2Decode($code, $toEnc)
486 // Little speedup by using array_flip($this->_encTables) and later hash access.
487 static $flippedTable = null;
488 if ($code < 128) return chr($code);
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
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));
510 * UCS-2BE -> 1-byte encodings (from #128).
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,
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