Last active
October 26, 2022 01:53
-
-
Save opiy-org/0367572317a68e2d5b4253db97495c62 to your computer and use it in GitHub Desktop.
Laravel middleware to fix form-data/multipart in PATCH, PUT requests
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 | |
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