Test Setup Failed
Pull Request — master (#5)
by
unknown
24:03
created

RestRequestListener   A

Complexity

Total Complexity 35

Size/Duplication

Total Lines 257
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 18

Test Coverage

Coverage 96.3%

Importance

Changes 0
Metric Value
wmc 35
lcom 1
cbo 18
dl 0
loc 257
ccs 104
cts 108
cp 0.963
rs 9.6
c 0
b 0
f 0

11 Methods

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