-
-
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; | |
| } | |
| } | |
| } |
@sera527 still it has problem with file: "The image failed to upload." any ideas?!
@sera527 still it has problem with file: "The image failed to upload." any ideas?!
I was struggling with the same while using $image->isValid() method and it was causing this error message. I replaced with $image->isReadable() and it worked for me.
isValid() method looks for the file that if the file is uploaded successfully and has been uploaded with a POST request so it returns false because of it's a PUT request.
On the other hand isReadable() method looks for a file that is readable.
I would like to read one part from the stream at a time, rather than read all parts into an array.
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;
}
Fantastic addition! Thank you.