Last active
June 2, 2022 18:22
-
-
Save jas-/5c3fdc26fedd11cb9fb5 to your computer and use it in GitHub Desktop.
PHP stream handler w/ support for multiple files over PUT
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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 => $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 <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]; | |
} | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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']); | |
} | |
} | |
} |
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
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;
}
}
}
}
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;
}
}
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
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?