RestRequestListener::decodeBody()   B
last analyzed

Complexity

Conditions 6
Paths 5

Size

Total Lines 35

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 6.2163

Importance

Changes 0
Metric Value
dl 0
loc 35
ccs 18
cts 22
cp 0.8182
rs 8.7377
c 0
b 0
f 0
cc 6
nc 5
nop 2
crap 6.2163
1
<?php
2
declare(strict_types=1);
3
4
namespace Paysera\Bundle\ApiBundle\Listener;
5
6
use Paysera\Bundle\ApiBundle\Entity\RestRequestOptions;
7
use Paysera\Bundle\ApiBundle\Exception\ForbiddenApiException;
8
use Paysera\Bundle\ApiBundle\Exception\NotFoundApiException;
9
use Paysera\Bundle\ApiBundle\Service\ContentTypeMatcher;
10
use Paysera\Bundle\ApiBundle\Service\PathAttributeResolver\PathAttributeResolutionManager;
11
use Paysera\Bundle\ApiBundle\Service\RestRequestHelper;
12
use Paysera\Bundle\ApiBundle\Service\Validation\EntityValidator;
13
use Paysera\Component\Normalization\CoreDenormalizer;
14
use Paysera\Component\Normalization\DenormalizationContext;
15
use Paysera\Component\ObjectWrapper\Exception\InvalidItemException;
16
use Symfony\Component\HttpFoundation\Request;
17
use Paysera\Component\Normalization\Exception\InvalidDataException;
18
use Paysera\Bundle\ApiBundle\Exception\ApiException;
19
use Symfony\Component\HttpKernel\Event\ControllerEvent;
20
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
21
use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken;
22
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
23
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
24
use stdClass;
25
use Exception;
26
27
/**
28
 * @internal
29
 */
