Skip to content

Commit 0b23744

Browse files
committed
Refactor and fix ComponentVoter to check published resource if available
1 parent 1012d11 commit 0b23744

File tree

13 files changed

+106
-60
lines changed

13 files changed

+106
-60
lines changed

src/Action/Uploadable/UploadAction.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public function __invoke(?object $data, Request $request, UploadableFileManager
5454
* if it IS currently published
5555
* if the user DOES have permission.
5656
*/
57-
$publishableAnnotationReader = $publishableStatusChecker->getAnnotationReader();
57+
$publishableAnnotationReader = $publishableStatusChecker->getAttributeReader();
5858
if ($publishableAnnotationReader->isConfigured($resource)) {
5959
$configuration = $publishableAnnotationReader->getConfiguration($resource);
6060
$isGranted = $publishableStatusChecker->isGranted($resource);

src/ApiPlatform/Api/MercureIriConverter.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public function getIriFromResource($resource, int $referenceType = UrlGeneratorI
4040
return $iri;
4141
}
4242

43-
if ($this->publishableStatusChecker->getAnnotationReader()->isConfigured($resource) && !$this->publishableStatusChecker->isActivePublishedAt($resource)) {
43+
if ($this->publishableStatusChecker->getAttributeReader()->isConfigured($resource) && !$this->publishableStatusChecker->isActivePublishedAt($resource)) {
4444
$iri .= '?draft=1';
4545
}
4646

src/Doctrine/Extension/ORM/PublishableExtension.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,10 +133,10 @@ private function updateQueryBuilderForUnauthorizedUsers(QueryBuilder $queryBuild
133133

134134
private function getConfiguration(string $resourceClass): ?Publishable
135135
{
136-
if (!$this->publishableStatusChecker->getAnnotationReader()->isConfigured($resourceClass)) {
136+
if (!$this->publishableStatusChecker->getAttributeReader()->isConfigured($resourceClass)) {
137137
return null;
138138
}
139139

140-
return $this->publishableStatusChecker->getAnnotationReader()->getConfiguration($resourceClass);
140+
return $this->publishableStatusChecker->getAttributeReader()->getConfiguration($resourceClass);
141141
}
142142
}

src/EventListener/Api/PublishableEventListener.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ final class PublishableEventListener
4545

4646
public function __construct(PublishableStatusChecker $publishableStatusChecker, ManagerRegistry $registry, ValidatorInterface $validator)
4747
{
48-
$this->publishableAnnotationReader = $publishableStatusChecker->getAnnotationReader();
48+
$this->publishableAnnotationReader = $publishableStatusChecker->getAttributeReader();
4949
$this->publishableStatusChecker = $publishableStatusChecker;
5050
$this->initRegistry($registry);
5151
$this->validator = $validator;

src/Helper/Publishable/PublishableStatusChecker.php

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,15 @@ class PublishableStatusChecker
2929
{
3030
use ClassMetadataTrait;
3131

32-
private PublishableAttributeReader $annotationReader;
33-
private AuthorizationCheckerInterface $authorizationChecker;
3432
private string $permission;
3533

36-
public function __construct(ManagerRegistry $registry, PublishableAttributeReader $annotationReader, AuthorizationCheckerInterface $authorizationChecker, string $permission)
37-
{
34+
public function __construct(
35+
ManagerRegistry $registry,
36+
private readonly PublishableAttributeReader $attributeReader,
37+
private readonly AuthorizationCheckerInterface $authorizationChecker,
38+
string $permission
39+
) {
3840
$this->initRegistry($registry);
39-
$this->annotationReader = $annotationReader;
40-
$this->authorizationChecker = $authorizationChecker;
4141
$this->permission = $permission;
4242
}
4343

@@ -47,39 +47,39 @@ public function __construct(ManagerRegistry $registry, PublishableAttributeReade
4747
public function isGranted($class): bool
4848
{
4949
try {
50-
return $this->authorizationChecker->isGranted(new Expression($this->annotationReader->getConfiguration($class)->isGranted ?? $this->permission));
50+
return $this->authorizationChecker->isGranted(new Expression($this->attributeReader->getConfiguration($class)->isGranted ?? $this->permission));
5151
} catch (AuthenticationCredentialsNotFoundException $e) {
5252
return false;
5353
}
5454
}
5555

5656
public function isActivePublishedAt(object $object): bool
5757
{
58-
if (!$this->annotationReader->isConfigured($object)) {
58+
if (!$this->attributeReader->isConfigured($object)) {
5959
throw new \InvalidArgumentException(sprintf('Object of class %s does not implement publishable configuration.', \get_class($object)));
6060
}
6161

62-
$value = $this->getClassMetadata($object)->getFieldValue($object, $this->annotationReader->getConfiguration($object)->fieldName);
62+
$value = $this->getClassMetadata($object)->getFieldValue($object, $this->attributeReader->getConfiguration($object)->fieldName);
6363

6464
return null !== $value && new \DateTimeImmutable() >= $value;
6565
}
6666

6767
public function hasPublicationDate(object $object): bool
6868
{
69-
if (!$this->annotationReader->isConfigured($object)) {
69+
if (!$this->attributeReader->isConfigured($object)) {
7070
throw new \InvalidArgumentException(sprintf('Object of class %s does not implement publishable configuration.', \get_class($object)));
7171
}
7272

73-
return null !== $this->getClassMetadata($object)->getFieldValue($object, $this->annotationReader->getConfiguration($object)->fieldName);
73+
return null !== $this->getClassMetadata($object)->getFieldValue($object, $this->attributeReader->getConfiguration($object)->fieldName);
7474
}
7575

7676
public function isPublishedRequest(Request $request): bool
7777
{
7878
return $request->query->getBoolean('published', false);
7979
}
8080

81-
public function getAnnotationReader(): PublishableAttributeReader
81+
public function getAttributeReader(): PublishableAttributeReader
8282
{
83-
return $this->annotationReader;
83+
return $this->attributeReader;
8484
}
8585
}

src/Mercure/PublishableAwareHub.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public function publish(Update $update): string
5959
return $this->decorated->publish($update);
6060
}
6161

62-
if ($this->publishableStatusChecker->getAnnotationReader()->isConfigured($resource) && !$this->publishableStatusChecker->isActivePublishedAt($resource)) {
62+
if ($this->publishableStatusChecker->getAttributeReader()->isConfigured($resource) && !$this->publishableStatusChecker->isActivePublishedAt($resource)) {
6363
$update = new Update(topics: $update->getTopics(), data: $update->getData(), private: true, id: $update->getId(), type: $update->getType(), retry: $update->getRetry());
6464
}
6565
}

src/Metadata/Factory/ComponentUsageMetadataFactory.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public function __construct(
5050

5151
public function create(ComponentInterface $component): ComponentUsageMetadata
5252
{
53-
$annotationReader = $this->publishableStatusChecker->getAnnotationReader();
53+
$annotationReader = $this->publishableStatusChecker->getAttributeReader();
5454
if ($annotationReader->isConfigured($component) && !$this->publishableStatusChecker->isActivePublishedAt($component)) {
5555
// get the published component to run checks against
5656
$configuration = $annotationReader->getConfiguration($component);

src/Resources/config/services.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1345,6 +1345,8 @@
13451345
new Reference('api_platform.iri_converter'),
13461346
new Reference('http_kernel'),
13471347
new Reference('request_stack'),
1348+
new Reference(PublishableStatusChecker::class),
1349+
new Reference('doctrine')
13481350
])
13491351
->tag('security.voter');
13501352

src/Security/Voter/ComponentVoter.php

Lines changed: 74 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,14 @@
1414
namespace Silverback\ApiComponentsBundle\Security\Voter;
1515

1616
use ApiPlatform\Api\IriConverterInterface;
17+
use Doctrine\Persistence\ManagerRegistry;
18+
use Silverback\ApiComponentsBundle\AttributeReader\PublishableAttributeReader;
1719
use Silverback\ApiComponentsBundle\DataProvider\PageDataProvider;
1820
use Silverback\ApiComponentsBundle\Entity\Core\AbstractComponent;
1921
use Silverback\ApiComponentsBundle\Entity\Core\AbstractPageData;
2022
use Silverback\ApiComponentsBundle\Entity\Core\Route;
23+
use Silverback\ApiComponentsBundle\Helper\Publishable\PublishableStatusChecker;
24+
use Silverback\ApiComponentsBundle\Utility\ClassMetadataTrait;
2125
use Symfony\Component\HttpFoundation\Request;
2226
use Symfony\Component\HttpFoundation\RequestStack;
2327
use Symfony\Component\HttpFoundation\Response;
@@ -31,23 +35,19 @@
3135
*/
3236
class ComponentVoter extends Voter
3337
{
34-
public const READ_COMPONENT = 'read_component';
38+
use ClassMetadataTrait;
3539

36-
private PageDataProvider $pageDataProvider;
37-
private IriConverterInterface $iriConverter;
38-
private HttpKernelInterface $httpKernel;
39-
private RequestStack $requestStack;
40+
public const READ_COMPONENT = 'read_component';
4041

4142
public function __construct(
42-
PageDataProvider $pageDataProvider,
43-
IriConverterInterface $iriConverter,
44-
HttpKernelInterface $httpKernel,
45-
RequestStack $requestStack
43+
private readonly PageDataProvider $pageDataProvider,
44+
private readonly IriConverterInterface $iriConverter,
45+
private readonly HttpKernelInterface $httpKernel,
46+
private readonly RequestStack $requestStack,
47+
private readonly PublishableStatusChecker $publishableStatusChecker,
48+
ManagerRegistry $registry
4649
) {
47-
$this->pageDataProvider = $pageDataProvider;
48-
$this->iriConverter = $iriConverter;
49-
$this->httpKernel = $httpKernel;
50-
$this->requestStack = $requestStack;
50+
$this->initRegistry($registry);
5151
}
5252

5353
protected function supports($attribute, $subject): bool
@@ -64,24 +64,47 @@ protected function voteOnAttribute($attribute, $subject, TokenInterface $token):
6464
if (!$request) {
6565
return true;
6666
}
67-
// TODO: if the subject is publishable, we should also check if there is a published version and the pages that one is in.
68-
// The draft will not be in any locations.
67+
68+
$subject = $this->getPublishedSubject($subject);
6969

7070
$pagesGenerator = $this->getComponentPages($subject);
7171
$pages = iterator_to_array($pagesGenerator);
72-
// Check if accessible via any route
73-
$routes = $this->getComponentRoutesFromPages($pages);
74-
$routeCount = 0;
75-
foreach ($routes as $route) {
76-
++$routeCount;
77-
if ($this->isRouteReachableResource($route, $request)) {
72+
73+
// 1. Check if accessible via any route
74+
$routeVoteResult = $this->voteByRoute($pages, $request);
75+
if ($routeVoteResult) {
76+
return true;
77+
}
78+
79+
// 2. as a page data property
80+
$pageDataResult = $this->voteByPageData($subject, $request);
81+
if ($pageDataResult) {
82+
return true;
83+
}
84+
85+
// 3. as a component in the page template being used by page data
86+
$pageTemplateResult = $this->voteByPageTemplate($pages, $request);
87+
if ($pageTemplateResult) {
88+
return true;
89+
}
90+
91+
// vote is ok if all sub votes abstain
92+
return null === $routeVoteResult && null === $pageDataResult && null === $pageTemplateResult;
93+
}
94+
95+
private function voteByPageTemplate($pages, Request $request): ?bool
96+
{
97+
$pageDataByPagesComponentUsedIn = $this->pageDataProvider->findPageDataResourcesByPages($pages);
98+
foreach ($pageDataByPagesComponentUsedIn as $pageData) {
99+
if ($this->isPageDataReachableResource($pageData, $request)) {
78100
return true;
79101
}
80102
}
103+
return \count($pageDataByPagesComponentUsedIn) ? false : null;
104+
}
81105

82-
// check if accessible via any page data
83-
84-
// 1. as a page data property
106+
private function voteByPageData($subject, Request $request): ?bool
107+
{
85108
$pageData = $this->pageDataProvider->findPageDataComponentMetadata($subject);
86109
$pageDataCount = 0;
87110
foreach ($pageData as $pageDatum) {
@@ -92,16 +115,37 @@ protected function voteOnAttribute($attribute, $subject, TokenInterface $token):
92115
}
93116
}
94117
}
118+
return $pageDataCount ? false : null;
119+
}
95120

96-
// 2. as a component in the page template being used by page data
97-
$pageDataByPagesComponentUsedIn = $this->pageDataProvider->findPageDataResourcesByPages($pages);
98-
foreach ($pageDataByPagesComponentUsedIn as $pageData) {
99-
if ($this->isPageDataReachableResource($pageData, $request)) {
121+
private function voteByRoute($pages, Request $request): ?bool
122+
{
123+
$routes = $this->getComponentRoutesFromPages($pages);
124+
$routeCount = 0;
125+
foreach ($routes as $route) {
126+
++$routeCount;
127+
if ($this->isRouteReachableResource($route, $request)) {
100128
return true;
101129
}
102130
}
131+
return $routeCount ? false : null;
132+
}
133+
103134

104-
return !$routeCount && !$pageDataCount && !\count($pageDataByPagesComponentUsedIn);
135+
private function getPublishedSubject($subject)
136+
{
137+
// 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
138+
$publishableAttributeReader = $this->publishableStatusChecker->getAttributeReader();
139+
if ($publishableAttributeReader->isConfigured($subject) && !$this->publishableStatusChecker->isActivePublishedAt($subject)) {
140+
$configuration = $publishableAttributeReader->getConfiguration($subject);
141+
$classMetadata = $this->getClassMetadata($subject);
142+
143+
$publishedResourceAssociation = $classMetadata->getFieldValue($subject, $configuration->associationName);
144+
if ($publishedResourceAssociation) {
145+
return $publishedResourceAssociation;
146+
}
147+
}
148+
return $subject;
105149
}
106150

107151
private function isRouteReachableResource(Route $route, Request $request): bool
@@ -150,7 +194,7 @@ private function isPathReachable(string $path, Request $request): bool
150194
}
151195
}
152196

153-
private function getComponentPages(AbstractComponent $component): iterable
197+
private function getComponentPages(AbstractComponent $component): \Traversable
154198
{
155199
$componentPositions = $component->getComponentPositions();
156200
if (!\count($componentPositions)) {

src/Serializer/ContextBuilder/PublishableContextBuilder.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public function createFromRequest(Request $request, bool $normalization, array $
4040
empty($resourceClass = $context['resource_class']) ||
4141
empty($context['groups']) ||
4242
\in_array('Route:manifest:read', $context['groups'], true) ||
43-
!$this->publishableStatusChecker->getAnnotationReader()->isConfigured($resourceClass)
43+
!$this->publishableStatusChecker->getAttributeReader()->isConfigured($resourceClass)
4444
) {
4545
return $context;
4646
}

0 commit comments

Comments
 (0)