-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add BodyParsingMiddleware #2798
Changes from all commits
b573a00
d812554
14a1e99
ec0243a
3e973d7
b682dca
093017e
50ca8e8
8af2dae
84fe283
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
<?php | ||
/** | ||
* Slim Framework (https://slimframework.com) | ||
* | ||
* @license https://github.com/slimphp/Slim/blob/4.x/LICENSE.md (MIT License) | ||
*/ | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Slim\Middleware; | ||
|
||
use Psr\Http\Message\ResponseInterface; | ||
use Psr\Http\Message\ServerRequestInterface; | ||
use Psr\Http\Server\MiddlewareInterface; | ||
use Psr\Http\Server\RequestHandlerInterface; | ||
use RuntimeException; | ||
|
||
class BodyParsingMiddleware implements MiddlewareInterface | ||
{ | ||
/** | ||
* @var callable[] | ||
*/ | ||
protected $bodyParsers; | ||
|
||
/** | ||
* @param callable[] $bodyParsers list of body parsers as an associative array of mediaType => callable | ||
*/ | ||
public function __construct(array $bodyParsers = []) | ||
{ | ||
$this->registerDefaultBodyParsers(); | ||
|
||
foreach ($bodyParsers as $mediaType => $parser) { | ||
$this->registerBodyParser($mediaType, $parser); | ||
} | ||
} | ||
|
||
/** | ||
* @param ServerRequestInterface $request | ||
* @param RequestHandlerInterface $handler | ||
* @return ResponseInterface | ||
*/ | ||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface | ||
{ | ||
$parsedBody = $request->getParsedBody(); | ||
if ($parsedBody === null || empty($parsedBody)) { | ||
$parsedBody = $this->parseBody($request); | ||
$request = $request->withParsedBody($parsedBody); | ||
} | ||
|
||
return $handler->handle($request); | ||
} | ||
|
||
/** | ||
* @param string $mediaType A HTTP media type (excluding content-type params). | ||
* @param callable $callable A callable that returns parsed contents for media type. | ||
* @return self | ||
*/ | ||
public function registerBodyParser(string $mediaType, callable $callable): self | ||
{ | ||
$this->bodyParsers[$mediaType] = $callable; | ||
return $this; | ||
} | ||
|
||
/** | ||
* @param string $mediaType A HTTP media type (excluding content-type params). | ||
* @return boolean | ||
*/ | ||
public function hasBodyParser(string $mediaType): bool | ||
{ | ||
return isset($this->bodyParsers[$mediaType]); | ||
} | ||
|
||
/** | ||
* @param string $mediaType A HTTP media type (excluding content-type params). | ||
* @return callable | ||
* @throws RuntimeException | ||
*/ | ||
public function getBodyParser(string $mediaType): callable | ||
{ | ||
if (!isset($this->bodyParsers[$mediaType])) { | ||
throw new RuntimeException('No parser for type ' . $mediaType); | ||
akrabat marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
return $this->bodyParsers[$mediaType]; | ||
} | ||
|
||
|
||
protected function registerDefaultBodyParsers(): void | ||
{ | ||
$this->registerBodyParser('application/json', function ($input) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is fine for basic applications, but what about users of JSON-API that use |
||
$result = json_decode($input, true); | ||
|
||
if (!is_array($result)) { | ||
return null; | ||
} | ||
|
||
return $result; | ||
}); | ||
|
||
$this->registerBodyParser('application/x-www-form-urlencoded', function ($input) { | ||
parse_str($input, $data); | ||
return $data; | ||
}); | ||
|
||
$xmlCallable = function ($input) { | ||
$backup = libxml_disable_entity_loader(true); | ||
$backup_errors = libxml_use_internal_errors(true); | ||
$result = simplexml_load_string($input); | ||
|
||
libxml_disable_entity_loader($backup); | ||
libxml_clear_errors(); | ||
libxml_use_internal_errors($backup_errors); | ||
|
||
if ($result === false) { | ||
return null; | ||
} | ||
|
||
return $result; | ||
}; | ||
|
||
$this->registerBodyParser('application/xml', $xmlCallable); | ||
$this->registerBodyParser('text/xml', $xmlCallable); | ||
} | ||
|
||
/** | ||
* @param ServerRequestInterface $request | ||
* @return null|array|object | ||
*/ | ||
protected function parseBody(ServerRequestInterface $request) | ||
{ | ||
$mediaType = $this->getMediaType($request); | ||
if ($mediaType === null) { | ||
return null; | ||
} | ||
|
||
// Check if this specific media type has a parser registered first | ||
if (!isset($this->bodyParsers[$mediaType])) { | ||
// If not, look for a media type with a structured syntax suffix (RFC 6839) | ||
$parts = explode('+', $mediaType); | ||
if (count($parts) >= 2) { | ||
$mediaType = 'application/' . $parts[count($parts) - 1]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay, I get that this is supposed to handle |
||
} | ||
} | ||
|
||
if (isset($this->bodyParsers[$mediaType])) { | ||
$body = (string)$request->getBody(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if the stream is not seekable (rewindable)? One solution would be to simply copy the non-seekable stream to a new seekable stream. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If the stream is not rewindable, then there's no guarantee that the data at the beginning is anywhere in memory now, so I don't see any advantage to copying to seekable stream? At the end of the day, I think that if you're using a non-rewindable string in your ServerRequest, then you're incompatible with getParsedBody() and won't need or want this middleware. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
According to PSR-7 it is not a requirement to attempt to rewind the stream after the data has been read by
Well most often you will see something like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Right. I see what you're saying now. The problem is that we can't copy the stream as there's no method in Thoughts? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A further note. It looks like this has to be solved at the PSR-7 implementation level. e.g. Diactoros's PhpInputStream which is then used by ServerRequest. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @anfly0 I'm confused trying to do this. If the stream is not seeable, then we can't rewind it and hence can't parse it in this middleware. Have I missed something? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So maybe I copy the stream if it's not seekable and There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @akrabat I'm no 100 on this, but I think that One idea is to copy the stream if it's not seekable and then compare the size of the new stream with what's in the Content-Length header. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How do we get a non-seeable stream into a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure I understand the question. |
||
$parsed = $this->bodyParsers[$mediaType]($body); | ||
|
||
if (!is_null($parsed) && !is_object($parsed) && !is_array($parsed)) { | ||
throw new RuntimeException( | ||
'Request body media type parser return value must be an array, an object, or null' | ||
); | ||
} | ||
|
||
return $parsed; | ||
} | ||
|
||
return null; | ||
} | ||
|
||
/** | ||
* @param ServerRequestInterface $request | ||
* @return string|null The serverRequest media type, minus content-type params | ||
*/ | ||
protected function getMediaType(ServerRequestInterface $request): ?string | ||
{ | ||
$contentType = $request->getHeader('Content-Type')[0] ?? null; | ||
|
||
if (is_string($contentType) && trim($contentType) != '') { | ||
$contentTypeParts = explode(';', $contentType); | ||
return strtolower(trim($contentTypeParts[0])); | ||
} | ||
|
||
return null; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather than a generic
callable
, why not create an interface?Then you can do:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't disagree with you on this. This to me feels like it should be extracted in its own package if we're going to go that route so we can contain the files contextually. The middleware we ship right now are self-contained into one file.