Skip to content

Instantly share code, notes, and snippets.

@jas-
Last active June 2, 2022 18:22
Show Gist options
  • Star 26 You must be signed in to star a gist
  • Fork 11 You must be signed in to fork a gist
  • Save jas-/5c3fdc26fedd11cb9fb5 to your computer and use it in GitHub Desktop.
Save jas-/5c3fdc26fedd11cb9fb5 to your computer and use it in GitHub Desktop.
PHP stream handler w/ support for multiple files over PUT
<?php
/**
* stream - Handle raw input stream
*
* LICENSE: This source file is subject to version 3.01 of the GPL license
* that is available through the world-wide-web at the following URI:
* http://www.gnu.org/licenses/gpl.html. If you did not receive a copy of
* the GPL License and are unable to obtain it through the web, please
*
* @author jason.gerfen@gmail.com
* @license http://www.gnu.org/licenses/gpl.html GPL License 3
*/
class stream
{
/**
* @var input
* @abstract Raw input stream
*/
protected $input;
/**
* @function __construct
* @param $data stream
*/
public function __construct(array &$data)
{
$this->input = file_get_contents('php://input');
$boundary = $this->boundary();
if (!count($boundary)) {
return array(
'post' => $this->parse($this->input),
'file' => array()
);
}
$blocks = $this->split($boundary);
$data = $this->blocks($blocks);
return $data;
}
/**
* @function boundary
* @returns Array
*/
private function boundary()
{
preg_match('/boundary=(.*)$/', $_SERVER['CONTENT_TYPE'], $matches);
return $matches[1];
}
/**
* @function parse
* @returns Array
*/
private function parse()
{
parse_str(urldecode($this->input), $result);
return $result;
}
/**
* @function split
* @param $boundary string
* @returns Array
*/
private function split($boundary)
{
$result = preg_split("/-+$boundary/", $this->input);
array_pop($result);
return $result;
}
/**
* @function blocks
* @param $array array
* @returns Array
*/
private function blocks($array)
{
$results = array(
'post' => array(),
'file' => array()
);
foreach($array as $key => $value)
{
if (empty($value))
continue;
$block = $this->decide($value);
if (count($block['post']) > 0)
array_push($results['post'], $block['post']);
if (count($block['file']) > 0)
array_push($results['file'], $block['file']);
}
return $this->merge($results);
}
/**
* @function decide
* @param $string string
* @returns Array
*/
private function decide($string)
{
if (strpos($string, 'application/octet-stream') !== FALSE)
{
return array(
'post' => $this->file($string),
'file' => array()
);
}
if (strpos($string, 'filename') !== FALSE)
{
return array(
'post' => array(),
'file' => $this->file_stream($string)
);
}
return array(
'post' => $this->post($string),
'file' => array()
);
}
/**
* @function file
* @param $boundary string
* @returns Array
*/
private function file($string)
{
preg_match('/name=\"([^\"]*)\".*stream[\n|\r]+([^\n\r].*)?$/s', $string, $match);
return array(
$match[1] => $match[2]
);
}
/**
* @function file_stream
* @param $boundary string
* @returns Array
*/
private function file_stream($string)
{
$data = array();
preg_match('/name=\"([^\"]*)\"; filename=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $string, $match);
preg_match('/Content-Type: (.*)?/', $match[3], $mime);
$image = preg_replace('/Content-Type: (.*)[^\n\r]/', '', $match[3]);
$path = sys_get_temp_dir().'/php'.substr(sha1(rand()), 0, 6);
$err = file_put_contents($path, $image);
if (preg_match('/^(.*)\[\]$/i', $match[1], $tmp)) {
$index = $tmp[1];
} else {
$index = $match[1];
}
$data[$index]['name'][] = $match[2];
$data[$index]['type'][] = $mime[1];
$data[$index]['tmp_name'][] = $path;
$data[$index]['error'][] = ($err === FALSE) ? $err : 0;
$data[$index]['size'][] = filesize($path);
return $data;
}
/**
* @function post
* @param $boundary string
* @returns Array
*/
private function post($string)
{
$data = array();
preg_match('/name=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $string, $match);
if (preg_match('/^(.*)\[\]$/i', $match[1], $tmp)) {
$data[$tmp[1]][] = $match[2];
} else {
$data[$match[1]] = $match[2];
}
return $data;
}
/**
* @function merge
* @param $array array
*
* Ugly ugly ugly
*
* @returns Array
*/
private function merge($array)
{
$results = array(
'post' => array(),
'file' => array()
);
if (count($array['post'] > 0)) {
foreach($array['post'] as $key => $value) {
foreach($value as $k => $v) {
if (is_array($v)) {
foreach($v as $kk => $vv) {
$results['post'][$k][] = $vv;
}
} else {
$results['post'][$k] = $v;
}
}
}
}
if (count($array['file'] > 0)) {
foreach($array['file'] as $key => $value) {
foreach($value as $k => $v) {
if (is_array($v)) {
foreach($v as $kk => $vv) {
$results['file'][$kk][] = $vv[0];
}
} else {
$results['file'][$key] = $v;
}
}
}
}
return $results;
}
}
<?php
/**
* Parse raw HTTP request data
* http://www.chlab.ch/blog/archives/php/manually-parse-raw-http-data-php
*
* Pass in $a_data as an array. This is done by reference to avoid copying
* the data around too much.
*
* Any files found in the request will be added by their field name to the
* $data['files'] array.
*
* @param array Empty array to fill with data
* @return array Associative array of request data
*/
function parse_raw_http_request(array &$a_data)
{
// read incoming data
$input = file_get_contents('php://input');
// grab multipart boundary from content type header
preg_match('/boundary=(.*)$/', $_SERVER['CONTENT_TYPE'], $matches);
// content type is probably regular form-encoded
if (!count($matches))
{
// we expect regular puts to containt a query string containing data
parse_str(urldecode($input), $a_data);
return $a_data;
}
$boundary = $matches[1];
// split content by boundary and get rid of last -- element
$a_blocks = preg_split("/-+$boundary/", $input);
array_pop($a_blocks);
// loop data blocks
foreach ($a_blocks as $id =&gt; $block)
{
if (empty($block))
continue;
// you'll have to var_dump $block to understand this and maybe replace \n or \r with a visibile char
// parse uploaded files
if (strpos($block, 'application/octet-stream') !== FALSE)
{
// match "name", then everything after "stream" (optional) except for prepending newlines
preg_match("/name=\"([^\"]*)\".*stream[\n|\r]+([^\n\r].*)?$/s", $block, $matches);
$a_data['files'][$matches[1]] = $matches[2];
}
// parse all other fields
else
{
if (strpos($block, 'filename') !== FALSE)
{
// match "name" and optional value in between newline sequences
preg_match('/name=\"([^\"]*)\"; filename=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $block, $matches);
preg_match('/Content-Type: (.*)?/', $matches[3], $mime);
// match the mime type supplied from the browser
$image = preg_replace('/Content-Type: (.*)[^\n\r]/', '', $matches[3]);
// get current system path and create tempory file name & path
$path = sys_get_temp_dir().'/php'.substr(sha1(rand()), 0, 6);
// write temporary file to emulate $_FILES super global
$err = file_put_contents($path, $image);
// Did the user use the infamous &lt;input name="array[]" for multiple file uploads?
if (preg_match('/^(.*)\[\]$/i', $matches[1], $tmp)) {
$a_data[$tmp[1]]['name'][] = $matches[2];
} else {
$a_data[$matches[1]]['name'][] = $matches[2];
}
// Create the remainder of the $_FILES super global
$a_data[$tmp[1]]['type'][] = $mime[1];
$a_data[$tmp[1]]['tmp_name'][] = $path;
$a_data[$tmp[1]]['error'][] = ($err === FALSE) ? $err : 0;
$a_data[$tmp[1]]['size'][] = filesize($path);
}
else
{
// match "name" and optional value in between newline sequences
preg_match('/name=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $block, $matches);
if (preg_match('/^(.*)\[\]$/i', $matches[1], $tmp)) {
$a_data[$tmp[1]][] = $matches[2];
} else {
$a_data[$matches[1]] = $matches[2];
}
}
}
}
}
<?php
include_once('class.stream.php');
$data = array();
new stream($data);
$_PUT = $data['post'];
$_FILES = $data['file'];
/* Handle moving the file(s) */
if (count($_FILES) > 0) {
foreach($_FILES as $key => $value) {
if (!is_uploaded_file($value['tmp_name'])) {
rename($value['tmp_name'], '/path/to/uploads/'.$value['name']);
} else {
move_uploaded_file($value['tmp_name'], '/path/to/uploads/'.$value['name']);
}
}
}
@marcusfritze
Copy link

nice work, but I think there is a little error in the stream.php

on line 71-81:

when the pattern isn't found in the subject and the preg_match returns 0, you can't use $tmp[1] in the lines 78-81

possible fix:

$name = "name";

// Did the user use the infamous for multiple file uploads?
if (preg_match('/^(.*)[]$/i', $matches[1], $tmp)) {
$name = $tmp[1];
} else {
$name = $matches[1];
}

$a_data[$name]['name'][] = $matches[2];

// Create the remainder of the $_FILES super global
$a_data[$name]['type'][] = $mime[1];
$a_data[$name]['tmp_name'][] = $path;
$a_data[$name]['error'][] = ($err === FALSE) ? $err : 0;
$a_data[$name]['size'][] = filesize($path);

@pogotc
Copy link

pogotc commented Jan 12, 2015

This doesn't seem to work for me using a multipart form and from reading http://php.net/manual/en/wrappers.php.php it says php://input is not available with an encoding type of "multipart/form-data" so it seems like it would never work.

Am I confusing the two use cases here?

@pogotc
Copy link

pogotc commented Jan 12, 2015

Just to add incase anybody else gets confused by this, it seems if you turn "enable_post_data_reading" off in php.ini then all post data (including multipart-form-data) goes into php://input

@gazou78
Copy link

gazou78 commented Aug 12, 2015

Great work!

Small corrections for "class.stream.php":

// line 146 (replace line) - some notice error in case of empty array

        $match[1] => (!empty($match[2]) ? $match[2] : '')

// line 166 (replace line) - space were added at the beginning of the image raw data

    $err = file_put_contents($path, ltrim($image));

// line 197 (replace line) - some notice error in case of empty array

        $data[$match[1]] = (!empty($match[2]) ? $match[2] : '');

Small improvement for "class.stream.php" (in order to keep the file input name in the $_FILES var like $_FILES["toto"] instead of $_FILES[0]):

// line 232 (replace whole block)

    if (count($array['file'] > 0)) {
        foreach($array['file'] as $key => $value) {
            foreach($value as $k => $v) {
                if (is_array($v)) {
                    foreach($v as $kk => $vv) {
                        if(
                            is_array($vv)
                            && (count($vv) == 1)
                        ) {
                            $results['file'][$k][$kk] = $vv[0];
                        } else {
                            $results['file'][$k][$kk][] = $vv[0];
                        }
                    }
                } else {
                    $results['file'][$k][$key] = $v;
                }
            }
        }
    }

@oytuntez
Copy link

This is the final version I am using:

<?php

/**
 * stream - Handle raw input stream
 *
 * LICENSE: This source file is subject to version 3.01 of the GPL license
 * that is available through the world-wide-web at the following URI:
 * http://www.gnu.org/licenses/gpl.html. If you did not receive a copy of
 * the GPL License and are unable to obtain it through the web, please
 *
 * @author jason.gerfen@gmail.com
 * @license http://www.gnu.org/licenses/gpl.html GPL License 3
 */

class Stream
{
    /**
     * @abstract Raw input stream
     */
    protected $input;

    /**
     * @function __construct
     *
     * @param array $data stream
     */
    public function __construct(array &$data)
    {
        $this->input = file_get_contents('php://input');

        $boundary = $this->boundary();

        if (!strlen($boundary)) {
            return array(
                'post' => $this->parse(),
                'file' => array()
            );
        }

        $blocks = $this->split($boundary);

        $data = $this->blocks($blocks);

        return $data;
    }

    /**
     * @function boundary
     * @returns string
     */
    private function boundary()
    {
        preg_match('/boundary=(.*)$/', $_SERVER['CONTENT_TYPE'], $matches);
        return $matches[1];
    }

    /**
     * @function parse
     * @returns array
     */
    private function parse()
    {
        parse_str(urldecode($this->input), $result);
        return $result;
    }

    /**
     * @function split
     * @param $boundary string
     * @returns array
     */
    private function split($boundary)
    {
        $result = preg_split("/-+$boundary/", $this->input);
        array_pop($result);
        return $result;
    }

    /**
     * @function blocks
     * @param $array array
     * @returns array
     */
    private function blocks($array)
    {
        $results = array(
            'post' => array(),
            'file' => array()
        );

        foreach($array as $key => $value)
        {
            if (empty($value))
                continue;

            $block = $this->decide($value);

            if (count($block['post']) > 0)
                array_push($results['post'], $block['post']);

            if (count($block['file']) > 0)
                array_push($results['file'], $block['file']);
        }

        return $this->merge($results);
    }

    /**
     * @function decide
     * @param $string string
     * @returns array
     */
    private function decide($string)
    {
        if (strpos($string, 'application/octet-stream') !== FALSE)
        {
            return array(
                'post' => $this->file($string),
                'file' => array()
            );
        }

        if (strpos($string, 'filename') !== FALSE)
        {
            return array(
                'post' => array(),
                'file' => $this->file_stream($string)
            );
        }

        return array(
            'post' => $this->post($string),
            'file' => array()
        );
    }

    /**
     * @function file
     *
     * @param $string
     *
     * @return array
     */
    private function file($string)
    {
        preg_match('/name=\"([^\"]*)\".*stream[\n|\r]+([^\n\r].*)?$/s', $string, $match);
        return array(
            $match[1] => (!empty($match[2]) ? $match[2] : '')
        );
    }

    /**
     * @function file_stream
     *
     * @param $string
     *
     * @return array
     */
    private function file_stream($string)
    {
        $data = array();

        preg_match('/name=\"([^\"]*)\"; filename=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $string, $match);
        preg_match('/Content-Type: (.*)?/', $match[3], $mime);

        $image = preg_replace('/Content-Type: (.*)[^\n\r]/', '', $match[3]);

        $path = sys_get_temp_dir().'/php'.substr(sha1(rand()), 0, 6);

        $err = file_put_contents($path, ltrim($image));

        if (preg_match('/^(.*)\[\]$/i', $match[1], $tmp)) {
            $index = $tmp[1];
        } else {
            $index = $match[1];
        }

        $data[$index]['name'][] = $match[2];
        $data[$index]['type'][] = $mime[1];
        $data[$index]['tmp_name'][] = $path;
        $data[$index]['error'][] = ($err === FALSE) ? $err : 0;
        $data[$index]['size'][] = filesize($path);

        return $data;
    }

    /**
     * @function post
     *
     * @param $string
     *
     * @return array
     */
    private function post($string)
    {
        $data = array();

        preg_match('/name=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $string, $match);

        if (preg_match('/^(.*)\[\]$/i', $match[1], $tmp)) {
            $data[$tmp[1]][] = (!empty($match[2]) ? $match[2] : '');
        } else {
            $data[$match[1]] = (!empty($match[2]) ? $match[2] : '');
        }

        return $data;
    }

    /**
     * @function merge
     * @param $array array
     *
     * Ugly ugly ugly
     *
     * @returns array
     */
    private function merge($array)
    {
        $results = array(
            'post' => array(),
            'file' => array()
        );

        if (count($array['post']) > 0) {
            foreach($array['post'] as $key => $value) {
                foreach($value as $k => $v) {
                    if (is_array($v)) {
                        foreach($v as $kk => $vv) {
                            $results['post'][$k][] = $vv;
                        }
                    } else {
                        $results['post'][$k] = $v;
                    }
                }
            }
        }

        if (count($array['file']) > 0) {
            foreach($array['file'] as $key => $value) {
                foreach($value as $k => $v) {
                    if (is_array($v)) {
                        foreach($v as $kk => $vv) {
                            if(is_array($vv) && (count($vv) === 1)) {
                                $results['file'][$k][$kk] = $vv[0];
                            } else {
                                $results['file'][$k][$kk][] = $vv[0];
                            }
                        }
                    } else {
                        $results['file'][$k][$key] = $v;
                    }
                }
            }
        }

        return $results;
    }
}

@oytuntez
Copy link

With a couple minor validations:

<?php

/**
 * stream - Handle raw input stream
 *
 * LICENSE: This source file is subject to version 3.01 of the GPL license
 * that is available through the world-wide-web at the following URI:
 * http://www.gnu.org/licenses/gpl.html. If you did not receive a copy of
 * the GPL License and are unable to obtain it through the web, please
 *
 * @author jason.gerfen@gmail.com
 * @license http://www.gnu.org/licenses/gpl.html GPL License 3
 */

class Stream
{
    /**
     * @abstract Raw input stream
     */
    protected $input;

    /**
     * @function __construct
     *
     * @param array $data stream
     */
    public function __construct(array &$data)
    {
        $this->input = file_get_contents('php://input');

        $boundary = $this->boundary();

        if (!strlen($boundary)) {
            $data = [
                'post' => $this->parse(),
                'file' => []
            ];
        } else {
            $blocks = $this->split($boundary);

            $data = $this->blocks($blocks);
        }

        return $data;
    }

    /**
     * @function boundary
     * @returns string
     */
    private function boundary()
    {
        if(!isset($_SERVER['CONTENT_TYPE'])) {
            return null;
        }

        preg_match('/boundary=(.*)$/', $_SERVER['CONTENT_TYPE'], $matches);
        return $matches[1];
    }

    /**
     * @function parse
     * @returns array
     */
    private function parse()
    {
        parse_str(urldecode($this->input), $result);
        return $result;
    }

    /**
     * @function split
     * @param $boundary string
     * @returns array
     */
    private function split($boundary)
    {
        $result = preg_split("/-+$boundary/", $this->input);
        array_pop($result);
        return $result;
    }

    /**
     * @function blocks
     * @param $array array
     * @returns array
     */
    private function blocks($array)
    {
        $results = [
            'post' => [],
            'file' => []
        ];

        foreach($array as $key => $value)
        {
            if (empty($value))
                continue;

            $block = $this->decide($value);

            if (count($block['post']) > 0)
                array_push($results['post'], $block['post']);

            if (count($block['file']) > 0)
                array_push($results['file'], $block['file']);
        }

        return $this->merge($results);
    }

    /**
     * @function decide
     * @param $string string
     * @returns array
     */
    private function decide($string)
    {
        if (strpos($string, 'application/octet-stream') !== FALSE)
        {
            return [
                'post' => $this->file($string),
                'file' => []
            ];
        }

        if (strpos($string, 'filename') !== FALSE)
        {
            return [
                'post' => [],
                'file' => $this->file_stream($string)
            ];
        }

        return [
            'post' => $this->post($string),
            'file' => []
        ];
    }

    /**
     * @function file
     *
     * @param $string
     *
     * @return array
     */
    private function file($string)
    {
        preg_match('/name=\"([^\"]*)\".*stream[\n|\r]+([^\n\r].*)?$/s', $string, $match);
        return [
            $match[1] => (!empty($match[2]) ? $match[2] : '')
        ];
    }

    /**
     * @function file_stream
     *
     * @param $string
     *
     * @return array
     */
    private function file_stream($string)
    {
        $data = [];

        preg_match('/name=\"([^\"]*)\"; filename=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $string, $match);
        preg_match('/Content-Type: (.*)?/', $match[3], $mime);

        $image = preg_replace('/Content-Type: (.*)[^\n\r]/', '', $match[3]);

        $path = sys_get_temp_dir().'/php'.substr(sha1(rand()), 0, 6);

        $err = file_put_contents($path, ltrim($image));

        if (preg_match('/^(.*)\[\]$/i', $match[1], $tmp)) {
            $index = $tmp[1];
        } else {
            $index = $match[1];
        }

        $data[$index]['name'][] = $match[2];
        $data[$index]['type'][] = $mime[1];
        $data[$index]['tmp_name'][] = $path;
        $data[$index]['error'][] = ($err === FALSE) ? $err : 0;
        $data[$index]['size'][] = filesize($path);

        return $data;
    }

    /**
     * @function post
     *
     * @param $string
     *
     * @return array
     */
    private function post($string)
    {
        $data = [];

        preg_match('/name=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $string, $match);

        if (preg_match('/^(.*)\[\]$/i', $match[1], $tmp)) {
            $data[$tmp[1]][] = (!empty($match[2]) ? $match[2] : '');
        } else {
            $data[$match[1]] = (!empty($match[2]) ? $match[2] : '');
        }

        return $data;
    }

    /**
     * @function merge
     * @param $array array
     *
     * Ugly ugly ugly
     *
     * @returns array
     */
    private function merge($array)
    {
        $results = [
            'post' => [],
            'file' => []
        ];

        if (count($array['post']) > 0) {
            foreach($array['post'] as $key => $value) {
                foreach($value as $k => $v) {
                    if (is_array($v)) {
                        foreach($v as $kk => $vv) {
                            $results['post'][$k][] = $vv;
                        }
                    } else {
                        $results['post'][$k] = $v;
                    }
                }
            }
        }

        if (count($array['file']) > 0) {
            foreach($array['file'] as $key => $value) {
                foreach($value as $k => $v) {
                    if (is_array($v)) {
                        foreach($v as $kk => $vv) {
                            if(is_array($vv) && (count($vv) === 1)) {
                                $results['file'][$k][$kk] = $vv[0];
                            } else {
                                $results['file'][$k][$kk][] = $vv[0];
                            }
                        }
                    } else {
                        $results['file'][$k][$key] = $v;
                    }
                }
            }
        }

        return $results;
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment