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