30
class RestRequestListener
31
{
32
    private $coreDenormalizer;
33
    private $authorizationChecker;
34
    private $tokenStorage;
35
    private $requestHelper;
36
    private $entityValidator;
37
    private $contentTypeMatcher;
38
    private $pathAttributeResolutionManager;
39
40 96
    public function __construct(
41
        CoreDenormalizer $coreDenormalizer,
42
        AuthorizationCheckerInterface $authorizationChecker,
43
        TokenStorageInterface $tokenStorage,
44
        RestRequestHelper $requestHelper,
45
        EntityValidator $entityValidator,
46
        ContentTypeMatcher $contentTypeMatcher,
47
        PathAttributeResolutionManager $pathAttributeResolutionManager
48
    ) {
49 96
        $this->coreDenormalizer = $coreDenormalizer;
50 96
        $this->authorizationChecker = $authorizationChecker;
51 96
        $this->tokenStorage = $tokenStorage;
52 96
        $this->requestHelper = $requestHelper;
53 96
        $this->entityValidator = $entityValidator;
54 96
        $this->contentTypeMatcher = $contentTypeMatcher;
55 96
        $this->pathAttributeResolutionManager = $pathAttributeResolutionManager;
56 96
    }
57
58
    /**
59
     * Ran on kernel.controller event
60
     *
61
     * Both events are typecasted as one is deprecated from 4.3, but another not available before this version
62
     * @param FilterControllerEvent|ControllerEvent $event
63
     *
64
     * @throws ApiException
65
     * @throws InvalidDataException
66
     * @throws Exception
67
     */
68 96
    public function onKernelController($event)
69
    {
70 96
        $request = $event->getRequest();
71
72 96
        $options = $this->requestHelper->resolveRestRequestOptions($request, $event->getController());
73 96
        if ($options === null) {
74 2
            return;
75
        }
76
77 94
        $this->requestHelper->setOptionsForRequest($request, $options);
78
79 94
        $this->checkRequiredPermissions($options);
80
81 87
        $this->addAttributesFromURL($request, $options);
82 85
        $this->addAttributesFromQuery($request, $options);
83 78
        $this->addAttributesFromBody($request, $options);
84 61
    }
85
86 94
    private function checkRequiredPermissions(RestRequestOptions $options)
87
    {
88 94
        if (count($options->getRequiredPermissions()) === 0) {
89 83
            return;
90
        }
91
92 11
        $token = $this->tokenStorage->getToken();
93 11
        if ($token === null || $token instanceof AnonymousToken) {
94 4
            $exception = new ApiException(
95 4
                ApiException::UNAUTHORIZED,
96 4
                'This API endpoint requires authentication, none found'
97
            );
98
        } else {
99 7
            $exception = new ForbiddenApiException(
100 7
                'Access to this API endpoint is forbidden for current client'
101
            );
102
        }
103
104 11
        foreach ($options->getRequiredPermissions() as $permission) {
105 11
            if (!$this->authorizationChecker->isGranted($permission)) {
106 11
                throw $exception;
107
            }
108
        }
109 4
    }
110
111 87
    private function addAttributesFromURL(Request $request, RestRequestOptions $options)
112
    {
113 87
        foreach ($options->getPathAttributeResolverOptionsList() as $pathResolverOptions) {
114 10
            $attributeValue = $request->attributes->get($pathResolverOptions->getPathPartName());
115
116 10
            $value = $attributeValue !== null ? $this->pathAttributeResolutionManager->resolvePathAttribute(
117 8
                $attributeValue,
118 8
                $pathResolverOptions->getPathAttributeResolverType()
119 10
            ) : null;
120
121 10
            if ($value !== null) {
122 7
                $request->attributes->set($pathResolverOptions->getParameterName(), $value);
123 7
                continue;
124
            }
125
126 5
            if ($pathResolverOptions->isResolutionMandatory()) {
127 5
                throw new NotFoundApiException('Resource was not found');
128
            }
129
        }
130 85
    }
131
132
    /**
133
     * Handle request with request query mapper
134
     *
135
     * @param Request $request
136
     * @param RestRequestOptions $options
137
     *
138
     * @throws ApiException
139
     * @throws InvalidItemException
140
     */
141 85
    private function addAttributesFromQuery(Request $request, RestRequestOptions $options)
142
    {
143 85
        foreach ($options->getQueryResolverOptionsList() as $queryResolverOptions) {
144 32
            $context = new DenormalizationContext(
145 32
                $this->coreDenormalizer,
146 32
                $queryResolverOptions->getDenormalizationGroup()
147
            );
148 32
            $value = $this->coreDenormalizer->denormalize(
149 32
                $this->convertToObject($request->query->all()),
150 32
                $queryResolverOptions->getDenormalizationType(),
151 32
                $context
152
            );
153
154 29
            if ($queryResolverOptions->isValidationNeeded()) {
155 27
                $this->entityValidator->validate(
156 27
                    $value,
157 27
                    $queryResolverOptions->getValidationOptions()
158
                );
159
            }
160
161 27
            $request->attributes->set($queryResolverOptions->getParameterName(), $value);
162
        }
163 78
    }
164
165 32
    private function convertToObject(array $query): stdClass
166
    {
167 32
        return (object)json_decode(json_encode($query));
168
    }
169
170
    /**
171
     * Handles request with request mapper
172
     *
173
     * @param Request $request
174
     * @param RestRequestOptions $options
175
     *
176
     * @throws ApiException
177
     * @throws InvalidItemException
178
     */
179 78
    private function addAttributesFromBody(Request $request, RestRequestOptions $options)
180
    {
181 78
        if (!$options->hasBodyDenormalization()) {
182 42
            return;
183
        }
184
185 36
        $data = $this->decodeBody($request, $options);
186 31
        if ($data === null) {
187 5
            $this->handleEmptyRequestBody($options);
188 2
            return;
189
        }
190
191 26
        $context = new DenormalizationContext($this->coreDenormalizer, $options->getBodyDenormalizationGroup());
192 26
        $entity = $this->coreDenormalizer->denormalize(
193 26
            $data,
194 26
            $options->getBodyDenormalizationType(),
195 26
            $context
196
        );
197
198 25
        if ($options->isBodyValidationNeeded()) {
199 22
            $this->entityValidator->validate(
200 22
                $entity,
201 22
                $options->getBodyValidationOptions()
202
            );
203
        }
204
205 17
        $request->attributes->set($options->getBodyParameterName(), $entity);
206 17
    }
207
208 36
    private function decodeBody(Request $request, RestRequestOptions $options)
209
    {
210 36
        $content = $request->getContent();
211 36
        if ($content === '') {
212 5
            return null;
213
        }
214
215 31
        $contentType = $request->headers->get('CONTENT_TYPE', '');
216 31
        $contentTypeSupported = $this->contentTypeMatcher->isContentTypeSupported(
217 31
            $contentType,
218 31
            $options->getSupportedRequestContentTypes()
219
        );
220 31
        if (!$contentTypeSupported) {
221 5
            throw new ApiException(
222 5
                ApiException::INVALID_REQUEST,
223 5
                $contentType === ''
224
                    ? 'Content-Type must be provided'
225 5
                    : sprintf('This Content-Type (%s) is not supported', $contentType)
226
            );
227
        }
228
229 26
        if (!$options->isJsonEncodedBody()) {
230 4
            return $content;
231
        }
232
233 22
        $data = json_decode($content);
234 22
        if (json_last_error() !== JSON_ERROR_NONE) {
235
            throw new ApiException(
236
                ApiException::INVALID_REQUEST,
237
                'Cannot decode request body to JSON'
238
            );
239
        }
240
241 22
        return $data;
242
    }
243
244 5
    private function handleEmptyRequestBody(RestRequestOptions $options)
245
    {
246 5
        if (!$options->isBodyOptional()) {
247 3
            throw new ApiException(ApiException::INVALID_REQUEST, 'Expected non-empty request body');
248
        }
249 2
    }
250
}
251