ComponentVoter   B
last analyzed

Complexity

Total Complexity 43

Size/Duplication

Total Lines 192
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 5
Bugs 2 Features 0
Metric Value
eloc 87
c 5
b 2
f 0
dl 0
loc 192
ccs 0
cts 98
cp 0
rs 8.96
wmc 43

12 Methods

Rating   Name   Duplication   Size   Complexity  
A supports() 0 3 2
B voteOnAttribute() 0 32 7
A __construct() 0 9 1
A isRouteReachableResource() 0 5 1
A voteByPageTemplate() 0 13 5
A voteByPageData() 0 14 5
A isPageDataReachableResource() 0 5 1
A getComponentPages() 0 16 5
A getComponentRoutesFromPages() 0 6 3
A voteByRoute() 0 12 4
A isPathReachable() 0 29 5
A getPublishedSubject() 0 15 4

How to fix   Complexity   

Complex Class

Complex classes like ComponentVoter often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ComponentVoter, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
 * This file is part of the Silverback API Components Bundle Project
5
 *
6
 * (c) Daniel West <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace Silverback\ApiComponentsBundle\Security\Voter;
15
16
use ApiPlatform\Metadata\IriConverterInterface;
17
use Doctrine\Persistence\ManagerRegistry;
18
use Silverback\ApiComponentsBundle\DataProvider\PageDataProvider;
19
use Silverback\ApiComponentsBundle\Entity\Core\AbstractComponent;
20
use Silverback\ApiComponentsBundle\Entity\Core\AbstractPageData;
21
use Silverback\ApiComponentsBundle\Entity\Core\Route;
22
use Silverback\ApiComponentsBundle\Helper\Publishable\PublishableStatusChecker;
23
use Silverback\ApiComponentsBundle\Utility\ClassMetadataTrait;
24
use Symfony\Component\HttpFoundation\Request;
25
use Symfony\Component\HttpFoundation\RequestStack;
26
use Symfony\Component\HttpFoundation\Response;
27
use Symfony\Component\HttpKernel\HttpKernelInterface;
28
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
29
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
30
use Symfony\Component\Serializer\Exception\NotEncodableValueException;
31
32
/**
33
 * @author Daniel West <[email protected]>
34
 */
