Completed
Push — master ( 30ef78...8f8c1f )
by Niels
16s queued 10s
created

  A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 5
dl 0
loc 10
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 1
1
<?php
2
3
/*
4
 * This file is part of the OpenapiBundle package.
5
 *
6
 * (c) Niels Nijens <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Nijens\OpenapiBundle\EventListener;
13
14
use Exception;
15
use JsonSchema\Validator;
16
use Nijens\OpenapiBundle\Exception\BadJsonRequestHttpException;
17
use Nijens\OpenapiBundle\Exception\InvalidRequestHttpException;
18
use Nijens\OpenapiBundle\Json\JsonPointer;
19
use Nijens\OpenapiBundle\Json\SchemaLoaderInterface;
20
use Seld\JsonLint\JsonParser;
21
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
22
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
23
use Symfony\Component\HttpKernel\KernelEvents;
24
use Symfony\Component\Routing\Route;
25
use Symfony\Component\Routing\RouterInterface;
26
27
/**
28
 * Validates a JSON request body for routes loaded through the OpenAPI specification.
29
 *
30
 * @author Niels Nijens <[email protected]>
31
 */
32
class JsonRequestBodyValidationSubscriber implements EventSubscriberInterface
33
{
34
    /**
35
     * @var RouterInterface
36
     */
37
    private $router;
38
39
    /**
40
     * @var JsonParser
41
     */
42
    private $jsonParser;
43
44
    /**
45
     * @var SchemaLoaderInterface
46
     */
47
    private $schemaLoader;
48
49
    /**
50
     * @var Validator
51
     */
52
    private $jsonValidator;
53
54
    /**
55
     * {@inheritdoc}
56
     */
57
    public static function getSubscribedEvents(): array
58
    {
59
        return array(
60
            KernelEvents::REQUEST => array(
61
                array('validateRequestBody', 28),
62
            ),
63
        );
64
    }
65
66
    /**
67
     * Constructs a new JsonRequestBodyValidationSubscriber instance.
68
     *
69
     * @param RouterInterface       $router
70
     * @param JsonParser            $jsonParser
71
     * @param SchemaLoaderInterface $schemaLoader
72
     * @param Validator             $jsonValidator
73
     */
74
    public function __construct(
75
        RouterInterface $router,
76
        JsonParser $jsonParser,
77
        SchemaLoaderInterface $schemaLoader,
78
        Validator $jsonValidator
79
    ) {
80
        $this->router = $router;
81
        $this->jsonParser = $jsonParser;
82
        $this->schemaLoader = $schemaLoader;
83
        $this->jsonValidator = $jsonValidator;
84
    }
85
86
    /**
87
     * Validates the body of a request to an OpenAPI specification route. Throws an exception when validation failed.
88
     *
89
     * @param GetResponseEvent $event
90
     */
91
    public function validateRequestBody(GetResponseEvent $event): void
92
    {
93
        $request = $event->getRequest();
94
        $requestContentType = $request->headers->get('Content-Type');
95
96
        $route = $this->router->getRouteCollection()->get(
97
            $request->attributes->get('_route')
98
        );
99
100
        if ($route instanceof Route === false) {
101
            return;
102
        }
103
104
        if ($route->hasOption('openapi_resource') === false || $route->hasOption('openapi_json_request_validation_pointer') === false) {
105
            return;
106
        }
107
108
        if ($requestContentType !== 'application/json') {
109
            throw new BadJsonRequestHttpException("The request content-type should be 'application/json'.");
110
        }
111
112
        $requestBody = $request->getContent();
113
        $decodedJsonRequestBody = $this->validateJsonRequestBody($requestBody);
114
115
        $this->validateJsonAgainstSchema($route, $decodedJsonRequestBody);
116
    }
117
118
    /**
119
     * Validates if the request body is valid JSON.
120
     *
121
     * @param string $requestBody
122
     *
123
     * @return mixed
124
     */
125
    private function validateJsonRequestBody(string $requestBody)
126
    {
127
        $decodedJsonRequestBody = json_decode($requestBody);
128
        if ($decodedJsonRequestBody !== null || $requestBody === 'null') {
129
            return $decodedJsonRequestBody;
130
        }
131
132
        $exception = $this->jsonParser->lint($requestBody);
133
134
        throw new BadJsonRequestHttpException('The request body should be valid JSON.', $exception);
135
    }
136
137
    /**
138
     * Validates the JSON request body against the JSON Schema within the OpenAPI specification.
139
     *
140
     * @param Route $route
141
     * @param mixed $decodedJsonRequestBody
142
     */
143
    private function validateJsonAgainstSchema(Route $route, $decodedJsonRequestBody): void
144
    {
145
        $schema = $this->schemaLoader->load($route->getOption('openapi_resource'));
146
147
        $jsonPointer = new JsonPointer($schema);
148
        $jsonSchema = $jsonPointer->get($route->getOption('openapi_json_request_validation_pointer'));
149
150
        $this->jsonValidator->validate($decodedJsonRequestBody, $jsonSchema);
151
152
        if ($this->jsonValidator->isValid() === false) {
153
            $validationErrors = $this->jsonValidator->getErrors();
154
            $this->jsonValidator->reset();
155
156
            $this->throwInvalidRequestException($validationErrors);
157
        }
158
    }
159
160
    /**
161
     * @param array $errors
162
     */
163
    private function throwInvalidRequestException(array $errors): void
164
    {
165
        $errorMessages = array_map(
166
            function ($error) {
167
                return $error['message'];
168
            },
169
            $errors
170
        );
171
172
        $exception = new InvalidRequestHttpException('Validation of JSON request body failed.');
173
        $exception->setErrors($errorMessages);
174
175
        throw $exception;
176
    }
177
}
178