Skip to content

Instantly share code, notes, and snippets.

@opiy-org
Last active October 26, 2022 01:53
Show Gist options
  • Save opiy-org/0367572317a68e2d5b4253db97495c62 to your computer and use it in GitHub Desktop.
Save opiy-org/0367572317a68e2d5b4253db97495c62 to your computer and use it in GitHub Desktop.
Laravel middleware to fix form-data/multipart in PATCH, PUT requests
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Closure;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile;
use Log;
use Symfony\Component\HttpFoundation\ParameterBag;
/**
* @author https://github.com/opiy-org
* @author https://github.com/Stunext
*
* PHP, and Laravel does not support multipart/form-data requests when using any request method other than POST.
* This limits the ability to implement RESTful architectures. This is a middleware for Laravel 8 that manually decoding
* the php://input stream when the request type is PUT or PATCH and the Content-Type header is mutlipart/form-data.
*
* this implementation is based on https://gist.github.com/Stunext/9171b7a8f3633b0b601a0feb8088dca1
* by https://github.com/Stunext
*/
class MultipartFix
{
private const SKIP_METHODS = [
Request::METHOD_POST,
Request::METHOD_GET,
];
private const LINE_BREAK = "\r\n";
private const CONTENT_TYPE = 'content-type';
private const CONTENT_DISPOSITION = 'content-disposition';
private const FILENAME_PREFIX = 'mtp';
/**
* Handle an incoming request.
*
* @param Request $request
* @param Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next): mixed
{
if (in_array($request->method(), self::SKIP_METHODS)) {
return $next($request);
}
if (preg_match('/multipart\/form-data/i', $request->headers->get(self::CONTENT_TYPE))) {
try {
$parameters = $this->getRequestFromStdIn();
$request->merge($parameters['inputs']);
$request->files->add($parameters['files']);
} catch (Exception $exception) {
Log::error('MultipartFix error', [
'error_message' => $exception->getMessage(),
]);
}
}
return $next($request);
}
/**
* @return array
* @throws Exception
*/
public function getRequestFromStdIn(): array
{
$files = [];
$fieldsData = [];
$rawData = file_get_contents('php://input');
if (!$rawData) {
throw new Exception('Can not read from stdin');
}
// Fetch content and determine boundary
$boundary = substr($rawData, 0, strpos($rawData, self::LINE_BREAK));
if (strlen($boundary) === 0) {
throw new Exception('Wrong data format');
}
// Fetch and process each part
$dataArr = explode($boundary, $rawData);
$dataParts = array_slice($dataArr, 1);
foreach ($dataParts as $part) {
// If this is the last part, break
if ($part === '--' . self::LINE_BREAK) {
break;
}
// Separate content from headers
$part = ltrim($part, self::LINE_BREAK);
[$rawHeaders, $content] = explode(self::LINE_BREAK . self::LINE_BREAK, $part, 2);
$content = substr($content, 0, strlen($content) - 2);
// Parse the headers list
$rawHeaders = explode(self::LINE_BREAK, $rawHeaders);
$headers = [];
foreach ($rawHeaders as $header) {
[$name, $value] = explode(':', $header);
$headers[strtolower($name)] = ltrim($value, ' ');
}
// Parse the Content-Disposition to get the field name, etc.
if (isset($headers[self::CONTENT_DISPOSITION])) {
preg_match(
'/^form-data; *name="([^"]+)"(; *filename="([^"]+)")?/',
$headers[self::CONTENT_DISPOSITION],
$matches
);
$fieldName = $matches[1];
$fileName = $matches[3] ?? null;
// If we have a file, save it. Otherwise, save the data.
if ($fileName) {
$localFileName = tempnam(sys_get_temp_dir(), self::FILENAME_PREFIX);
file_put_contents($localFileName, $content);
$files[$fieldName] = new UploadedFile($localFileName, $fileName, $headers[self::CONTENT_TYPE]);
// register a shutdown function to cleanup the temporary file
register_shutdown_function(static function () use ($localFileName) {
unlink($localFileName);
});
} else {
$fieldsData[$fieldName] = $content;
}
}
}
$fields = new ParameterBag($fieldsData);
return ['inputs' => $fields->all(), 'files' => $files];
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment