getRequestBodySchemaFromRequest()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 4
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the OpenapiBundle package.
7
 *
8
 * (c) Niels Nijens <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Nijens\OpenapiBundle\Validation\RequestValidator;
15
16
use JsonSchema\Validator;
17
use Nijens\OpenapiBundle\ExceptionHandling\Exception\InvalidRequestBodyProblemException;
18
use Nijens\OpenapiBundle\ExceptionHandling\Exception\InvalidRequestProblemExceptionInterface;
19
use Nijens\OpenapiBundle\ExceptionHandling\Exception\ProblemException;
20
use Nijens\OpenapiBundle\ExceptionHandling\Exception\RequestProblemExceptionInterface;
21
use Nijens\OpenapiBundle\ExceptionHandling\Exception\Violation;
22
use Nijens\OpenapiBundle\Json\Reference;
23
use Nijens\OpenapiBundle\Routing\RouteContext;
24
use Nijens\OpenapiBundle\Validation\ValidationContext;
25
use Seld\JsonLint\JsonParser;
26
use stdClass;
27
use Symfony\Component\HttpFoundation\HeaderUtils;
28
use Symfony\Component\HttpFoundation\Request;
29
use Symfony\Component\HttpFoundation\Response;
30
31
/**
32
 * Validates a JSON request body with the JSON schema.
33
 *
34
 * @author Niels Nijens <[email protected]>
35
 */
36
final class RequestBodyValidator implements ValidatorInterface
37
{
38
    /**
39
     * @var JsonParser
40
     */
41
    private $jsonParser;
42
43
    /**
44
     * @var Validator
45
     */
46
    private $jsonValidator;
47
48
    public function __construct(JsonParser $jsonParser, Validator $jsonValidator)
49
    {
50
        $this->jsonParser = $jsonParser;
51
        $this->jsonValidator = $jsonValidator;
52
    }
53
54
    public function validate(Request $request): ?RequestProblemExceptionInterface
55
    {
56
        $requestContentType = $this->getRequestContentType($request);
57
        if ($requestContentType !== 'application/json') {
58
            return null;
59
        }
60
61
        $requestBodySchema = $this->getRequestBodySchemaFromRequest($request);
62
        if ($requestBodySchema === null) {
63
            return null;
64
        }
65
66
        $requestBody = $request->getContent();
67
        $decodedJsonRequestBody = null;
68
69
        $violations = $this->validateJsonSyntax($requestBody, $decodedJsonRequestBody);
70
        if (count($violations) > 0) {
71
            return $this->createInvalidRequestBodyProblemException(
72
                $violations,
73
                'The request body must be valid JSON.'
74
            );
75
        }
76
77
        $violations = array_merge(
78
            $violations,
79
            $this->validateJsonAgainstSchema(
80
                unserialize($requestBodySchema),
81
                $decodedJsonRequestBody
82
            )
83
        );
84
85
        if (count($violations) > 0) {
86
            return $this->createInvalidRequestBodyProblemException(
87
                $violations,
88
                'Validation of JSON request body failed.'
89
            );
90
        }
91
92
        $request->attributes->set(
93
            ValidationContext::REQUEST_ATTRIBUTE,
94
            [
95
                ValidationContext::VALIDATED => true,
96
                ValidationContext::REQUEST_BODY => json_encode($decodedJsonRequestBody),
97
            ]
98
        );
99
100
        return null;
101
    }
102
103
    private function getRequestContentType(Request $request): string
104
    {
105
        return current(HeaderUtils::split($request->headers->get('Content-Type', ''), ';')) ?: '';
106
    }
107
108
    private function getRequestBodySchemaFromRequest(Request $request): ?string
109
    {
110
        return $request->attributes
111
            ->get(RouteContext::REQUEST_ATTRIBUTE)[RouteContext::REQUEST_BODY_SCHEMA] ?? null;
112
    }
113
114
    /**
115
     * @param array|stdClass|string|int|float|bool|null $decodedJsonRequestBody
116
     *
117
     * @return Violation[]
118
     */
119
    private function validateJsonSyntax(string $requestBody, &$decodedJsonRequestBody): array
120
    {
121
        $decodedJsonRequestBody = json_decode($requestBody);
122
        if ($decodedJsonRequestBody !== null || $requestBody === 'null') {
123
            return [];
124
        }
125
126
        $exception = $this->jsonParser->lint($requestBody);
127
128
        return [
129
            new Violation('valid_json', $exception->getMessage()),
130
        ];
131
    }
132
133
    /**
134
     * @param stdClass|Reference                        $jsonSchema
135
     * @param array|stdClass|string|int|float|bool|null $decodedJsonRequestBody
136
     *
137
     * @return Violation[]
138
     */
139
    private function validateJsonAgainstSchema($jsonSchema, &$decodedJsonRequestBody): array
140
    {
141
        $this->jsonValidator->validate($decodedJsonRequestBody, $jsonSchema);
142
143
        if ($this->jsonValidator->isValid()) {
144
            return [];
145
        }
146
147
        $validationErrors = $this->jsonValidator->getErrors();
148
        $this->jsonValidator->reset();
149
150
        return array_map(
151
            function (array $validationError): Violation {
152
                return Violation::fromArray($validationError);
153
            },
154
            $validationErrors
155
        );
156
    }
157
158
    /**
159
     * @param Violation[] $violations
160
     */
161
    private function createInvalidRequestBodyProblemException(
162
        array $violations,
163
        string $message
164
    ): InvalidRequestProblemExceptionInterface {
165
        $exception = new InvalidRequestBodyProblemException(
166
            ProblemException::DEFAULT_TYPE_URI,
167
            'The request body contains errors.',
168
            Response::HTTP_BAD_REQUEST,
169
            $message
170
        );
171
172
        return $exception->withViolations($violations);
173
    }
174
}
175