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 21class JsHttpRequest 22{ 23 var $SCRIPT_ENCODING = "windows-1251"; 24 var $SCRIPT_DECODE_MODE = ''; 25 var $LOADER = null; 26 var $ID = null; 27 28 // Internal; uniq value. 29 var $_uniqHash; 30 // Internal: response content-type depending on loader type. 31 var $_contentTypes = array( 32 "script" => "text/javascript", 33 "xml" => "text/plain", // In XMLHttpRequest mode we must return text/plain - stupid Opera 8.0. :( 34 "form" => "text/html", 35 "" => "text/plain", // for unknown loader 36 ); 37 // Internal: conversion to UTF-8 JSON cancelled because of non-ascii key. 38 var $_toUtfFailed = false; 39 // Internal: list of characters 128...255 (for strpbrk() ASCII check). 40 var $_nonAsciiChars = ''; 41 42 43 /** 44 * Constructor. 45 * 46 * Create new JsHttpRequest backend object and attach it 47 * to script output buffer. As a result - script will always return 48 * correct JavaScript code, even in case of fatal errors. 49 */ 50 function JsHttpRequest($enc) 51 { 52 // QUERY_STRING is in form of: PHPSESSID=<sid>&a=aaa&b=bbb&JsHttpRequest=<id>-<loader> 53 // where <id> is a request ID, <loader> is a loader name, <sid> - a session ID (if present), 54 // PHPSESSID - session parameter name (by default = "PHPSESSID"). 55 56 // Parse QUERY_STRING. 57 if (preg_match('/^(.*)(?:&|^)JsHttpRequest=(?:(\d+)-)?([^&]+)((?:&|$).*)$/s', $_SERVER['QUERY_STRING'], $m)) { 58 $this->ID = $m[2]; 59 $this->LOADER = strtolower($m[3]); 60 $_SERVER['QUERY_STRING'] = preg_replace('/^&+|&+$/s', '', preg_replace('/(^|&)'.session_name().'=[^&]*&?/s', '&', $m[1] . $m[4])); 61 unset( 62 $_GET['JsHttpRequest'], 63 $_REQUEST['JsHttpRequest'], 64 $_GET[session_name()], 65 $_POST[session_name()], 66 $_REQUEST[session_name()] 67 ); 68 } else { 69 $this->ID = 0; 70 $this->LOADER = 'unknown'; 71 } 72 73 // Start OB handling early. 74 $this->_uniqHash = md5(microtime() . getmypid()); 75 ini_set('error_prepend_string', ini_get('error_prepend_string') . $this->_uniqHash); 76 ini_set('error_append_string', ini_get('error_append_string') . $this->_uniqHash); 77 ob_start(array(&$this, "_obHandler")); 78 79 // Set up the encoding. 80 $this->setEncoding($enc); 81 82 // Check if headers are already sent (see Content-Type library usage). 83 // If true - generate a debug message and exit. 84 $file = $line = null; 85 if (headers_sent($file, $line)) { 86 trigger_error( 87 "HTTP headers are already sent" . ($line !== null? " in $file on line $line" : "") . ". " 88 . "Possibly you have an extra space (or a newline) before the first line of the script or any library. " 89 . "Please note that JsHttpRequest uses its own Content-Type header and fails if " 90 . "this header cannot be set. See header() function documentation for more details", 91 E_USER_ERROR 92 ); 93 exit(); 94 } 95 } 96 97 98 /** 99 * string getJsCode() 100 * 101 * Return JavaScript part of the library. 102 */ 103 function getJsCode() 104 { 105 return file_get_contents(dirname(__FILE__).'/JsHttpRequest.js'); 106 } 107 108 109 /** 110 * void setEncoding(string $encoding) 111 * 112 * Set an active script encoding & correct QUERY_STRING according to it. 113 * Examples: 114 * "windows-1251" - set plain encoding (non-windows characters, 115 * e.g. hieroglyphs, are totally ignored) 116 * "windows-1251 entities" - set windows encoding, BUT additionally replace: 117 * "&" -> "&" 118 * hieroglyph -> &#XXXX; entity 119 */ 120 function setEncoding($enc) 121 { 122 // Parse an encoding. 123 preg_match('/^(\S*)(?:\s+(\S*))$/', $enc, $p); 124 $this->SCRIPT_ENCODING = strtolower(!empty($p[1])? $p[1] : $enc); 125 $this->SCRIPT_DECODE_MODE = !empty($p[2])? $p[2] : ''; 126 // Manually parse QUERY_STRING because of damned Unicode's %uXXXX. 127 $this->_correctSuperglobals(); 128 } 129 130 131 /** 132 * string quoteInput(string $input) 133 * 134 * Quote a string according to the input decoding mode. 135 * If entities are used (see setEncoding()), no '&' character is quoted, 136 * only '"', '>' and '<' (we presume that '&' is already quoted by 137 * an input reader function). 138 * 139 * Use this function INSTEAD of htmlspecialchars() for $_GET data 140 * in your scripts. 141 */ 142 function quoteInput($s) 143 { 144 if ($this->SCRIPT_DECODE_MODE == 'entities') 145 return str_replace(array('"', '<', '>'), array('"', '<', '>'), $s); 146 else 147 return htmlspecialchars($s); 148 } 149 150 151 /** 152 * Convert a PHP scalar, array or hash to JS scalar/array/hash. This function is 153 * an analog of json_encode(), but it can work with a non-UTF8 input and does not 154 * analyze the passed data. Output format must be fully JSON compatible. 155 * 156 * @param mixed $a Any structure to convert to JS. 157 * @return string JavaScript equivalent structure. 158 */ 159 function php2js($a=false) 160 { 161 if (is_null($a)) return 'null'; 162 if ($a === false) return 'false'; 163 if ($a === true) return 'true'; 164 if (is_scalar($a)) { 165 if (is_float($a)) { 166 // Always use "." for floats. 167 $a = str_replace(",", ".", strval($a)); 168 } 169 // All scalars are converted to strings to avoid indeterminism. 170 // PHP's "1" and 1 are equal for all PHP operators, but 171 // JS's "1" and 1 are not. So if we pass "1" or 1 from the PHP backend, 172 // we should get the same result in the JS frontend (string). 173 // Character replacements for JSON. 174 static $jsonReplaces = array( 175 array("\\", "/", "\n", "\t", "\r", "\b", "\f", '"'), 176 array('\\\\', '\\/', '\\n', '\\t', '\\r', '\\b', '\\f', '\"') 177 ); 178 return '"' . str_replace($jsonReplaces[0], $jsonReplaces[1], $a) . '"'; 179 } 180 $isList = true; 181 for ($i = 0, reset($a); $i < count($a); $i++, next($a)) { 182 if (key($a) !== $i) { 183 $isList = false; 184 break; 185 } 186 } 187 $result = array(); 188 if ($isList) { 189 foreach ($a as $v) { 190 $result[] = JsHttpRequest::php2js($v); 191 } 192 return '[ ' . join(', ', $result) . ' ]'; 193 } else { 194 foreach ($a as $k => $v) { 195 $result[] = JsHttpRequest::php2js($k) . ': ' . JsHttpRequest::php2js($v); 196 } 197 return '{ ' . join(', ', $result) . ' }'; 198 } 199 } 200 201 202 203 /** 204 * Internal methods. 205 */ 206 207 /** 208 * Parse & decode QUERY_STRING. 209 */ 210 function _correctSuperglobals() 211 { 212 // In case of FORM loader we may go to nirvana, everything is already parsed by PHP. 213 if ($this->LOADER == 'form') return; 214 215 // ATTENTION!!! 216 // HTTP_RAW_POST_DATA is only accessible when Content-Type of POST request 217 // is NOT default "application/x-www-form-urlencoded"!!! 218 // Library frontend sets "application/octet-stream" for that purpose, 219 // see JavaScript code. 220 $source = array( 221 '_GET' => !empty($_SERVER['QUERY_STRING'])? $_SERVER['QUERY_STRING'] : null, 222 '_POST'=> !empty($GLOBALS['HTTP_RAW_POST_DATA'])? $GLOBALS['HTTP_RAW_POST_DATA'] : null 223 ); 224 foreach ($source as $dst=>$src) { 225 // First correct all 2-byte entities. 226 $s = preg_replace('/%(?!5B)(?!5D)([0-9a-f]{2})/si', '%u00\\1', $src); 227 // Now we can use standard parse_str() with no worry! 228 $data = null; 229 parse_str($s, $data); 230 $GLOBALS[$dst] = $this->_ucs2EntitiesDecode($data); 231 } 232 $GLOBALS['HTTP_GET_VARS'] = $_GET; // deprecated vars 233 $GLOBALS['HTTP_POST_VARS'] = $_POST; 234 $_REQUEST = 235 (isset($_COOKIE)? $_COOKIE : array()) + 236 (isset($_POST)? $_POST : array()) + 237 (isset($_GET)? $_GET : array()); 238 if (ini_get('register_globals')) { 239 // TODO? 240 } 241 } 242 243 244 /** 245 * Called in case of error too! 246 */ 247 function _obHandler($text) 248 { 249 // Check for error. 250 if (preg_match('{'.$this->_uniqHash.'(.*?)'.$this->_uniqHash.'}sx', $text)) { 251 $text = str_replace($this->_uniqHash, '', $text); 252 } 253 254 // Make a resulting hash. 255 if (!isset($this->RESULT)) { 256 $this->RESULT = isset($GLOBALS['_RESULT'])? $GLOBALS['_RESULT'] : null; 257 } 258 $encoding = $this->SCRIPT_ENCODING; 259 $result = array( 260 'id' => $this->ID, 261 'js' => $this->RESULT, 262 'text' => $text, 263 ); 264 if (function_exists('array_walk_recursive') && function_exists('iconv') && function_exists('json_encode')) { 265 $encoding = "UTF-8"; 266 $this->_nonAsciiChars = join("", array_map('chr', range(128, 255))); 267 $this->_toUtfFailed = false; 268 array_walk_recursive($result, array(&$this, '_toUtf8_callback'), $this->SCRIPT_ENCODING); 269 if (!$this->_toUtfFailed) { 270 // If some key contains non-ASCII character, convert everything manually. 271 $text = json_encode($result); 272 } else { 273 $text = $this->php2js($result); 274 } 275 } else { 276 $text = $this->php2js($result); 277 } 278 279 // Content-type header. 280 // In XMLHttpRequest mode we must return text/plain - damned stupid Opera 8.0. :( 281 $ctype = !empty($this->_contentTypes[$this->LOADER])? $this->_contentTypes[$this->LOADER] : $this->_contentTypes['']; 282 header("Content-type: $ctype; charset=$encoding"); 283 284 if ($this->LOADER != "xml") { 285 // In non-XML mode we cannot use plain JSON. So - wrap with JS function call. 286 // If top.JsHttpRequestGlobal is not defined, loading is aborted and 287 // iframe is removed, so - do not call dataReady(). 288 $text = "" 289 . ($this->LOADER == "form"? 'top && top.JsHttpRequestGlobal && top.JsHttpRequestGlobal' : 'JsHttpRequest') 290 . ".dataReady(" . $text . ")\n" 291 . ""; 292 if ($this->LOADER == "form") { 293 $text = '<script type="text/javascript" language="JavaScript"><!--' . "\n$text" . '//--></script>'; 294 } 295 } 296 297 return $text; 298 } 299 300 301 /** 302 * Internal function, used in array_walk_recursive() before json_encode() call. 303 * If a key contains non-ASCII characters, this function sets $this->_toUtfFailed = true, 304 * becaues array_walk_recursive() cannot modify array keys. 305 */ 306 function _toUtf8_callback(&$v, $k, $fromEnc) 307 { 308 if ($this->_toUtfFailed || strpbrk($k, $this->_nonAsciiChars) !== false) { 309 $this->_toUtfFailed = true; 310 } else { 311 $v = iconv($fromEnc, 'UTF-8', $v); 312 } 313 } 314 315 316 /** 317 * Decode all %uXXXX entities in string or array (recurrent). 318 * String must not contain %XX entities - they are ignored! 319 */ 320 function _ucs2EntitiesDecode($data) 321 { 322 if (is_array($data)) { 323 $d = array(); 324 foreach ($data as $k=>$v) { 325 $d[$this->_ucs2EntitiesDecode($k)] = $this->_ucs2EntitiesDecode($v); 326 } 327 return $d; 328 } else { 329 if (strpos($data, '%u') !== false) { // improve speed 330 $data = preg_replace_callback('/%u([0-9A-F]{1,4})/si', array(&$this, '_ucs2EntitiesDecodeCallback'), $data); 331 } 332 return $data; 333 } 334 } 335 336 337 /** 338 * Decode one %uXXXX entity (RE callback). 339 */ 340 function _ucs2EntitiesDecodeCallback($p) 341 { 342 $hex = $p[1]; 343 $dec = hexdec($hex); 344 if ($dec === "38" && $this->SCRIPT_DECODE_MODE == 'entities') { 345 // Process "&" separately in "entities" decode mode. 346 $c = "&"; 347 } else { 348 if (is_callable('iconv')) { 349 $c = @iconv('UCS-2BE', $this->SCRIPT_ENCODING, pack('n', $dec)); 350 } else { 351 $c = $this->_decUcs2Decode($dec, $this->SCRIPT_ENCODING); 352 } 353 if (!strlen($c)) { 354 if ($this->SCRIPT_DECODE_MODE == 'entities') { 355 $c = '&#'.$dec.';'; 356 } else { 357 $c = '?'; 358 } 359 } 360 } 361 return $c; 362 } 363 364 365 /** 366 * If there is no ICONV, try to decode 1-byte characters manually 367 * (for most popular charsets only). 368 */ 369 370 /** 371 * Convert from UCS-2BE decimal to $toEnc. 372 */ 373 function _decUcs2Decode($code, $toEnc) 374 { 375 if ($code < 128) return chr($code); 376 if (isset($this->_encTables[$toEnc])) { 377 // TODO: possible speedup by using array_flip($this->_encTables) and later hash access in the constructor. 378 $p = array_search($code, $this->_encTables[$toEnc]); 379 if ($p !== false) return chr(128 + $p); 380 } 381 return ""; 382 } 383 384 385 /** 386 * UCS-2BE -> 1-byte encodings (from #128). 387 */ 388 var $_encTables = array( 389 'windows-1251' => array( 390 0x0402, 0x0403, 0x201A, 0x0453, 0x201E, 0x2026, 0x2020, 0x2021, 391 0x20AC, 0x2030, 0x0409, 0x2039, 0x040A, 0x040C, 0x040B, 0x040F, 392 0x0452, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 393 0x0098, 0x2122, 0x0459, 0x203A, 0x045A, 0x045C, 0x045B, 0x045F, 394 0x00A0, 0x040E, 0x045E, 0x0408, 0x00A4, 0x0490, 0x00A6, 0x00A7, 395 0x0401, 0x00A9, 0x0404, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x0407, 396 0x00B0, 0x00B1, 0x0406, 0x0456, 0x0491, 0x00B5, 0x00B6, 0x00B7, 397 0x0451, 0x2116, 0x0454, 0x00BB, 0x0458, 0x0405, 0x0455, 0x0457, 398 0x0410, 0x0411, 0x0412, 0x0413, 0x0414, 0x0415, 0x0416, 0x0417, 399 0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041D, 0x041E, 0x041F, 400 0x0420, 0x0421, 0x0422, 0x0423, 0x0424, 0x0425, 0x0426, 0x0427, 401 0x0428, 0x0429, 0x042A, 0x042B, 0x042C, 0x042D, 0x042E, 0x042F, 402 0x0430, 0x0431, 0x0432, 0x0433, 0x0434, 0x0435, 0x0436, 0x0437, 403 0x0438, 0x0439, 0x043A, 0x043B, 0x043C, 0x043D, 0x043E, 0x043F, 404 0x0440, 0x0441, 0x0442, 0x0443, 0x0444, 0x0445, 0x0446, 0x0447, 405 0x0448, 0x0449, 0x044A, 0x044B, 0x044C, 0x044D, 0x044E, 0x044F, 406 ), 407 'koi8-r' => array( 408 0x2500, 0x2502, 0x250C, 0x2510, 0x2514, 0x2518, 0x251C, 0x2524, 409 0x252C, 0x2534, 0x253C, 0x2580, 0x2584, 0x2588, 0x258C, 0x2590, 410 0x2591, 0x2592, 0x2593, 0x2320, 0x25A0, 0x2219, 0x221A, 0x2248, 411 0x2264, 0x2265, 0x00A0, 0x2321, 0x00B0, 0x00B2, 0x00B7, 0x00F7, 412 0x2550, 0x2551, 0x2552, 0x0451, 0x2553, 0x2554, 0x2555, 0x2556, 413 0x2557, 0x2558, 0x2559, 0x255A, 0x255B, 0x255C, 0x255d, 0x255E, 414 0x255F, 0x2560, 0x2561, 0x0401, 0x2562, 0x2563, 0x2564, 0x2565, 415 0x2566, 0x2567, 0x2568, 0x2569, 0x256A, 0x256B, 0x256C, 0x00A9, 416 0x044E, 0x0430, 0x0431, 0x0446, 0x0434, 0x0435, 0x0444, 0x0433, 417 0x0445, 0x0438, 0x0439, 0x043A, 0x043B, 0x043C, 0x043d, 0x043E, 418 0x043F, 0x044F, 0x0440, 0x0441, 0x0442, 0x0443, 0x0436, 0x0432, 419 0x044C, 0x044B, 0x0437, 0x0448, 0x044d, 0x0449, 0x0447, 0x044A, 420 0x042E, 0x0410, 0x0411, 0x0426, 0x0414, 0x0415, 0x0424, 0x0413, 421 0x0425, 0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041d, 0x041E, 422 0x041F, 0x042F, 0x0420, 0x0421, 0x0422, 0x0423, 0x0416, 0x0412, 423 0x042C, 0x042B, 0x0417, 0x0428, 0x042d, 0x0429, 0x0427, 0x042A 424 ), 425 ); 426} 427?>