source: trunk/contrib/php-xinha.php @ 1344

Last change on this file since 1344 was 1327, checked in by gogo, 7 years ago

#1595 - suhosin caused breakage of MootoolsFileManager?, typical error session expired due to suhosin having killed the session, this works around it.

  • Property svn:keywords set to LastChangedDate LastChangedRevision LastChangedBy HeadURL Id
File size: 15.5 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   *  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?>
Note: See TracBrowser for help on using the repository browser.