1 | <?php |
---|
2 | /** Pass data to a Xinha plugin backend without using PHP session handling functions. |
---|
3 | * |
---|
4 | * Why? Suhosin, that's why. Suhosin has an option to encrypt sessions, which sounds |
---|
5 | * great, but is a bad idea for us, particularly when it includes the user_agent in the |
---|
6 | * encryption key, because any flash file doing a GET/POST has a different user agent to |
---|
7 | * the browser itself causing the session to not decrypt, and worse, causes the session |
---|
8 | * to reset even though it doesn't decrypt. |
---|
9 | * |
---|
10 | * As a result, this method (in combination with the above) uses a simple file-storage |
---|
11 | * mechanism to record the necessary secret data instead of the php sessions. |
---|
12 | * |
---|
13 | * Caution, if you are load balancing, then all the servers in that cluster will need |
---|
14 | * to be able to get to the tmp_path, or you tie each user to a certain server. |
---|
15 | * |
---|
16 | * @param array The data to pass |
---|
17 | * @param string The path to a temporary folder where we can create some folders and files |
---|
18 | * if not supplied, it will be sys_get_temp_dir() |
---|
19 | * @param bool If you want the data returned as a PHP structure instead of echo'd as javascript |
---|
20 | */ |
---|
21 | |
---|
22 | function xinha_pass_to_php_backend_without_php_sessions($data, $tmp_path = NULL, $ReturnPHP = FALSE) |
---|
23 | { |
---|
24 | $bk = array(); |
---|
25 | $bk['data'] = serialize($data); |
---|
26 | $key = xinha_get_backend_hash_key($bk, $tmp_path); |
---|
27 | |
---|
28 | $bk['hash'] = |
---|
29 | function_exists('sha1') ? |
---|
30 | sha1($key . $bk['data']) |
---|
31 | : md5($key . $bk['data']); |
---|
32 | |
---|
33 | |
---|
34 | // The data will be passed via a postback to the |
---|
35 | // backend, we want to make sure these are going to come |
---|
36 | // out from the PHP as an array like $bk above, so |
---|
37 | // we need to adjust the keys. |
---|
38 | $backend_data = array(); |
---|
39 | foreach($bk as $k => $v) |
---|
40 | { |
---|
41 | $backend_data["backend_data[$k]"] = $v; |
---|
42 | } |
---|
43 | |
---|
44 | if($ReturnPHP) |
---|
45 | { |
---|
46 | return array('backend_data' => $backend_data); |
---|
47 | } |
---|
48 | else |
---|
49 | { |
---|
50 | echo 'backend_data = ' . xinha_to_js($backend_data) . "; \n"; |
---|
51 | } |
---|
52 | } |
---|
53 | |
---|
54 | /** The completment of xinha_pass_to_php_backend_without_php_sessions(), read the backend |
---|
55 | * data which was passed and return it. |
---|
56 | * |
---|
57 | * @return array|NULL As was supplied as $data to xinha_pass_to_php_backend_without_php_sessions() |
---|
58 | * |
---|
59 | */ |
---|
60 | |
---|
61 | function xinha_read_passed_data_without_php_sessions() |
---|
62 | { |
---|
63 | $bk = $_REQUEST['backend_data']; |
---|
64 | $key = xinha_get_backend_hash_key($bk); |
---|
65 | $hash = function_exists('sha1') ? |
---|
66 | sha1($key . $bk['data']) |
---|
67 | : md5($key . $bk['data']); |
---|
68 | |
---|
69 | if(!strlen($hash) || ($hash !== $bk['hash'])) |
---|
70 | { |
---|
71 | throw new Exception("Security error, hash mis-match."); |
---|
72 | } |
---|
73 | |
---|
74 | return unserialize($bk['data']); |
---|
75 | } |
---|
76 | |
---|
77 | /** |
---|
78 | * For a given backend data, return a key (salt) to use when hashing. |
---|
79 | * |
---|
80 | * @param array array('data' => 'serialized data' ) |
---|
81 | * @param string Path to store temporary files which contain the keys, must be writable |
---|
82 | * defaults to sys_get_temp_dir() |
---|
83 | * |
---|
84 | * @modifies $bk will be modified to add ['tmp_path'] and ['xinha-backend-key-id'] |
---|
85 | * the former is added only if it does not exist and will be equivalent to |
---|
86 | * the supplied $tmp_path |
---|
87 | * |
---|
88 | * the latter is added only if it does not exist, and is generated as a unique |
---|
89 | * identifier of arbitrary length |
---|
90 | * |
---|
91 | * @access private |
---|
92 | */ |
---|
93 | |
---|
94 | function xinha_get_backend_hash_key(&$bk, $tmp_path = NULL) |
---|
95 | { |
---|
96 | // The key itself will be written into a file inside the "tmp_path", |
---|
97 | if(!isset($bk['tmp_path'])) |
---|
98 | { |
---|
99 | if(!isset($tmp_path)) |
---|
100 | { |
---|
101 | $tmp_path = sys_get_temp_dir(); |
---|
102 | } |
---|
103 | |
---|
104 | $bk['tmp_path'] = $tmp_path; |
---|
105 | } |
---|
106 | |
---|
107 | // The file will be formed from the "key id" |
---|
108 | if(isset($bk['xinha-backend-key-id'])) |
---|
109 | { |
---|
110 | $key_id = $bk['xinha-backend-key-id']; |
---|
111 | } |
---|
112 | elseif(isset($_COOKIE['xinha-backend-key-id'])) |
---|
113 | { |
---|
114 | $key_id = $_COOKIE['xinha-backend-key-id']; |
---|
115 | $bk['xinha-backend-key-id'] = $key_id; |
---|
116 | } |
---|
117 | else |
---|
118 | { |
---|
119 | $key_id = uniqid('xinha-', TRUE); |
---|
120 | @setcookie('xinha-backend-key-id', $key_id, 0, '/'); // Not the end of the world if this fails |
---|
121 | $bk['xinha-backend-key-id'] = $key_id; |
---|
122 | } |
---|
123 | |
---|
124 | // We don't trust the key-id itself to not be some naughty construct, so |
---|
125 | // the filename is md5'd, we chunk_split it to ensure that we don't go using |
---|
126 | // too many inodes in a single folder. We create that split path in a sub folder |
---|
127 | // of the tmp_path so that multiple Xinha installs on the same server don't trample |
---|
128 | // each other. So the final result is... |
---|
129 | // [tmp_path]/xinha-[install-path-md5-hash]/a1/a1/a1/a1/a1/a1/a1...../xinha_key |
---|
130 | $KeyFile = realpath($bk['tmp_path']) . "/xinha-".md5(__FILE__) . "/" . chunk_split(md5($key_id),2, "/") . "xinha_key"; |
---|
131 | |
---|
132 | if(!file_exists($KeyFile)) |
---|
133 | { |
---|
134 | // Without a keyfile, this could mean 2 things |
---|
135 | // 1. We have been asked to create a keyfile, in this case, $tmp_path MUST be set (by now, see default above) |
---|
136 | // 2. The keyfile has disappeared |
---|
137 | |
---|
138 | // Case 2 |
---|
139 | if(!isset($tmp_path)) |
---|
140 | { |
---|
141 | throw new Exception("Unable to locate security key, reload the page and try again."); |
---|
142 | } |
---|
143 | |
---|
144 | // Case 1, because we named the keyfile from the tmp_path in $bk, double check somebody isn't fiddling with that |
---|
145 | if($tmp_path !== $bk['tmp_path']) |
---|
146 | { |
---|
147 | throw new Exception("Attempt to write new key with invalid tmp_path"); |
---|
148 | } |
---|
149 | |
---|
150 | // If we can't write to the path, then we are no good |
---|
151 | if(!is_dir($bk['tmp_path']) || !is_writable($bk['tmp_path'])) |
---|
152 | { |
---|
153 | throw new Exception( "Xinha is unable to write to {$bk['tmp_path']} while trying to pass to backend without sessions." ); |
---|
154 | } |
---|
155 | |
---|
156 | // Finally looks to be OK, so write a new key into the keyfile |
---|
157 | mkdir(dirname($KeyFile), 0700, TRUE); |
---|
158 | file_put_contents($KeyFile, uniqid('Key_')); |
---|
159 | |
---|
160 | // Roll the dice to see if we should garbage collect |
---|
161 | if(rand(0,100) >= 90) |
---|
162 | { |
---|
163 | xinha_garbage_collect(realpath($bk['tmp_path']) . "/xinha-".md5(__FILE__)); |
---|
164 | } |
---|
165 | } |
---|
166 | else |
---|
167 | { |
---|
168 | // We have a keyfile, touch it to make sure the server knows it's been used recently |
---|
169 | touch($KeyFile); |
---|
170 | } |
---|
171 | |
---|
172 | return file_get_contents($KeyFile); |
---|
173 | } |
---|
174 | |
---|
175 | |
---|
176 | /** |
---|
177 | * Garbage collect old key files which are created by xinha_pass_to_php_backend_without_php_sessions() |
---|
178 | * |
---|
179 | * Key files which are 12 hours old or more are culled. |
---|
180 | * |
---|
181 | * This method is called randomly by xinha_get_backend_hash_key when creating a new key (10% of the time) |
---|
182 | * |
---|
183 | * @param string path to start garbage collection on |
---|
184 | * @param string the maximum number of files to check (limits time taken) |
---|
185 | * @return bool true = folder now empty, false = folder still contains data |
---|
186 | * @access private |
---|
187 | */ |
---|
188 | |
---|
189 | function xinha_garbage_collect($path, &$maxcount = 100) |
---|
190 | { |
---|
191 | $d = opendir($path); |
---|
192 | $empty = true; |
---|
193 | while($f = readdir($d)) |
---|
194 | { |
---|
195 | // If this is the key file, check it's age, unlink and return true (dir empty) if older than 12 hours |
---|
196 | // otherwise, return false (dir not empty) |
---|
197 | if($f === 'xinha_key') |
---|
198 | { |
---|
199 | $maxcount--; |
---|
200 | |
---|
201 | if(@filemtime($path . '/' . $f) < (time() - 60*60*12)) |
---|
202 | { |
---|
203 | if(@unlink($path . '/' . $f)) |
---|
204 | { |
---|
205 | return true; |
---|
206 | } |
---|
207 | } |
---|
208 | |
---|
209 | return false; |
---|
210 | } |
---|
211 | |
---|
212 | // If this is a chunk directory recurse, if the recursion return false (not empty) |
---|
213 | // or it returns true but we can't delete for some reason, then set empty to false |
---|
214 | elseif(preg_match('/^[0-9a-f]{2,2}$/', $f)) |
---|
215 | { |
---|
216 | if($maxcount <= 0) |
---|
217 | { |
---|
218 | // If we have already checked our maximum, don't do any more |
---|
219 | $empty = false; |
---|
220 | } |
---|
221 | elseif(!xinha_garbage_collect($path . '/' . $f, $maxcount) || !rmdir($f)) |
---|
222 | { |
---|
223 | $empty = false; |
---|
224 | } |
---|
225 | } |
---|
226 | } |
---|
227 | closedir($d); |
---|
228 | |
---|
229 | return $maxcount ? $empty : false; |
---|
230 | } |
---|
231 | /** Write the appropriate xinha_config directives to pass data to a PHP (Plugin) backend file. |
---|
232 | * |
---|
233 | * ImageManager Example: |
---|
234 | * The following would be placed in step 3 of your configuration (see the NewbieGuide |
---|
235 | * (http://xinha.python-hosting.com/wiki/NewbieGuide) |
---|
236 | * |
---|
237 | * <script language="javascript"> |
---|
238 | * with (xinha_config.ImageManager) |
---|
239 | * { |
---|
240 | * <?php |
---|
241 | * xinha_pass_to_php_backend |
---|
242 | * ( |
---|
243 | * array |
---|
244 | * ( |
---|
245 | * 'images_dir' => '/home/your/directory', |
---|
246 | * 'images_url' => '/directory' |
---|
247 | * ) |
---|
248 | * ) |
---|
249 | * ?> |
---|
250 | * } |
---|
251 | * </script> |
---|
252 | * |
---|
253 | */ |
---|
254 | |
---|
255 | function xinha_pass_to_php_backend($Data, $KeyLocation = 'Xinha:BackendKey', $ReturnPHP = FALSE) |
---|
256 | { |
---|
257 | // A non default KeyLocation which is an existing directory is treated as |
---|
258 | // a request to not use sessions |
---|
259 | if($KeyLocation != 'Xinha:BackendKey' && file_exists($KeyLocation) && is_dir($KeyLocation)) |
---|
260 | { |
---|
261 | return xinha_pass_to_php_backend_without_php_sessions($Data, $KeyLocation, $ReturnPHP); |
---|
262 | } |
---|
263 | |
---|
264 | // If we are using session based key passing, then make sure that suhosin isn't |
---|
265 | // going to screw things up, fall back to no-sessions version |
---|
266 | if(@ini_get('suhosin.session.cryptua')) |
---|
267 | { |
---|
268 | if($KeyLocation == 'Xinha:BackendKey') |
---|
269 | { |
---|
270 | // Really should throw up a warning here, because the file-based key storage might |
---|
271 | // not be suitable out of the box for cluster type environments |
---|
272 | return xinha_pass_to_php_backend_without_php_sessions($Data, NULL, $ReturnPHP); |
---|
273 | } |
---|
274 | else |
---|
275 | { |
---|
276 | throw new Exception("Use of the standard xinha_pass_to_php_backend() is not possible because this server uses suhosin.session.cryptua. Use xinha_pass_to_php_backend_without_php_sessions() instead, or disable suhosin.session.cryptua."); |
---|
277 | } |
---|
278 | } |
---|
279 | |
---|
280 | $bk = array(); |
---|
281 | $bk['data'] = serialize($Data); |
---|
282 | |
---|
283 | @session_start(); |
---|
284 | if(!isset($_SESSION[$KeyLocation])) |
---|
285 | { |
---|
286 | $_SESSION[$KeyLocation] = uniqid('Key_'); |
---|
287 | } |
---|
288 | |
---|
289 | $bk['session_name'] = session_name(); |
---|
290 | $bk['key_location'] = $KeyLocation; |
---|
291 | $bk['hash'] = |
---|
292 | function_exists('sha1') ? |
---|
293 | sha1($_SESSION[$KeyLocation] . $bk['data']) |
---|
294 | : md5($_SESSION[$KeyLocation] . $bk['data']); |
---|
295 | |
---|
296 | |
---|
297 | // The data will be passed via a postback to the |
---|
298 | // backend, we want to make sure these are going to come |
---|
299 | // out from the PHP as an array like $bk above, so |
---|
300 | // we need to adjust the keys. |
---|
301 | $backend_data = array(); |
---|
302 | foreach($bk as $k => $v) |
---|
303 | { |
---|
304 | $backend_data["backend_data[$k]"] = $v; |
---|
305 | } |
---|
306 | |
---|
307 | // The session_start() above may have been after data was sent, so cookies |
---|
308 | // wouldn't have worked. |
---|
309 | $backend_data[session_name()] = session_id(); |
---|
310 | |
---|
311 | if($ReturnPHP) |
---|
312 | { |
---|
313 | return array('backend_data' => $backend_data); |
---|
314 | } |
---|
315 | else |
---|
316 | { |
---|
317 | echo 'backend_data = ' . xinha_to_js($backend_data) . "; \n"; |
---|
318 | } |
---|
319 | } |
---|
320 | |
---|
321 | /** Convert PHP data structure to Javascript */ |
---|
322 | |
---|
323 | function xinha_to_js($var, $tabs = 0) |
---|
324 | { |
---|
325 | if(is_numeric($var)) |
---|
326 | { |
---|
327 | return $var; |
---|
328 | } |
---|
329 | |
---|
330 | if(is_string($var)) |
---|
331 | { |
---|
332 | return "'" . xinha_js_encode($var) . "'"; |
---|
333 | } |
---|
334 | |
---|
335 | if(is_bool($var)) |
---|
336 | { |
---|
337 | return $var ? 'true': 'false'; |
---|
338 | } |
---|
339 | |
---|
340 | if(is_array($var)) |
---|
341 | { |
---|
342 | $useObject = false; |
---|
343 | foreach(array_keys($var) as $k) { |
---|
344 | if(!is_numeric($k)) $useObject = true; |
---|
345 | } |
---|
346 | $js = array(); |
---|
347 | foreach($var as $k => $v) |
---|
348 | { |
---|
349 | $i = ""; |
---|
350 | if($useObject) { |
---|
351 | if(preg_match('#^[a-zA-Z_]+[a-zA-Z0-9_]*$#', $k)) { |
---|
352 | $i .= "$k: "; |
---|
353 | } else { |
---|
354 | $i .= "'$k': "; |
---|
355 | } |
---|
356 | } |
---|
357 | $i .= xinha_to_js($v, $tabs + 1); |
---|
358 | $js[] = $i; |
---|
359 | } |
---|
360 | if($useObject) { |
---|
361 | $ret = "{\n" . xinha_tabify(implode(",\n", $js), $tabs) . "\n}"; |
---|
362 | } else { |
---|
363 | $ret = "[\n" . xinha_tabify(implode(",\n", $js), $tabs) . "\n]"; |
---|
364 | } |
---|
365 | return $ret; |
---|
366 | } |
---|
367 | |
---|
368 | return 'null'; |
---|
369 | } |
---|
370 | |
---|
371 | /** Like htmlspecialchars() except for javascript strings. */ |
---|
372 | |
---|
373 | function xinha_js_encode($string) |
---|
374 | { |
---|
375 | static $strings = "\\,\",',%,&,<,>,{,},@,\n,\r"; |
---|
376 | |
---|
377 | if(!is_array($strings)) |
---|
378 | { |
---|
379 | $tr = array(); |
---|
380 | foreach(explode(',', $strings) as $chr) |
---|
381 | { |
---|
382 | $tr[$chr] = sprintf('\x%02X', ord($chr)); |
---|
383 | } |
---|
384 | $strings = $tr; |
---|
385 | } |
---|
386 | |
---|
387 | return strtr($string, $strings); |
---|
388 | } |
---|
389 | |
---|
390 | |
---|
391 | /** Used by plugins to get the config passed via |
---|
392 | * xinha_pass_to_backend() |
---|
393 | * returns either the structure given, or NULL |
---|
394 | * if none was passed or a security error was encountered. |
---|
395 | */ |
---|
396 | |
---|
397 | function xinha_read_passed_data($KeyLocation = 'Xinha:BackendKey') |
---|
398 | { |
---|
399 | if(isset($_REQUEST['backend_data']['xinha-backend-key-id'])) |
---|
400 | { |
---|
401 | // This is a without sessions passing, |
---|
402 | return xinha_read_passed_data_without_php_sessions(); |
---|
403 | } |
---|
404 | if(isset($_REQUEST['backend_data']) && is_array($_REQUEST['backend_data'])) |
---|
405 | { |
---|
406 | $bk = $_REQUEST['backend_data']; |
---|
407 | session_name($bk['session_name']); |
---|
408 | @session_start(); @session_write_close(); |
---|
409 | if(!isset($_SESSION[$bk['key_location']])) return NULL; |
---|
410 | |
---|
411 | if($KeyLocation !== $bk['key_location']) |
---|
412 | { |
---|
413 | trigger_error('Programming Error - please contact the website administrator/programmer to alert them to this problem. A non-default backend key location is being used to pass backend data to Xinha, but the same key location is not being used to receive data. The special backend configuration has been ignored. To resolve this, find where you are using xinha_pass_to_php_backend and remove the non default key, or find the locations where xinha_read_passed_data is used (in Xinha) and add a parameter with the non default key location, or edit contrib/php-xinha.php and change the default key location in both these functions. See: http://trac.xinha.org/ticket/1518', E_USER_ERROR); |
---|
414 | return NULL; |
---|
415 | } |
---|
416 | |
---|
417 | if($bk['hash'] === |
---|
418 | function_exists('sha1') ? |
---|
419 | sha1($_SESSION[$bk['key_location']] . $bk['data']) |
---|
420 | : md5($_SESSION[$bk['key_location']] . $bk['data'])) |
---|
421 | { |
---|
422 | return unserialize(ini_get('magic_quotes_gpc') ? stripslashes($bk['data']) : $bk['data']); |
---|
423 | } |
---|
424 | } |
---|
425 | |
---|
426 | return NULL; |
---|
427 | } |
---|
428 | |
---|
429 | /** Used by plugins to get a query string that can be sent to the backend |
---|
430 | * (or another part of the backend) to send the same data. |
---|
431 | */ |
---|
432 | |
---|
433 | function xinha_passed_data_querystring() |
---|
434 | { |
---|
435 | $qs = array(); |
---|
436 | if(isset($_REQUEST['backend_data']) && is_array($_REQUEST['backend_data'])) |
---|
437 | { |
---|
438 | foreach($_REQUEST['backend_data'] as $k => $v) |
---|
439 | { |
---|
440 | $v = ini_get('magic_quotes_gpc') ? stripslashes($v) : $v; |
---|
441 | $qs[] = "backend_data[" . rawurlencode($k) . "]=" . rawurlencode($v); |
---|
442 | } |
---|
443 | } |
---|
444 | |
---|
445 | $qs[] = session_name() . '=' . session_id(); |
---|
446 | return implode('&', $qs); |
---|
447 | } |
---|
448 | |
---|
449 | |
---|
450 | /** Just space-tab indent some text */ |
---|
451 | function xinha_tabify($text, $tabs) |
---|
452 | { |
---|
453 | if($text) |
---|
454 | { |
---|
455 | return str_repeat(" ", $tabs) . preg_replace('/\n(.)/', "\n" . str_repeat(" ", $tabs) . "\$1", $text); |
---|
456 | } |
---|
457 | } |
---|
458 | |
---|
459 | /** Return upload_max_filesize value from php.ini in kilobytes (function adapted from php.net)**/ |
---|
460 | function upload_max_filesize_kb() |
---|
461 | { |
---|
462 | $val = ini_get('upload_max_filesize'); |
---|
463 | $val = trim($val); |
---|
464 | $last = strtolower($val{strlen($val)-1}); |
---|
465 | switch($last) |
---|
466 | { |
---|
467 | // The 'G' modifier is available since PHP 5.1.0 |
---|
468 | case 'g': |
---|
469 | $val *= 1024; |
---|
470 | case 'm': |
---|
471 | $val *= 1024; |
---|
472 | } |
---|
473 | return $val; |
---|
474 | } |
---|
475 | ?> |
---|