35
class ComponentVoter extends Voter
36
{
37
    use ClassMetadataTrait;
38
39
    public const READ_COMPONENT = 'read_component';
40
41
    public function __construct(
42
        private readonly PageDataProvider $pageDataProvider,
43
        private readonly IriConverterInterface $iriConverter,
44
        private readonly HttpKernelInterface $httpKernel,
45
        private readonly RequestStack $requestStack,
46
        private readonly PublishableStatusChecker $publishableStatusChecker,
47
        ManagerRegistry $registry
48
    ) {
49
        $this->initRegistry($registry);
50
    }
51
52
    protected function supports($attribute, $subject): bool
53
    {
54
        return self::READ_COMPONENT === $attribute && $subject instanceof AbstractComponent;
55
    }
56
57
    /**
58
     * @param AbstractComponent $subject
59
     */
60
    protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
61
    {
62
        $request = $this->requestStack->getCurrentRequest();
63
        if (!$request) {
64
            return true;
65
        }
66
67
        $subject = $this->getPublishedSubject($subject);
68
69
        $pagesGenerator = $this->getComponentPages($subject);
70
        $pages = iterator_to_array($pagesGenerator);
71
72
        // 1. Check if accessible via any route
73
        $routeVoteResult = $this->voteByRoute($pages, $request);
74
        if ($routeVoteResult) {
75
            return true;
76
        }
77
78
        // 2. as a page data property
79
        $pageDataResult = $this->voteByPageData($subject, $request);
80
        if ($pageDataResult) {
81
            return true;
82
        }
83
84
        // 3. as a component in the page template being used by page data
85
        $pageTemplateResult = $this->voteByPageTemplate($pages, $request);
86
        if ($pageTemplateResult) {
87
            return true;
88
        }
89
90
        // vote is ok if all sub votes abstain
91
        return null === $routeVoteResult && null === $pageDataResult && null === $pageTemplateResult;
92
    }
93
94
    private function voteByPageTemplate($pages, Request $request): ?bool
95
    {
96
        if (!\count($pages)) {
97
            return null;
98
        }
99
        $pageDataByPagesComponentUsedIn = $this->pageDataProvider->findPageDataResourcesByPages($pages);
100
        foreach ($pageDataByPagesComponentUsedIn as $pageData) {
101
            if ($this->isPageDataReachableResource($pageData, $request)) {
102
                return true;
103
            }
104
        }
105
106
        return \count($pageDataByPagesComponentUsedIn) ? false : null;
107
    }
108
109
    private function voteByPageData($subject, Request $request): ?bool
110
    {
111
        $pageData = $this->pageDataProvider->findPageDataComponentMetadata($subject);
112
        $pageDataCount = 0;
113
        foreach ($pageData as $pageDatum) {
114
            foreach ($pageDatum->getPageDataResources() as $pageDataResource) {
115
                ++$pageDataCount;
116
                if ($this->isPageDataReachableResource($pageDataResource, $request)) {
117
                    return true;
118
                }
119
            }
120
        }
121
122
        return $pageDataCount ? false : null;
123
    }
124
125
    private function voteByRoute($pages, Request $request): ?bool
126
    {
127
        $routes = $this->getComponentRoutesFromPages($pages);
128
        $routeCount = 0;
129
        foreach ($routes as $route) {
130
            ++$routeCount;
131
            if ($this->isRouteReachableResource($route, $request)) {
132
                return true;
133
            }
134
        }
135
136
        return $routeCount ? false : null;
137
    }
138
139
    private function getPublishedSubject($subject)
140
    {
141
        // is a draft publishable. If a published version is available we should be checking the published version to see if it is in an accessible location
142
        $publishableAttributeReader = $this->publishableStatusChecker->getAttributeReader();
143
        if ($publishableAttributeReader->isConfigured($subject) && !$this->publishableStatusChecker->isActivePublishedAt($subject)) {
144
            $configuration = $publishableAttributeReader->getConfiguration($subject);
145
            $classMetadata = $this->getClassMetadata($subject);
146
147
            $publishedResourceAssociation = $classMetadata->getFieldValue($subject, $configuration->associationName);
148
            if ($publishedResourceAssociation) {
149
                return $publishedResourceAssociation;
150
            }
151
        }
152
153
        return $subject;
154
    }
155
156
    private function isRouteReachableResource(Route $route, Request $request): bool
157
    {
158
        $path = $this->iriConverter->getIriFromResource($route);
159
160
        return $this->isPathReachable($path, $request);
0 ignored issues
show
Bug introduced by
It seems like $path can also be of type null; however, parameter $path of Silverback\ApiComponents...oter::isPathReachable() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

160
        return $this->isPathReachable(/** @scrutinizer ignore-type */ $path, $request);
Loading history...
161
    }
162
163
    private function isPageDataReachableResource(AbstractPageData $pageData, Request $request): bool
164
    {
165
        $path = $this->iriConverter->getIriFromResource($pageData);
166
167
        return $this->isPathReachable($path, $request);
0 ignored issues
show
Bug introduced by
It seems like $path can also be of type null; however, parameter $path of Silverback\ApiComponents...oter::isPathReachable() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

167
        return $this->isPathReachable(/** @scrutinizer ignore-type */ $path, $request);
Loading history...
168
    }
169
170
    private function isPathReachable(string $path, Request $request): bool
171
    {
172
        $serverVars = $request->server->all();
173
        if (isset($serverVars['HTTP_ACCEPT'])) {
174
            $serverVars['HTTP_ACCEPT'] = 'application/ld+json,application/json,text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8';
175
        }
176
        $subRequest = Request::create(
177
            $path,
178
            Request::METHOD_GET,
179
            [],
180
            $request->cookies->all(),
181
            [],
182
            $serverVars,
183
            null
184
        );
185
186
        try {
187
            $this->httpKernel->handle($subRequest, HttpKernelInterface::SUB_REQUEST, false);
188
189
            return true;
190
        } catch (\Exception $e) {
191
            // unsupported format requested
192
            if ($e instanceof NotEncodableValueException) {
193
                return false;
194
            }
195
            if (\in_array($e->getCode(), [Response::HTTP_UNAUTHORIZED, Response::HTTP_FORBIDDEN], true)) {
196
                return false;
197
            }
198
            throw $e;
199
        }
200
    }
201
202
    private function getComponentPages(AbstractComponent $component): \Traversable
203
    {
204
        $componentPositions = $component->getComponentPositions();
205
        if (!\count($componentPositions)) {
206
            return;
207
        }
208
209
        foreach ($componentPositions as $componentPosition) {
210
            $componentGroup = $componentPosition->componentGroup;
211
            foreach ($componentGroup->layouts as $layout) {
212
                yield from $layout->pages;
213
            }
214
            foreach ($componentGroup->components as $parentComponent) {
215
                yield from $this->getComponentPages($parentComponent);
216
            }
217
            yield from $componentGroup->pages;
218
        }
219
    }
220
221
    private function getComponentRoutesFromPages(array $pages): iterable
222
    {
223
        foreach ($pages as $page) {
224
            $route = $page->getRoute();
225
            if ($route) {
226
                yield $route;
227
            }
228
        }
229
    }
230
}
231