2
\$\begingroup\$

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;
}
?>
\$\endgroup\$
  • \$\begingroup\$ currently, this code differs from php native because it urldecodes values and field names in multipart/form-data, i.e. variable%5B1%5D becomes variable[1] whereas native POST population fails to do this and will create $_POST = array( variable%5B1%5D => 'value' ) instead of $_POST = array( variable => array( 1 => 'value' ) ). It also checks encodings and reencodes all variables into utf8 whereas, assumably, PHP doesn't do anything as of 3.5 because it seems to assume that everything is ascii. \$\endgroup\$ – Reid Johnson Nov 14 '14 at 19:30

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.