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);
99 // Start OB handling early.
100 ob_start(array(&$this, "_obHandler"));
101 $JsHttpRequest_Active = true;
103 // Set up the encoding.
104 $this->setEncoding($enc);
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);
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",
122 $this->LOADER = 'unknown';
123 $JsHttpRequest_Active = false;
130 * Returns true if JsHttpRequest output processor is currently active.
132 * @return boolean True if the library is active, false otherwise.
136 return !empty($GLOBALS['JsHttpRequest_Active']);
143 * Return JavaScript part of the library.
147 return file_get_contents(dirname(__FILE__) . '/JsHttpRequest.js');
152 * void setEncoding(string $encoding)
154 * Set an active script encoding & correct QUERY_STRING according to it.
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:
160 * hieroglyph -> &#XXXX; entity
162 function setEncoding($enc)
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();
174 * string quoteInput(string $input)
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).
181 * Use this function INSTEAD of htmlspecialchars() for $_GET data
184 function quoteInput($s)
186 if ($this->SCRIPT_DECODE_MODE == 'entities')
187 return str_replace(array('"', '<', '>'), array('"', '<', '>'), $s);
189 return htmlspecialchars($s);
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.
198 * @param mixed $a Any structure to convert to JS.
199 * @return string JavaScript equivalent structure.
201 function php2js($a=false)
203 if (is_null($a)) return 'null';
204 if ($a === false) return 'false';
205 if ($a === true) return 'true';
208 // Always use "." for floats.
209 $a = str_replace(",", ".", strval($a));
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', '\"')
220 return '"' . str_replace($jsonReplaces[0], $jsonReplaces[1], $a) . '"';
223 for ($i = 0, reset($a); $i < count($a); $i++, next($a)) {
224 if (key($a) !== $i) {
232 $result[] = JsHttpRequest::php2js($v);
234 return '[ ' . join(', ', $result) . ' ]';
236 foreach ($a as $k => $v) {
237 $result[] = JsHttpRequest::php2js($k) . ': ' . JsHttpRequest::php2js($v);
239 return '{ ' . join(', ', $result) . ' }';
249 * Parse & decode QUERY_STRING.
251 function _correctSuperglobals()
253 // In case of FORM loader we may go to nirvana, everything is already parsed by PHP.
254 if ($this->LOADER == 'form') return;
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;
264 '_GET' => !empty($_SERVER['QUERY_STRING'])? $_SERVER['QUERY_STRING'] : null,
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!
272 parse_str($s, $data);
273 $GLOBALS[$dst] = $this->_ucs2EntitiesDecode($data);
275 $GLOBALS['HTTP_GET_VARS'] = $_GET; // deprecated vars
276 $GLOBALS['HTTP_POST_VARS'] = $_POST;
278 (isset($_COOKIE)? $_COOKIE : array()) +
279 (isset($_POST)? $_POST : array()) +
280 (isset($_GET)? $_GET : array());
281 if (ini_get('register_globals')) {
288 * Called in case of error too!
290 function _obHandler($text)
292 unset($this->_emergBuffer); // free a piece of memory for memory_limit error
293 unset($GLOBALS['JsHttpRequest_Active']);
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)) {
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
303 $text = str_replace($this->_uniqHash, '', $text);
306 if ($m && preg_match('/\bFatal error(<.*?>)?:/i', $m[1])) {
307 // On fatal errors - force null result (generate 500 error).
308 $this->RESULT = null;
310 // Make a resulting hash.
311 if (!isset($this->RESULT)) {
313 $this->RESULT = $_RESULT;
319 'js' => $this->RESULT,
323 $encoding = $this->SCRIPT_ENCODING;
324 $status = $this->RESULT !== null? 200 : 500;
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);
339 // On failure, use manual encoding.
340 if ($text === null) {
341 $text = $this->php2js($result);
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().
349 . ($this->LOADER == "form"? 'top && top.JsHttpRequestGlobal && top.JsHttpRequestGlobal' : 'JsHttpRequest')
350 . ".dataReady(" . $text . ")\n"
352 if ($this->LOADER == "form") {
353 $text = '<script type="text/javascript" language="JavaScript"><!--' . "\n$text" . '//--></script>';
356 // Always return 200 code in non-XML mode (else SCRIPT does not work in FF).
357 // For XML mode, 500 code is okay.
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");
368 header("HTTP/1.1 $status");
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");
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.
385 function _toUtf8_callback(&$v, $k, $fromEnc)
387 if ($v === null || is_bool($v)) return;
388 if ($this->_toUtfFailed || !is_scalar($v) || strpbrk($k, $this->_nonAsciiChars) !== false) {
389 $this->_toUtfFailed = true;
391 $v = $this->_unicodeConv($fromEnc, 'UTF-8', $v);
397 * Decode all %uXXXX entities in string or array (recurrent).
398 * String must not contain %XX entities - they are ignored!
400 function _ucs2EntitiesDecode($data)
402 if (is_array($data)) {
404 foreach ($data as $k=>$v) {
405 $d[$this->_ucs2EntitiesDecode($k)] = $this->_ucs2EntitiesDecode($v);
409 if (strpos($data, '%u') !== false) { // improve speed
410 $data = preg_replace_callback('/%u([0-9A-F]{1,4})/si', array(&$this, '_ucs2EntitiesDecodeCallback'), $data);
418 * Decode one %uXXXX entity (RE callback).
420 function _ucs2EntitiesDecodeCallback($p)
424 if ($dec === "38" && $this->SCRIPT_DECODE_MODE == 'entities') {
425 // Process "&" separately in "entities" decode mode.
428 if ($this->_unicodeConvMethod) {
429 $c = @$this->_unicodeConv('UCS-2BE', $this->SCRIPT_ENCODING, pack('n', $dec));
431 $c = $this->_decUcs2Decode($dec, $this->SCRIPT_ENCODING);
434 if ($this->SCRIPT_DECODE_MODE == 'entities') {
435 $c = '&#' . $dec . ';';
446 * Wrapper for iconv() or mb_convert_encoding() functions.
447 * This function will generate fatal error if none of these functons available!
451 function _unicodeConv($fromEnc, $toEnc, $v)
453 if ($this->_unicodeConvMethod == 'iconv') {
454 return iconv($fromEnc, $toEnc, $v);
456 return mb_convert_encoding($v, $toEnc, $fromEnc);
461 * If there is no ICONV, try to decode 1-byte characters manually
462 * (for most popular charsets only).
466 * Convert from UCS-2BE decimal to $toEnc.
468 function _decUcs2Decode($code, $toEnc)
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);
481 * UCS-2BE -> 1-byte encodings (from #128).
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,
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