Added support for multipart forms, file uploads, and fatal exceptions in PhpHttpd; fixed wrong license tag on WebServer class
authorDan
Tue, 01 Jul 2008 04:17:46 -0400
changeset 23 08225c2eb0b6
parent 22 3a4f0cd0794e
child 24 d275dc8f4203
Added support for multipart forms, file uploads, and fatal exceptions in PhpHttpd; fixed wrong license tag on WebServer class
webserver.php
--- a/webserver.php	Tue Jul 01 04:16:04 2008 -0400
+++ b/webserver.php	Tue Jul 01 04:17:46 2008 -0400
@@ -33,7 +33,7 @@
  * @package Amarok
  * @subpackage WebControl
  * @author Dan Fuhry
- * @license Public domain
+ * @license GNU General Public License <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>
  */
 
 class WebServer
@@ -163,7 +163,7 @@
   function __construct($address = '127.0.0.1', $port = 8080, $targetuser = null, $targetgroup = null)
   {
     @set_time_limit(0);
-    @ini_set('memory_limit', '256M');
+    @ini_set('memory_limit', '128M');
     
     // do we have socket functions?
     if ( !function_exists('socket_create') )
@@ -406,24 +406,133 @@
       // anything on POST?
       $postdata = '';
       $_POST = array();
+      $_FILES = array();
       if ( $method == 'POST' )
       {
         // read POST data
-        if ( isset($_SERVER['HTTP_CONTENT_LENGTH']) )
+        if ( isset($_SERVER['HTTP_CONTENT_TYPE']) && preg_match('#^multipart/form-data; ?boundary=([A-z0-9_-]+)$#i', $_SERVER['HTTP_CONTENT_TYPE'], $match) )
         {
-          $postdata = socket_read($remote, intval($_SERVER['HTTP_CONTENT_LENGTH']), PHP_BINARY_READ);
+          // this is a multipart request
+          $boundary =& $match[1];
+          $mode = 'data';
+          $last_line = '';
+          $i = 0;
+          while ( $data = socket_read($remote, 8388608, PHP_NORMAL_READ) )
+          {
+            $data_trim = trim($data, "\r\n");
+            if ( $mode != 'data' )
+            {
+              $data = str_replace("\r", '', $data);
+            }
+            if ( ( $data_trim === "--$boundary" || $data_trim === "--$boundary--" ) && $i > 0 )
+            {
+              // trim off the first LF and the last CRLF
+              $currval_data = substr($currval_data, 1, strlen($currval_data)-3);
+              
+              // this is the end of a part of the message; parse it into either $_POST or $_FILES
+              if ( is_string($have_a_file) )
+              {
+                
+                // write data to a temporary file
+                $errcode = UPLOAD_ERR_OK;
+                $tempfile = tempnam('phpupload', ( function_exists('sys_get_temp_dir') ? sys_get_temp_dir() : '/tmp' ));
+                if ( $fh = @fopen($tempfile, 'w') )
+                {
+                  if ( empty($have_a_file) )
+                  {
+                    $errcode = UPLOAD_ERR_NO_FILE;
+                  }
+                  else
+                  {
+                    fwrite($fh, $currval_data);
+                  }
+                  fclose($fh);
+                }
+                else
+                {
+                  $errcode = UPLOAD_ERR_CANT_WRITE;
+                }
+                $_FILES[$currval_name] = array(
+                    'name' => $have_a_file,
+                    'type' => $currval_type,
+                    'size' => filesize($tempfile),
+                    'tmp_name' => $tempfile,
+                    'error' => $errcode
+                  );
+              }
+              else
+              {
+                $_POST[$currval_name] = $currval_data;
+              }
+            }
+            
+            if ( $data_trim === "--$boundary" )
+            {
+              // switch from "data" mode to "headers" mode
+              $currval_name = '';
+              $currval_data = '';
+              $currval_type = '';
+              $have_a_file = false;
+              $mode = 'headers';
+            }
+            else if ( $data_trim === "--$boundary--" )
+            {
+              // end of request
+              break;
+            }
+            else if ( ( empty($data_trim) && empty($last_line) ) && $mode == 'headers' )
+            {
+              // start of data
+              $mode = 'data';
+            }
+            else if ( $mode == 'headers' )
+            {
+              // read header
+              // we're only looking for Content-Disposition and Content-Type
+              if ( preg_match('#^Content-Disposition: form-data; name="([^"\a\t\r\n]+)"(?:; filename="([^"\a\t\r\n]+)")?#i', $data_trim, $match) )
+              {
+                // content-disposition header, set name and mode.
+                $currval_name = $match[1];
+                if ( isset($match[2]) )
+                {
+                  $have_a_file = $match[2];
+                }
+                else
+                {
+                  $have_a_file = false;
+                }
+              }
+              else if ( preg_match('#^Content-Type: ([a-z0-9-]+/[a-z0-9/-]+)$#i', $data_trim, $match) )
+              {
+                $currval_type = $match[1];
+              }
+            }
+            else if ( $mode == 'data' )
+            {
+              $currval_data .= $data;
+            }
+            $last_line = $data_trim;
+            $i++;
+          }
         }
         else
         {
-          $postdata = socket_read($remote, 8388608, PHP_NORMAL_READ);
-        }
-        if ( preg_match_all('/(^|&)([a-z0-9_\.\[\]-]+)(=[^ &]+)?/', $postdata, $matches) )
-        {
-          if ( isset($matches[1]) )
+          if ( isset($_SERVER['HTTP_CONTENT_LENGTH']) )
+          {
+            $postdata = socket_read($remote, intval($_SERVER['HTTP_CONTENT_LENGTH']), PHP_BINARY_READ);
+          }
+          else
           {
-            foreach ( $matches[0] as $i => $_ )
+            $postdata = socket_read($remote, 8388608, PHP_NORMAL_READ);
+          }
+          if ( preg_match_all('/(^|&)([a-z0-9_\.\[\]-]+)(=[^ &]+)?/', $postdata, $matches) )
+          {
+            if ( isset($matches[1]) )
             {
-              $_POST[$matches[2][$i]] = ( !empty($matches[3][$i]) ) ? urldecode(substr($matches[3][$i], 1)) : true;
+              foreach ( $matches[0] as $i => $_ )
+              {
+                $_POST[$matches[2][$i]] = ( !empty($matches[3][$i]) ) ? urldecode(substr($matches[3][$i], 1)) : true;
+              }
             }
           }
         }
@@ -456,6 +565,10 @@
         }
       }
       
