I have recently found that I needed to be able to parse multipart/form-data for PUT requests made to my REST API. This allows batch photo uploads, which has been a requested feature. As we all know, PHP (at least as of version 5.3) does not support this natively, so had to roll my own. I have tried my best to make it work exactly as the native PHP parsing works and have made it populate the $_POST variable with the data passed to PUT simply because, as the api also returns whole webpages inside browsers, the methods that use PUT also have to accept POST requests with uri's indicating that they be sent to the PUT action of the controller.
I found a bit of code for doing this on StackOverflow, but it always loaded the whole input into memory to work with it, thus making it arguably impractical in production because uploaded files, especially combined, could very well overflow the amount of memory allowed to PHP.
Anyway, code for parsing input is below. If anyone sees any flaws for security, things that could be optimized or places that it doesn't return the same as PHP's native parsing, let me know.
I know my coding style sucks, that's a given as a self taught programmer. I am currently working on getting everything into proper style across this project.
<?php
function parse_input(){
$content_type = ( isset( $_SERVER['CONTENT_TYPE'] ) )? $_SERVER['CONTENT_TYPE']: 'application/x-www-form-urlencoded';
$tmp = explode( ';', $content_type );
$boundary = '';
$encoding = '';
$content_type = array_shift( $tmp );
foreach( $tmp as $t ){
if( strpos( $t, 'boundary' ) !== false ){
$t = explode( '=', $t, 2 );
if( isset( $t[ 1 ] ) )
$boundary = '--' . $t[1];
}
else if( strpos( $t, 'charset' ) !== false ){
$t = explode( '=', $t, 2 );
if( isset( $t[ 1 ] ) )
$encoding = $t[1];
}
if( $boundary !== '' && $encoding !== '' )
break;
}
switch( $content_type ){
case 'multipart/form-data':
#grab multipart boundary from content type header
if( !empty( $boundary ) )
break;
$this->content_type = 'application/x-www-form-urlencoded';
case 'application/x-www-form-urlencoded':
parse_str( file_get_contents( 'php://input' ), $_POST );
return;
default:
return;
}
$_FILES = array();
$_POST = array();
$chunkLength = 8096;
$raw_headers = '';
$stream = fopen( 'php://input', 'rb' );
$sanity = fgets( $stream, strlen( $boundary ) + 5 );
if( rtrim( $sanity ) !== $boundary ) #malformed file, boundary should be first item
return;
while( ( $chunk = fgets( $stream ) ) !== false ){
if( $chunk === $boundary )
continue;
if( rtrim( $chunk ) == '' ){ #blank line means we have all the headers and are going to read content
$raw_headers = explode( "\r\n", $raw_headers );
$headers = array();
$matches = array();
foreach( $raw_headers as $header ){
if( strpos( $header, ':' ) === false )
continue;
list( $name, $value ) = explode( ':', $header, 2 );
$headers[ strtolower( $name ) ] = ltrim( $value, ' ' );
}
$raw_headers = '';
if( !isset( $headers[ 'content-disposition' ] ) )
continue;
$filename = NULL;
preg_match(
'/^(.+); *name="([^"]+)"(; *filename="([^"]+)")?/',
$headers['content-disposition'],
$matches
);
list( , $type, $name ) = $matches;
#process data
if( isset( $matches[ 4 ] ) ){ #pull in file
$error = UPLOAD_ERR_OK;
$filename = $matches[ 4 ];
$filename_parts = pathinfo( $filename );
$contentType = 'unknown';
if( isset( $headers[ 'content-type' ] ) ){
$tmp = explode( ';', $headers[ 'content-type' ] );
$contentType = $tmp[0];
}
$tmpnam = tempnam( ini_get( 'upload_tmp_dir' ), 'php' );
$fileHandle = fopen( $tmpnam, 'wb' );
if( $fileHandle === false )
$error = UPLOAD_ERR_CANT_WRITE;
else{
$lastLine = NULL;
while( ( $chunk = fgets( $stream, $chunkLength ) ) !== false && strpos( $chunk, $boundary ) !== 0 ) {
if( $lastLine !== NULL ){
if( fwrite( $fileHandle, $lastLine ) === false ){
$error = UPLOAD_ERR_CANT_WRITE;
break;
}
}
$lastLine = $chunk;
}
if( $lastLine !== NULL && $error !== UPLOAD_ERR_CANT_WRITE ){
if( fwrite( $fileHandle, rtrim( $lastLine, "\r\n" ) ) === false )
$error = UPLOAD_ERR_CANT_WRITE;
}
}
$items = array(
'name' => $filename,
'type' => $contentType,
'tmp_name' => $tmpnam,
'error' => $error,
'size' => filesize( $tmpnam )
);
$tmp = explode( '[', $name, 2 );
foreach( $items as $index => $item ){
$spec = $index;
if( count( $tmp ) > 1 )
$spec .= '[' . $tmp[1];
$t = $spec . '=' . $item;
parse_str( $t, $array2 );
$_FILES = recursive_setter( $spec, $_FILES, $array2 );
}
continue;
}
else{ #pull in variable
$fullValue = '';
$lastLine = NULL;
while( ( $chunk = fgets( $stream ) ) !== false && strpos( $chunk, $boundary ) !== 0 ){
if( $lastLine !== NULL )
$fullValue .= $lastLine;
$lastLine = $chunk;
}
if( $lastLine !== NULL )
$fullValue .= rtrim( $lastLine, "\r\n" );
if( isset( $headers[ 'content-type' ] ) ){
$tmp = explode( ';', $headers[ 'content-type' ] );
$encoding = '';
foreach( $tmp as $t ){
if( strpos( $t, 'charset' ) !== false ){
$t = explode( $t, '=', 2 );
if( isset( $t[ 1 ] ) )
$encoding = $t[1];
break;
}
}
if( $encoding !== '' && strtoupper( $encoding ) !== 'UTF-8' && strtoupper( $encoding ) !== 'UTF8' ){
$tmp = mb_convert_encoding( $fullValue, 'UTF-8', $encoding );
if( $tmp !== false )
$fullValue = $tmp;
}
}
$fullValue = $name . '=' . $fullValue;
$origName = $name;
$tmp = array();
parse_str( $fullValue, $tmp );
$_POST = recursive_setter( $origName, $_POST, $tmp );
}
continue;
}
$raw_headers .= $chunk;
}
fclose( $stream );
}
function recursive_setter( $spec, $array, $array2 ){
if( !is_array( $spec ) )
$spec = explode( '[', (string)$spec );
$currLev = array_shift( $spec );
$currLev = rtrim( $currLev, ']' );
if( $currLev !== '' ){
$currLev = $currLev . '=p';
$tmp = array();
parse_str( $currLev, $tmp );
$tmp = array_keys( $tmp );
$currLev = reset( $tmp );
}
if( !is_array( $array ) ){
$array = $array2;
}else if( $currLev === '' ){
$array[] = reset( $array2 );
}else if( isset( $array[ $currLev ] ) && isset( $array2[ $currLev ] ) ){
$array[ $currLev ] = recursive_setter( $spec, $array[ $currLev ], $array2[ $currLev ] );
}else if( isset( $array2[ $currLev ] ) ){
$array[ $currLev ] = $array2[ $currLev ];
}
return $array;
}
if( !is_array( $spec ) )
$spec = explode( '[', (string)$spec );
$currLev = array_shift( $spec );
$currLev = rtrim( $currLev, ']' );
if( $currLev !== '' ){
$currLev = $currLev . '=p';
$tmp = array();
parse_str( $currLev, $tmp );
$tmp = array_keys( $tmp );
$currLev = reset( $tmp );
}
if( !is_array( $array ) ){
$array = $array2;
}else if( $currLev === '' ){
$array[] = reset( $array2 );
}else if( isset( $array[ $currLev ] ) && isset( $array2[ $currLev ] ) ){
$array[ $currLev ] = self::recursive_setter( $spec, $array[ $currLev ], $array2[ $currLev ] );
}else if( isset( $array2[ $currLev ] ) ){
$array[ $currLev ] = $array2[ $currLev ];
}
return $array;
}
?>