source: trunk/contrib/php-xinha.php

Last change on this file was 1422, checked in by gogo, 18 months ago

A couple minor changes preparing for release.

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