Skip to content

Commit

Permalink
Merge pull request #2798 from akrabat/body-parsing-middleware
Browse files Browse the repository at this point in the history
Add BodyParsingMiddleware
  • Loading branch information
l0gicgate committed Aug 15, 2019
2 parents 86a22f7 + 84fe283 commit 907d149
Show file tree
Hide file tree
Showing 5 changed files with 433 additions and 2 deletions.
19 changes: 17 additions & 2 deletions Slim/App.php
Expand Up @@ -19,6 +19,7 @@
use Slim\Interfaces\CallableResolverInterface;
use Slim\Interfaces\RouteCollectorInterface;
use Slim\Interfaces\RouteResolverInterface;
use Slim\Middleware\BodyParsingMiddleware;
use Slim\Middleware\ErrorMiddleware;
use Slim\Middleware\RoutingMiddleware;
use Slim\Routing\RouteCollectorProxy;
Expand Down Expand Up @@ -100,7 +101,7 @@ public function addMiddleware(MiddlewareInterface $middleware): self
}

/**
* Add the slim built-in routing middleware to the app middleware stack
* Add the Slim built-in routing middleware to the app middleware stack
*
* @return RoutingMiddleware
*/
Expand All @@ -115,7 +116,7 @@ public function addRoutingMiddleware(): RoutingMiddleware
}

/**
* Add the slim built-in error middleware to the app middleware stack
* Add the Slim built-in error middleware to the app middleware stack
*
* @param bool $displayErrorDetails
* @param bool $logErrors
Expand All @@ -139,6 +140,20 @@ public function addErrorMiddleware(
return $errorMiddleware;
}

/**
* Add the Slim body parsing middleware to the app middleware stack
*
* @param callable[] $bodyParsers
*
* @return BodyParsingMiddleware
*/
public function addBodyParsingMiddleware(array $bodyParsers = []): BodyParsingMiddleware
{
$bodyParsingMiddleware = new BodyParsingMiddleware($bodyParsers);
$this->add($bodyParsingMiddleware);
return $bodyParsingMiddleware;
}

/**
* Run application
*
Expand Down
175 changes: 175 additions & 0 deletions Slim/Middleware/BodyParsingMiddleware.php
@@ -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);
}
return $this->bodyParsers[$mediaType];
}


protected function registerDefaultBodyParsers(): void
{
$this->registerBodyParser('application/json', function ($input) {
$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];
}
}

if (isset($this->bodyParsers[$mediaType])) {
$body = (string)$request->getBody();
$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;
}
}
2 changes: 2 additions & 0 deletions composer.json
Expand Up @@ -73,6 +73,8 @@
"phpstan": "php -d memory_limit=-1 vendor/bin/phpstan analyse Slim"
},
"suggest": {
"ext-simplexml": "Needed to support XML format in BodyParsingMiddleware",
"ext-xml": "Needed to support XML format in BodyParsingMiddleware",
"slim/psr7": "Slim PSR-7 implementation. See http://www.slimframework.com/docs/v4/start/installation.html for more information."
}
}
31 changes: 31 additions & 0 deletions tests/AppTest.php
Expand Up @@ -28,6 +28,7 @@
use Slim\Interfaces\RouteCollectorInterface;
use Slim\Interfaces\RouteCollectorProxyInterface;
use Slim\Interfaces\RouteParserInterface;
use Slim\Middleware\BodyParsingMiddleware;
use Slim\Middleware\ErrorMiddleware;
use Slim\Middleware\RoutingMiddleware;
use Slim\MiddlewareDispatcher;
Expand Down Expand Up @@ -714,6 +715,36 @@ public function testAddErrorMiddleware()
$this->assertInstanceOf(ErrorMiddleware::class, $errorMiddleware);
}

public function testAddBodyParsingMiddleware()
{
/** @var ResponseFactoryInterface $responseFactory */
$responseFactory = $this->prophesize(ResponseFactoryInterface::class)->reveal();

// Create the app.
$app = new App($responseFactory);

// Add the error middleware.
$bodyParsingMiddleware = $app->addBodyParsingMiddleware();

// Check that the body parsing middleware really has been added to the tip of the app middleware stack.
$middlewareDispatcherProperty = new \ReflectionProperty(App::class, 'middlewareDispatcher');
$middlewareDispatcherProperty->setAccessible(true);
/** @var MiddlewareDispatcher $middlewareDispatcher */
$middlewareDispatcher = $middlewareDispatcherProperty->getValue($app);

$tipProperty = new \ReflectionProperty(MiddlewareDispatcher::class, 'tip');
$tipProperty->setAccessible(true);
/** @var RequestHandlerInterface $tip */
$tip = $tipProperty->getValue($middlewareDispatcher);

$reflection = new \ReflectionClass($tip);
$middlewareProperty = $reflection->getProperty('middleware');
$middlewareProperty->setAccessible(true);

$this->assertSame($bodyParsingMiddleware, $middlewareProperty->getValue($tip));
$this->assertInstanceOf(BodyParsingMiddleware::class, $bodyParsingMiddleware);
}

public function testAddMiddlewareOnRoute()
{
$output = '';
Expand Down

0 comments on commit 907d149

Please sign in to comment.