Passed
Pull Request — main (#71)
by Niels
11:22
created

RequestBodyValidator::validate()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 41
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

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