-
-
Save devmycloud/df28012101fbc55d8de1737762b70348 to your computer and use it in GitHub Desktop.
<?php | |
use Illuminate\Support\Facades\Log; | |
use Symfony\Component\HttpFoundation\File\UploadedFile; | |
/** | |
* 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 [email protected] | |
* @license http://www.gnu.org/licenses/gpl.html GPL License 3 | |
* | |
* Massive modifications by TGE ([email protected]) to support | |
* proper parameter name processing and Laravel compatible UploadedFile | |
* support. Class name changed to be more descriptive and less likely to | |
* collide. | |
* | |
* Original Gist at: | |
* https://gist.github.com/jas-/5c3fdc26fedd11cb9fb5#file-class-stream-php | |
* | |
*/ | |
class ParseInputStream | |
{ | |
/** | |
* @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 = [ | |
'parameters' => $this->parse(), | |
'files' => [] | |
]; | |
} 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 = []; | |
foreach($array as $key => $value) | |
{ | |
if (empty($value)) | |
continue; | |
$block = $this->decide($value); | |
foreach ( $block['parameters'] as $key => $val ) { | |
$this->parse_parameter( $results, $key, $val ); | |
} | |
foreach ( $block['files'] as $key => $val ) { | |
$this->parse_parameter( $results, $key, $val ); | |
} | |
} | |
return $results; | |
} | |
/** | |
* @function decide | |
* @param $string string | |
* @returns array | |
*/ | |
private function decide($string) | |
{ | |
if (strpos($string, 'application/octet-stream') !== FALSE) | |
{ | |
return [ | |
'parameters' => $this->file($string), | |
'files' => [] | |
]; | |
} | |
if (strpos($string, 'filename') !== FALSE) | |
{ | |
return [ | |
'parameters' => [], | |
'files' => $this->file_stream($string) | |
]; | |
} | |
return [ | |
'parameters' => $this->parameter($string), | |
'files' => [] | |
]; | |
} | |
/** | |
* @function file | |
* | |
* @param $string | |
* | |
* @return array | |
*/ | |
private function file($string) | |
{ | |
preg_match('/name=\"([^\"]*)\".*stream[\n|\r]+([^\n\r].*)?$/s', $string, $match); | |
return [ | |
$match[1] => ($match[2] !== NULL ? $match[2] : '') | |
]; | |
} | |
/** | |
* @function file_stream | |
* | |
* @param $string | |
* | |
* @return array | |
*/ | |
private function file_stream($data) | |
{ | |
$result = []; | |
$data = ltrim($data); | |
$idx = strpos( $data, "\r\n\r\n" ); | |
if ( $idx === FALSE ) { | |
Log::warning( "ParseInputStream.file_stream(): Could not locate header separator in data:" ); | |
Log::warning( $data ); | |
} else { | |
$headers = substr( $data, 0, $idx ); | |
$content = substr( $data, $idx + 4, -2 ); // Skip the leading \r\n and strip the final \r\n | |
$name = '-unknown-'; | |
$filename = '-unknown-'; | |
$filetype = 'application/octet-stream'; | |
$header = strtok( $headers, "\r\n" ); | |
while ( $header !== FALSE ) { | |
if ( substr($header, 0, strlen("Content-Disposition: ")) == "Content-Disposition: " ) { | |
// Content-Disposition: form-data; name="attach_file[TESTING]"; filename="label2.jpg" | |
if ( preg_match('/name=\"([^\"]*)\"/', $header, $nmatch ) ) { | |
$name = $nmatch[1]; | |
} | |
if ( preg_match('/filename=\"([^\"]*)\"/', $header, $nmatch ) ) { | |
$filename = $nmatch[1]; | |
} | |
} elseif ( substr($header, 0, strlen("Content-Type: ")) == "Content-Type: " ) { | |
// Content-Type: image/jpg | |
$filetype = trim( substr($header, strlen("Content-Type: ")) ); | |
} else { | |
Log::debug( "PARSEINPUTSTREAM: Skipping Header: " . $header ); | |
} | |
$header = strtok("\r\n"); | |
} | |
if ( substr($data, -2) === "\r\n" ) { | |
$data = substr($data, 0, -2); | |
} | |
$path = sys_get_temp_dir() . '/php' . substr( sha1(rand()), 0, 6 ); | |
$bytes = file_put_contents( $path, $content ); | |
if ( $bytes !== FALSE ) { | |
$file = new UploadedFile( $path, $filename, $filetype, $bytes, UPLOAD_ERR_OK ); | |
$result = array( $name => $file ); | |
} | |
} | |
return $result; | |
} | |
/** | |
* @function parameter | |
* | |
* @param $string | |
* | |
* @return array | |
*/ | |
private function parameter($string) | |
{ | |
$data = []; | |
if ( preg_match('/name=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $string, $match) ) { | |
if (preg_match('/^(.*)\[\]$/i', $match[1], $tmp)) { | |
$data[$tmp[1]][] = ($match[2] !== NULL ? $match[2] : ''); | |
} else { | |
$data[$match[1]] = ($match[2] !== NULL ? $match[2] : ''); | |
} | |
} | |
return $data; | |
} | |
/** | |
* @function merge | |
* @param $array array | |
* | |
* Ugly ugly ugly | |
* | |
* @returns array | |
*/ | |
private function merge($array) | |
{ | |
$results = [ | |
'parameters' => [], | |
'files' => [] | |
]; | |
if (count($array['parameters']) > 0) { | |
foreach($array['parameters'] as $key => $value) { | |
foreach($value as $k => $v) { | |
if (is_array($v)) { | |
foreach($v as $kk => $vv) { | |
$results['parameters'][$k][] = $vv; | |
} | |
} else { | |
$results['parameters'][$k] = $v; | |
} | |
} | |
} | |
} | |
if (count($array['files']) > 0) { | |
foreach($array['files'] 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['files'][$k][$kk] = $vv[0]; | |
} else { | |
$results['files'][$k][$kk][] = $vv[0]; | |
} | |
} | |
} else { | |
$results['files'][$k][$key] = $v; | |
} | |
} | |
} | |
} | |
return $results; | |
} | |
function parse_parameter( &$params, $parameter, $value ) { | |
if ( strpos($parameter, '[') !== FALSE ) { | |
$matches = array(); | |
if ( preg_match( '/^([^[]*)\[([^]]*)\](.*)$/', $parameter, $match ) ) { | |
$name = $match[1]; | |
$key = $match[2]; | |
$rem = $match[3]; | |
if ( $name !== '' && $name !== NULL ) { | |
if ( ! isset($params[$name]) || ! is_array($params[$name]) ) { | |
$params[$name] = array(); | |
} else { | |
} | |
if ( strlen($rem) > 0 ) { | |
if ( $key === '' || $key === NULL ) { | |
$arr = array(); | |
$this->parse_parameter( $arr, $rem, $value ); | |
$params[$name][] = $arr; | |
} else { | |
if ( !isset($params[$name][$key]) || !is_array($params[$name][$key]) ) { | |
$params[$name][$key] = array(); | |
} | |
$this->parse_parameter( $params[$name][$key], $rem, $value ); | |
} | |
} else { | |
if ( $key === '' || $key === NULL ) { | |
$params[$name][] = $value; | |
} else { | |
$params[$name][$key] = $value; | |
} | |
} | |
} else { | |
if ( strlen($rem) > 0 ) { | |
if ( $key === '' || $key === NULL ) { | |
// REVIEW Is this logic correct?! | |
$this->parse_parameter( $params, $rem, $value ); | |
} else { | |
if ( ! isset($params[$key]) || ! is_array($params[$key]) ) { | |
$params[$key] = array(); | |
} | |
$this->parse_parameter( $params[$key], $rem, $value ); | |
} | |
} else { | |
if ( $key === '' || $key === NULL ) { | |
$params[] = $value; | |
} else { | |
$params[$key] = $value; | |
} | |
} | |
} | |
} else { | |
Log::warning( "ParseInputStream.parse_parameter() Parameter name regex failed: '" . $parameter . "'" ); | |
} | |
} else { | |
$params[$parameter] = $value; | |
} | |
} | |
} |
This library isn't working in laravel 5.6, at this following line "preg_match('/boundary=(.*)$/', $_SERVER['CONTENT_TYPE'], $matches);" it doesn't return anything.
I had the same problem.
I found a solution that worked as middlware:
https://gist.github.com/JhonatanRaul/cb2f9670ad0a8aa2fc32d263f948342a
Tyvm folks,
Good Jobs!
Thanks a lot! It was useful!
Some additions:
-
do we really need function merge()? Didn't find any usage.
-
i guess this class is only necessary for PUT and PATCH (POST works out of the box, OPTIONS/DELETE/HEAD/etc shouldn't send files), so middleware handler may looks like:
public function handle($request, Closure $next)
{
if ($request->method() == 'PUT' || $request->method() == 'PATCH') {
if (preg_match('/multipart\/form-data/', $request->headers->get('Content-Type'))) {
$params = array();
new ParseInputStream($params);
$request->request->add($params);
}
}
return $next($request);
}
- lines 211-212, variable $data is not used anywhere after:
if ( substr($data, -2) === "\r\n" ) {
$data = substr($data, 0, -2);
}
Guess, it suppose to be $content instead.
First thank you for this, been banging my head against a wall for the last few days on this, and of course to @t202wes for teh middleware!
Spotted a minor issue with it though, essentially it breaks down if empty/null values are sent through. Not wanting to strip them (we allow null values on updates for certain resources), so have modified this section slightly, to just cater for when a parameter doesn't have a supplied value (null).
Starts at line 248.
if (preg_match('/name=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $string, $match)) { if (preg_match('/^(.*)\[\]$/i', $match[1], $tmp)) { $data[$tmp[1]][] = (isset($match[2]) && $match[2] !== null ? $match[2] : null); } else { $data[$match[1]] = (isset($match[2]) && $match[2] !== null ? $match[2] : null); } }
Tested and working on 5.7
Thanks for the fix, I was looking for this.
I also found a minor issue with this. If the input is array, its only pick the last element. I modified slightly to fix this. Tested on laravel 5.8 with array and nested array.
private function parameter($string)
{
$data = [];
if (preg_match('/name=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $string, $match)) {
if (preg_match('/^(.*)\[\]$/i', $match[1], $tmp)) {
$data[$tmp[1].'[]'] = (isset($match[2]) && $match[2] !== null ? $match[2] : null);
} else {
$data[$match[1]] = (isset($match[2]) && $match[2] !== null ? $match[2] : null);
}
}
return $data;
}
I would like to read one part from the stream at a time, rather than read all parts into an array.