Manually Parse Multipart Form Data to Populate PHP Global $_POST/$_PUT Variables with a Simulated Request Body from stdin or File
For testing purposes I needed to simulate a multipart POST request from the command line. The body of a multipart request is huge and full of newlines so it's practical to dump it into a text file and redirect it to stdin. Multipart can't be parsed simply with parse_str() so it's necessary to manually parse the input. It seems most people running into this problem are trying to implement an ideologically-correct RESTful PUT interface but I found a tidy, global-agnostic class by misiek08 at https://gist.github.com/misiek08/7988b3b9a9911e35d0b3 and made a minor correction.
I've converted it into a static class and added a couple of functions to suit my use case:
class HttpMultipartParser
{
public static function populate_post_stdin()
{
$parsed = self::parse_stdin();
$_POST = $parsed['variables'];
$_FILES = $parsed['files'];
}
public static function populate_put_stdin()
{
$parsed = self::parse_stdin();
$_PUT = $parsed['variables'];
$_FILES = $parsed['files'];
}
public static function parse_stdin()
{
$stream = fopen('php://stdin', 'r');
return self::parse_multipart($stream);
}
public static function parse_multipart($stream, $boundary = null)
{
$return = array('variables' => array(), 'files' => array());
$partInfo = null;
while(($lineN = fgets($stream)) !== false)
{
if(strpos($lineN, '--') === 0)
{
if(!isSet($boundary) || $boundary == null)
{
$boundary = rtrim($lineN);
}
continue;
}
$line = rtrim($lineN);
if($line == '')
{
if(!empty($partInfo['Content-Disposition']['filename']))
{
self::parse_file($stream, $boundary, $partInfo, $return['files']);
}
elseif($partInfo != null)
{
self::parse_variable($stream, $boundary, $partInfo['Content-Disposition']['name'], $return['variables']);
}
$partInfo = null;
continue;
}
$delim = strpos($line, ':');
$headerKey = substr($line, 0, $delim);
$headerVal = ltrim($line, $delim + 1);
$partInfo[$headerKey] = self::parse_header_value($headerVal, $headerKey);
}
fclose($stream);
return $return;
}
public static function parse_header_value($line, $header = '')
{
$retval = array();
$regex = '/(^|;)\s*(?P<name>[^=:,;\s"]*):?(=("(?P<quotedValue>[^"]*(\\.[^"]*)*)")|(\s*(?P<value>[^=,;\s"]*)))?/mx';
$matches = null;
preg_match_all($regex, $line, $matches, PREG_SET_ORDER);
for($i = 0; $i < count($matches); $i++)
{
$match = $matches[$i];
$name = $match['name'];
$quotedValue = $match['quotedValue'];
if(empty($quotedValue))
{
$value = $match['value'];
}
else {
$value = stripcslashes($quotedValue);
}
if($name == $header && $i == 0)
{
$name = 'value';
}
$retval[$name] = $value;
}
return $retval;
}
public static function parse_variable($stream, $boundary, $name, &$array)
{
$fullValue = '';
$lastLine = null;
while(($lineN = fgets($stream)) !== false && strpos($lineN, $boundary) !== 0)
{
if($lastLine != null)
{
$fullValue .= $lastLine;
}
$lastLine = $lineN;
}
if($lastLine != null)
{
$fullValue .= rtrim($lastLine, '\r\n');
}
$array[$name] = $fullValue;
}
public static function parse_file($stream, $boundary, $info, &$array)
{
$tempdir = sys_get_temp_dir();
$name = $info['Content-Disposition']['name'];
$fileStruct['name'] = $info['Content-Disposition']['filename'];
$fileStruct['type'] = $info['Content-Type']['value'];
$array[$name] = &$fileStruct;
if(empty($tempdir))
{
$fileStruct['error'] = UPLOAD_ERR_NO_TMP_DIR;
return;
}
$tempname = tempnam($tempdir, 'php_upl');
$outFP = fopen($tempname, 'wb');
if($outFP === false)
{
$fileStruct['error'] = UPLOAD_ERR_CANT_WRITE;
return;
}
$lastLine = null;
while(($lineN = fgets($stream, 4096)) !== false)
{
if($lastLine != null)
{
if(strpos($lineN, $boundary) === 0) break;
if(fwrite($outFP, $lastLine) === false)
{
$fileStruct = UPLOAD_ERR_CANT_WRITE;
return;
}
}
$lastLine = $lineN;
}
if($lastLine != null)
{
if(fwrite($outFP, rtrim($lastLine, '\r\n')) === false)
{
$fileStruct['error'] = UPLOAD_ERR_CANT_WRITE;
return;
}
}
$fileStruct['error'] = UPLOAD_ERR_OK;
$fileStruct['size'] = filesize($tempname);
$fileStruct['tmp_name'] = $tempname;
}
}