Last active
December 6, 2023 15:22
-
-
Save devmycloud/df28012101fbc55d8de1737762b70348 to your computer and use it in GitHub Desktop.
Process php://input to get multipart/form-data parameters for PATCH API request
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 | |
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; | |
} | |
} | |
} |
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;
}
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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:
Guess, it suppose to be $content instead.