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 JsHttpRequest($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 (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 htmlspecialchars($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) {
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!
273 parse_str($s, $data);
274 $GLOBALS[$dst] = $this->_ucs2EntitiesDecode($data);
276 $GLOBALS['HTTP_GET_VARS'] = $_GET; // deprecated vars
277 $GLOBALS['HTTP_POST_VARS'] = $_POST;
279 (isset($_COOKIE)? $_COOKIE : array()) +
280 (isset($_POST)? $_POST : array()) +
281 (isset($_GET)? $_GET : array());
282 if (ini_get('register_globals')) {
289 * Called in case of error too!
291 function _obHandler($text)
293 unset($this->_emergBuffer); // free a piece of memory for memory_limit error
294 unset($GLOBALS['JsHttpRequest_Active']);
296 // Check for error & fetch a resulting data.
297 $wasFatalError = false;
298 if (preg_match_all("/{$this->_uniqHash}(.*?){$this->_uniqHash}/sx", $text, $m)) {
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;
307 if ($needRemoveErrorMessages) {
308 $text = str_replace($error, '', $text); // strip the whole error message
310 $text = str_replace($this->_uniqHash, '', $text);
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;
319 // Read the result from globals if not set directly.
320 if (!isset($this->RESULT)) {
322 $this->RESULT = $_RESULT;
324 // Avoid manual NULLs in the result (very important!).
325 if ($this->RESULT === null) {
326 $this->RESULT = false;
330 // Note that 500 error is generated when a PHP error occurred.
331 $status = $this->RESULT === null? 500 : 200;
334 'js' => $this->RESULT, // null always means a fatal error...
335 'text' => $text, // ...independent on $text!!!
337 $encoding = $this->SCRIPT_ENCODING;
338 $text = null; // to be on a safe side
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);
353 // On failure, use manual encoding.
354 if ($text === null) {
355 $text = $this->php2js($result);
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().
363 . ($this->LOADER == "form"? 'top && top.JsHttpRequestGlobal && top.JsHttpRequestGlobal' : 'JsHttpRequest')
364 . ".dataReady(" . $text . ")\n"
366 if ($this->LOADER == "form") {
367 $text = '<script type="text/javascript" language="JavaScript"><!--' . "\n$text" . '//--></script>';
370 // Always return 200 code in non-XML mode (else SCRIPT does not work in FF).
371 // For XML mode, 500 code is okay.
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");
382 header("HTTP/1.1 $status");
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");
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.
399 function _toUtf8_callback(&$v, $k, $fromEnc)
401 if ($v === null || is_bool($v)) return;
402 if ($this->_toUtfFailed || !is_scalar($v) || strpbrk($k, $this->_nonAsciiChars) !== false) {
403 $this->_toUtfFailed = true;
405 $v = $this->_unicodeConv($fromEnc, 'UTF-8', $v);
411 * Decode all %uXXXX entities in string or array (recurrent).
412 * String must not contain %XX entities - they are ignored!
414 function _ucs2EntitiesDecode($data)
416 if (is_array($data)) {
418 foreach ($data as $k=>$v) {
419 $d[$this->_ucs2EntitiesDecode($k)] = $this->_ucs2EntitiesDecode($v);
423 if (strpos($data, '%u') !== false) { // improve speed
424 $data = preg_replace_callback('/%u([0-9A-F]{1,4})/si', array(&$this, '_ucs2EntitiesDecodeCallback'), $data);
432 * Decode one %uXXXX entity (RE callback).
434 function _ucs2EntitiesDecodeCallback($p)
438 if ($dec === "38" && $this->SCRIPT_DECODE_MODE == 'entities') {
439 // Process "&" separately in "entities" decode mode.
442 if ($this->_unicodeConvMethod) {
443 $c = @$this->_unicodeConv('UCS-2BE', $this->SCRIPT_ENCODING, pack('n', $dec));
445 $c = $this->_decUcs2Decode($dec, $this->SCRIPT_ENCODING);
448 if ($this->SCRIPT_DECODE_MODE == 'entities') {
449 $c = '&#' . $dec . ';';
460 * Wrapper for iconv() or mb_convert_encoding() functions.
461 * This function will generate fatal error if none of these functons available!
465 function _unicodeConv($fromEnc, $toEnc, $v)
467 if ($this->_unicodeConvMethod == 'iconv') {
468 return iconv($fromEnc, $toEnc, $v);
470 return mb_convert_encoding($v, $toEnc, $fromEnc);
475 * If there is no ICONV, try to decode 1-byte characters and UTF-8 manually
476 * (for most popular charsets only).
480 * Convert from UCS-2BE decimal to $toEnc.
482 function _decUcs2Decode($code, $toEnc)
484 // Little speedup by using array_flip($this->_encTables) and later hash access.
485 static $flippedTable = null;
486 if ($code < 128) return chr($code);
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
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));
508 * UCS-2BE -> 1-byte encodings (from #128).
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,
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