+      $_GET = $this->parse_multi_depth_array($_GET);
+      $_POST = $this->parse_multi_depth_array($_POST);
+      $_FILES = $this->parse_multi_depth_array($_FILES);
+      
       // init handler
       $handler = false;
       
@@ -514,6 +627,18 @@
       
       $this->send_standard_response($remote, $handler, $uri, $params);
       
+      // now that we're done sending the response, delete any temporary uploaded files
+      if ( !empty($_FILES) )
+      {
+        foreach ( $_FILES as $file_data )
+        {
+          if ( file_exists($file_data['tmp_name']) )
+          {
+            @unlink($file_data['tmp_name']);
+          }
+        }
+      }
+      
       if ( !$this->in_keepalive && defined('HTTPD_WS_CHILD') )
       {
         // if ( defined('HTTPD_WS_CHILD') )
@@ -761,12 +886,21 @@
           $output = ob_get_contents();
           ob_end_clean();
         }
+        // throw an HttpExceptionFatal when you need to break out of an in-progress scriptlet due to an error, use it in place of die() or exit()
+        catch ( HttpExceptionFatal $e )
+        {
+          restore_error_handler();
+          $this->send_http_error($socket, 500, "A handler crashed reporting a fatal exception; see the command line for details.");
+          if ( function_exists('status') )
+            status("fatal exception in handler {$handler['id']}:\n$e");
+          return true;
+        }
         catch ( Exception $e )
         {
           restore_error_handler();
-          $this->send_http_error($socket, 500, "A handler crashed with an exception; see the command line for details.");
+          $this->send_http_error($socket, 500, "There was an uncaught exception during the execution of a scripted handler function. See the command line for details.");
           if ( function_exists('status') )
-            status("caught exception in handler {$handler['id']}:\n$e");
+            status("uncaught exception in handler {$handler['id']}:\n$e");
           return true;
         }
         restore_error_handler();
@@ -1302,6 +1436,40 @@
     return false;
   }
   
+  /**
+   * Takes a flat array with keys of format foo[bar] and parses it into multiple depths.
+   * @param array
+   * @return array
+   */
+  
+  function parse_multi_depth_array($array)
+  {
+    foreach ( $array as $key => $value )
+    {
+      if ( preg_match('/^([^\[\]]+)\[([^\]]+)\]/', $key, $match) )
+      {
+        $parent =& $match[1];
+        $child =& $match[2];
+        if ( !isset($array[$parent]) || ( isset($array[$parent]) && !is_array($array[$parent]) ) )
+        {
+          $array[$parent] = array();
+        }
+        $array[$parent][$child] = $value;
+        unset($array[$key]);
+        $array[$parent] = $this->parse_multi_depth_array($array[$parent]);
+      }
+    }
+    return $array;
+  }
+  
+}
+
+/**
+ * Exception class that allows breaking directly out of a scriptlet.
+ */
+
+class HttpExceptionFatal extends Exception
+{
 }
 
 /**