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     *                             "&"         ->  "&amp;"
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('&quot;', '&lt;', '&gt;'), $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 = "&amp;";
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?>