Test Failed
Push — master ( 9ceea8...5b7b5b )
by Daniel
09:29
created

ComponentVoter   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 134
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 63
c 1
b 0
f 0
dl 0
loc 134
rs 10
wmc 26

8 Methods

Rating   Name   Duplication   Size   Complexity  
A isRouteReachableResource() 0 5 1
B voteOnAttribute() 0 43 11
A getComponentPages() 0 13 4
A isPageDataReachableResource() 0 5 1
A isPathReachable() 0 21 3
A getComponentRoutesFromPages() 0 6 3
A __construct() 0 10 1
A supports() 0 3 2
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\Core\Api\IriConverterInterface;
17
use Silverback\ApiComponentsBundle\DataProvider\PageDataProvider;
18
use Silverback\ApiComponentsBundle\Entity\Core\AbstractComponent;
19
use Silverback\ApiComponentsBundle\Entity\Core\AbstractPageData;
20
use Silverback\ApiComponentsBundle\Entity\Core\Route;
21
use Symfony\Component\HttpFoundation\Request;
22
use Symfony\Component\HttpFoundation\RequestStack;
23
use Symfony\Component\HttpFoundation\Response;
24
use Symfony\Component\HttpKernel\HttpKernelInterface;
25
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
26
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
27
28
/**
29
 * @author Daniel West <[email protected]>
30
 */
31
class ComponentVoter extends Voter
32
{
33
    public const READ_COMPONENT = 'read_component';
34
35
    private PageDataProvider $pageDataProvider;
36
    private IriConverterInterface $iriConverter;
37
    private HttpKernelInterface $httpKernel;
38
    private RequestStack $requestStack;
39
40
    public function __construct(
41
        PageDataProvider $pageDataProvider,
42
        IriConverterInterface $iriConverter,
43
        HttpKernelInterface $httpKernel,
44
        RequestStack $requestStack
45
    ) {
46
        $this->pageDataProvider = $pageDataProvider;
47
        $this->iriConverter = $iriConverter;
48
        $this->httpKernel = $httpKernel;
49
        $this->requestStack = $requestStack;
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
        $pagesGenerator = $this->getComponentPages($subject);
68
        $pages = iterator_to_array($pagesGenerator);
69
70
        // Check if accessible via any route
71
        $routes = $this->getComponentRoutesFromPages($pages);
72
        $routeCount = 0;
73
        foreach ($routes as $route) {
74
            ++$routeCount;
75
            if ($this->isRouteReachableResource($route, $request)) {
76
                return true;
77
            }
78
        }
79
80
        // check if accessible via any page data
81
82
        // 1. as a page data property
83
        $pageData = $this->pageDataProvider->findPageDataComponentMetadata($subject);
84
        $pageDataCount = 0;
85
        foreach ($pageData as $pageDatum) {
86
            foreach ($pageDatum->getPageDataResources() as $pageDataResource) {
87
                ++$pageDataCount;
88
                if ($this->isPageDataReachableResource($pageDataResource, $request)) {
89
                    return true;
90
                }
91
            }
92
        }
93
94
        // 2. as a component in the page template being used by page data
95
        $pageDataByPagesComponentUsedIn = $this->pageDataProvider->findPageDataResourcesByPages($pages);
96
        foreach ($pageDataByPagesComponentUsedIn as $pageData) {
97
            if ($this->isPageDataReachableResource($pageData, $request)) {
98
                return true;
99
            }
100
        }
101
102
        return !$routeCount && !$pageDataCount && !\count($pageDataByPagesComponentUsedIn);
103
    }
104
105
    private function isRouteReachableResource(Route $route, Request $request): bool
106
    {
107
        $path = $this->iriConverter->getIriFromItem($route);
108
109
        return $this->isPathReachable($path, $request);
110
    }
111
112
    private function isPageDataReachableResource(AbstractPageData $pageData, Request $request): bool
113
    {
114
        $path = $this->iriConverter->getIriFromItem($pageData);
115
116
        return $this->isPathReachable($path, $request);
117
    }
118
119
    private function isPathReachable(string $path, Request $request): bool
120
    {
121
        $subRequest = Request::create(
122
            $path,
123
            Request::METHOD_GET,
124
            [],
125
            $request->cookies->all(),
126
            [],
127
            $request->server->all(),
128
            null
129
        );
130
131
        try {
132
            $this->httpKernel->handle($subRequest, HttpKernelInterface::SUB_REQUEST, false);
133
134
            return true;
135
        } catch (\Exception $e) {
136
            if (\in_array($e->getCode(), [Response::HTTP_UNAUTHORIZED, Response::HTTP_FORBIDDEN], true)) {
137
                return false;
138
            }
139
            throw $e;
140
        }
141
    }
142
143
    private function getComponentPages(AbstractComponent $component): iterable
144
    {
145
        $componentPositions = $component->getComponentPositions();
146
        if (!\count($componentPositions)) {
147
            return;
148
        }
149
150
        foreach ($componentPositions as $componentPosition) {
151
            $componentCollection = $componentPosition->componentCollection;
152
            foreach ($componentCollection->components as $parentComponent) {
153
                yield from $this->getComponentPages($parentComponent);
154
            }
155
            yield from $componentCollection->pages;
156
        }
157
    }
158
159
    private function getComponentRoutesFromPages(array $pages): iterable
160
    {
161
        foreach ($pages as $page) {
162
            $route = $page->getRoute();
163
            if ($route) {
164
                yield $route;
165
            }
166
        }
167
    }
168
}
169