From ac4e57c913b2a0c27fafa1e9066775b3c3b0f181 Mon Sep 17 00:00:00 2001 From: Vidar Langseid Date: Fri, 25 Oct 2024 13:34:42 +0200 Subject: [PATCH] IBX-8957: Fixed deserializing SiteAccess Matchers for ESI (#430) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For more details see https://issues.ibexa.co/browse/IBX-8957 and https://github.com/ibexa/core/pull/430 Key changes: * Fixed deserialization of Matchers in SiteAccessMatchListener * Implemented Fragment SiteAccessSerializer * Injected Fragment SiteAccessSerializer into fragment renderers * [Tests] Aligned tests with the changes * [Tests] Added coverage for Fragment SiteAccessSerializer --------- Co-Authored-By: Paweł Niedzielski --- phpstan-baseline.neon | 100 ----- .../Compiler/FragmentPass.php | 3 +- .../Fragment/DecoratedFragmentRenderer.php | 15 +- .../Core/Fragment/InlineFragmentRenderer.php | 13 +- .../Fragment/SiteAccessSerializationTrait.php | 4 + .../Core/Fragment/SiteAccessSerializer.php | 51 +++ .../SiteAccessSerializerInterface.php | 20 + src/bundle/Core/Resources/config/routing.yml | 5 +- .../Resources/config/routing/serializers.yml | 65 +++ .../Serializer/CompoundMatcherNormalizer.php | 36 +- .../Serializer/MatcherDenormalizer.php | 57 +++ .../Serializer/SiteAccessNormalizer.php | 84 ++++ .../EventListener/SiteAccessMatchListener.php | 122 +----- .../Compiler/FragmentPassTest.php | 7 +- .../DecoratedFragmentRendererTest.php | 30 +- .../Fragment/InlineFragmentRendererTest.php | 11 +- .../Fragment/SiteAccessSerializerTest.php | 78 ++++ .../SiteAccessMatchListenerTest.php | 375 +++++++++++------- .../MVC/Symfony/EventListener/TestMatcher.php | 20 + .../SiteAccess/MatcherSerializationTest.php | 137 ++++--- 20 files changed, 802 insertions(+), 431 deletions(-) create mode 100644 src/bundle/Core/Fragment/SiteAccessSerializer.php create mode 100644 src/bundle/Core/Fragment/SiteAccessSerializerInterface.php create mode 100644 src/bundle/Core/Resources/config/routing/serializers.yml create mode 100644 src/lib/MVC/Symfony/Component/Serializer/MatcherDenormalizer.php create mode 100644 src/lib/MVC/Symfony/Component/Serializer/SiteAccessNormalizer.php create mode 100644 tests/bundle/Core/Fragment/SiteAccessSerializerTest.php create mode 100644 tests/lib/MVC/Symfony/EventListener/TestMatcher.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 29e79b78ae..d601a47675 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -11320,11 +11320,6 @@ parameters: count: 1 path: src/lib/MVC/RepositoryAwareInterface.php - - - message: "#^Cannot access offset 'config' on array\\|ArrayObject\\<\\(int\\|string\\), mixed\\>\\|bool\\|float\\|int\\|string\\|null\\.$#" - count: 1 - path: src/lib/MVC/Symfony/Component/Serializer/CompoundMatcherNormalizer.php - - message: "#^Method Ibexa\\\\Core\\\\MVC\\\\Symfony\\\\Component\\\\Serializer\\\\SimplifiedRequestNormalizer\\:\\:supportsNormalization\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" count: 1 @@ -11560,31 +11555,6 @@ parameters: count: 1 path: src/lib/MVC/Symfony/EventListener/LanguageSwitchListener.php - - - message: "#^Call to an undefined method Ibexa\\\\Core\\\\MVC\\\\Symfony\\\\SiteAccess\\\\Matcher\\:\\:setSubMatchers\\(\\)\\.$#" - count: 1 - path: src/lib/MVC/Symfony/EventListener/SiteAccessMatchListener.php - - - - message: "#^Method Ibexa\\\\Core\\\\MVC\\\\Symfony\\\\EventListener\\\\SiteAccessMatchListener\\:\\:deserializeMatcher\\(\\) has parameter \\$serializedSubMatchers with no value type specified in iterable type array\\.$#" - count: 1 - path: src/lib/MVC/Symfony/EventListener/SiteAccessMatchListener.php - - - - message: "#^Parameter \\#1 \\$serializedGroups of method Ibexa\\\\Core\\\\MVC\\\\Symfony\\\\EventListener\\\\SiteAccessMatchListener\\:\\:buildGroups\\(\\) expects array\\, array\\ given\\.$#" - count: 1 - path: src/lib/MVC/Symfony/EventListener/SiteAccessMatchListener.php - - - - message: "#^Parameter \\#2 \\$haystack of function in_array expects array, array\\\\|false given\\.$#" - count: 1 - path: src/lib/MVC/Symfony/EventListener/SiteAccessMatchListener.php - - - - message: "#^Parameter \\#2 \\$matcherFQCN of method Ibexa\\\\Core\\\\MVC\\\\Symfony\\\\EventListener\\\\SiteAccessMatchListener\\:\\:deserializeMatcher\\(\\) expects string, Ibexa\\\\Core\\\\MVC\\\\Symfony\\\\SiteAccess\\\\Matcher given\\.$#" - count: 1 - path: src/lib/MVC/Symfony/EventListener/SiteAccessMatchListener.php - - message: "#^Method Ibexa\\\\Core\\\\MVC\\\\Symfony\\\\ExpressionLanguage\\\\TwigVariableProviderExtension\\:\\:hasParameterProvider\\(\\) has parameter \\$variables with no value type specified in iterable type array\\.$#" count: 1 @@ -25195,11 +25165,6 @@ parameters: count: 1 path: tests/bundle/Core/Fragment/DecoratedFragmentRendererTest.php - - - message: "#^Parameter \\#1 \\$innerRenderer of class Ibexa\\\\Bundle\\\\Core\\\\Fragment\\\\DecoratedFragmentRenderer constructor expects Symfony\\\\Component\\\\HttpKernel\\\\Fragment\\\\FragmentRendererInterface, PHPUnit\\\\Framework\\\\MockObject\\\\MockObject given\\.$#" - count: 5 - path: tests/bundle/Core/Fragment/DecoratedFragmentRendererTest.php - - message: "#^Binary operation \"\\.\" between 'rendered_' and \\(Closure\\(array\\\\)\\: string\\)\\|string results in an error\\.$#" count: 1 @@ -25245,11 +25210,6 @@ parameters: count: 1 path: tests/bundle/Core/Fragment/InlineFragmentRendererTest.php - - - message: "#^Parameter \\#1 \\$innerRenderer of class Ibexa\\\\Bundle\\\\Core\\\\Fragment\\\\InlineFragmentRenderer constructor expects Symfony\\\\Component\\\\HttpKernel\\\\Fragment\\\\FragmentRendererInterface, PHPUnit\\\\Framework\\\\MockObject\\\\MockObject given\\.$#" - count: 2 - path: tests/bundle/Core/Fragment/InlineFragmentRendererTest.php - - message: "#^Method Ibexa\\\\Tests\\\\Bundle\\\\Core\\\\Imagine\\\\AliasCleanerTest\\:\\:testRemoveAliases\\(\\) has no return type specified\\.$#" count: 1 @@ -46160,36 +46120,6 @@ parameters: count: 3 path: tests/lib/MVC/Symfony/EventListener/LanguageSwitchListenerTest.php - - - message: "#^Method Ibexa\\\\Tests\\\\Core\\\\MVC\\\\Symfony\\\\EventListener\\\\SiteAccessMatchListenerTest\\:\\:testGetSubscribedEvents\\(\\) has no return type specified\\.$#" - count: 1 - path: tests/lib/MVC/Symfony/EventListener/SiteAccessMatchListenerTest.php - - - - message: "#^Method Ibexa\\\\Tests\\\\Core\\\\MVC\\\\Symfony\\\\EventListener\\\\SiteAccessMatchListenerTest\\:\\:testOnKernelRequest\\(\\) has no return type specified\\.$#" - count: 1 - path: tests/lib/MVC/Symfony/EventListener/SiteAccessMatchListenerTest.php - - - - message: "#^Method Ibexa\\\\Tests\\\\Core\\\\MVC\\\\Symfony\\\\EventListener\\\\SiteAccessMatchListenerTest\\:\\:testOnKernelRequestSerializedSA\\(\\) has no return type specified\\.$#" - count: 1 - path: tests/lib/MVC/Symfony/EventListener/SiteAccessMatchListenerTest.php - - - - message: "#^Method Ibexa\\\\Tests\\\\Core\\\\MVC\\\\Symfony\\\\EventListener\\\\SiteAccessMatchListenerTest\\:\\:testOnKernelRequestSerializedSAWithCompoundMatcher\\(\\) has no return type specified\\.$#" - count: 1 - path: tests/lib/MVC/Symfony/EventListener/SiteAccessMatchListenerTest.php - - - - message: "#^Method Ibexa\\\\Tests\\\\Core\\\\MVC\\\\Symfony\\\\EventListener\\\\SiteAccessMatchListenerTest\\:\\:testOnKernelRequestSiteAccessPresent\\(\\) has no return type specified\\.$#" - count: 1 - path: tests/lib/MVC/Symfony/EventListener/SiteAccessMatchListenerTest.php - - - - message: "#^Method Ibexa\\\\Tests\\\\Core\\\\MVC\\\\Symfony\\\\EventListener\\\\SiteAccessMatchListenerTest\\:\\:testOnKernelRequestUserHashWithOriginalRequest\\(\\) has no return type specified\\.$#" - count: 1 - path: tests/lib/MVC/Symfony/EventListener/SiteAccessMatchListenerTest.php - - message: "#^Method Ibexa\\\\Tests\\\\Core\\\\MVC\\\\Symfony\\\\FieldType\\\\ImageAsset\\\\ParameterProviderTest\\:\\:dataProviderForTestGetViewParameters\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 @@ -47885,36 +47815,6 @@ parameters: count: 6 path: tests/lib/MVC/Symfony/SiteAccess/Compound/CompoundOrTest.php - - - message: "#^Method Ibexa\\\\Tests\\\\Core\\\\MVC\\\\Symfony\\\\SiteAccess\\\\MatcherSerializationTest\\:\\:getMapHostMatcherTestCase\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: tests/lib/MVC/Symfony/SiteAccess/MatcherSerializationTest.php - - - - message: "#^Method Ibexa\\\\Tests\\\\Core\\\\MVC\\\\Symfony\\\\SiteAccess\\\\MatcherSerializationTest\\:\\:getMapPortMatcherTestCase\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: tests/lib/MVC/Symfony/SiteAccess/MatcherSerializationTest.php - - - - message: "#^Method Ibexa\\\\Tests\\\\Core\\\\MVC\\\\Symfony\\\\SiteAccess\\\\MatcherSerializationTest\\:\\:getMapURIMatcherTestCase\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: tests/lib/MVC/Symfony/SiteAccess/MatcherSerializationTest.php - - - - message: "#^Method Ibexa\\\\Tests\\\\Core\\\\MVC\\\\Symfony\\\\SiteAccess\\\\MatcherSerializationTest\\:\\:matcherProvider\\(\\) has no return type specified\\.$#" - count: 1 - path: tests/lib/MVC/Symfony/SiteAccess/MatcherSerializationTest.php - - - - message: "#^Method Ibexa\\\\Tests\\\\Core\\\\MVC\\\\Symfony\\\\SiteAccess\\\\MatcherSerializationTest\\:\\:testDeserialize\\(\\) has no return type specified\\.$#" - count: 1 - path: tests/lib/MVC/Symfony/SiteAccess/MatcherSerializationTest.php - - - - message: "#^Parameter \\#1 \\$subMatchers of method Ibexa\\\\Core\\\\MVC\\\\Symfony\\\\SiteAccess\\\\Matcher\\\\Compound\\:\\:setSubMatchers\\(\\) expects array\\, array\\\\>\\> given\\.$#" - count: 4 - path: tests/lib/MVC/Symfony/SiteAccess/MatcherSerializationTest.php - - message: "#^Method Ibexa\\\\Tests\\\\Core\\\\MVC\\\\Symfony\\\\SiteAccess\\\\Provider\\\\ChainSiteAccessProviderTest\\:\\:getExistingSiteProvider\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 diff --git a/src/bundle/Core/DependencyInjection/Compiler/FragmentPass.php b/src/bundle/Core/DependencyInjection/Compiler/FragmentPass.php index 286d5d23b0..95adbd422b 100644 --- a/src/bundle/Core/DependencyInjection/Compiler/FragmentPass.php +++ b/src/bundle/Core/DependencyInjection/Compiler/FragmentPass.php @@ -9,6 +9,7 @@ use Ibexa\Bundle\Core\Fragment\DecoratedFragmentRenderer; use Ibexa\Bundle\Core\Fragment\FragmentListenerFactory; use Ibexa\Bundle\Core\Fragment\InlineFragmentRenderer; +use Ibexa\Bundle\Core\Fragment\SiteAccessSerializerInterface; use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -47,7 +48,7 @@ public function process(ContainerBuilder $container) $container->setDefinition($renamedId, $definition); $decoratedDef = new ChildDefinition(DecoratedFragmentRenderer::class); - $decoratedDef->setArguments([new Reference($renamedId)]); + $decoratedDef->setArguments([new Reference($renamedId), new Reference(SiteAccessSerializerInterface::class)]); $decoratedDef->setPublic($public); $decoratedDef->setTags($tags); // Special treatment for inline fragment renderer, to fit ESI renderer constructor type hinting (forced to InlineFragmentRenderer) diff --git a/src/bundle/Core/Fragment/DecoratedFragmentRenderer.php b/src/bundle/Core/Fragment/DecoratedFragmentRenderer.php index cf7acadec9..870663bdf9 100644 --- a/src/bundle/Core/Fragment/DecoratedFragmentRenderer.php +++ b/src/bundle/Core/Fragment/DecoratedFragmentRenderer.php @@ -15,17 +15,20 @@ class DecoratedFragmentRenderer implements FragmentRendererInterface, SiteAccessAware { - use SiteAccessSerializationTrait; - /** @var \Symfony\Component\HttpKernel\Fragment\FragmentRendererInterface */ private $innerRenderer; /** @var \Ibexa\Core\MVC\Symfony\SiteAccess */ private $siteAccess; - public function __construct(FragmentRendererInterface $innerRenderer) - { + private SiteAccessSerializerInterface $siteAccessSerializer; + + public function __construct( + FragmentRendererInterface $innerRenderer, + SiteAccessSerializerInterface $siteAccessSerializer + ) { $this->innerRenderer = $innerRenderer; + $this->siteAccessSerializer = $siteAccessSerializer; } /** @@ -61,10 +64,10 @@ public function setFragmentPath($path) public function render($uri, Request $request, array $options = []) { if ($uri instanceof ControllerReference && $request->attributes->has('siteaccess')) { - // Serialize the siteaccess to get it back after. + // Serialize a SiteAccess to get it back after. // @see \Ibexa\Core\MVC\Symfony\EventListener\SiteAccessMatchListener $siteAccess = $request->attributes->get('siteaccess'); - $this->serializeSiteAccess($siteAccess, $uri); + $this->siteAccessSerializer->serializeSiteAccessAsControllerAttributes($siteAccess, $uri); } return $this->innerRenderer->render($uri, $request, $options); diff --git a/src/bundle/Core/Fragment/InlineFragmentRenderer.php b/src/bundle/Core/Fragment/InlineFragmentRenderer.php index d47bc11a1e..de95deabd2 100644 --- a/src/bundle/Core/Fragment/InlineFragmentRenderer.php +++ b/src/bundle/Core/Fragment/InlineFragmentRenderer.php @@ -16,17 +16,20 @@ class InlineFragmentRenderer extends BaseRenderer implements SiteAccessAware { - use SiteAccessSerializationTrait; - /** @var \Symfony\Component\HttpKernel\Fragment\FragmentRendererInterface */ private $innerRenderer; /** @var \Ibexa\Core\MVC\Symfony\SiteAccess */ private $siteAccess; - public function __construct(FragmentRendererInterface $innerRenderer) - { + private SiteAccessSerializerInterface $siteAccessSerializer; + + public function __construct( + FragmentRendererInterface $innerRenderer, + SiteAccessSerializerInterface $siteAccessSerializer + ) { $this->innerRenderer = $innerRenderer; + $this->siteAccessSerializer = $siteAccessSerializer; } public function setFragmentPath($path) @@ -47,7 +50,7 @@ public function render($uri, Request $request, array $options = []) if ($request->attributes->has('siteaccess')) { /** @var \Ibexa\Core\MVC\Symfony\SiteAccess $siteAccess */ $siteAccess = $request->attributes->get('siteaccess'); - $this->serializeSiteAccess($siteAccess, $uri); + $this->siteAccessSerializer->serializeSiteAccessAsControllerAttributes($siteAccess, $uri); } if ($request->attributes->has('semanticPathinfo')) { $uri->attributes['semanticPathinfo'] = $request->attributes->get('semanticPathinfo'); diff --git a/src/bundle/Core/Fragment/SiteAccessSerializationTrait.php b/src/bundle/Core/Fragment/SiteAccessSerializationTrait.php index 0bde8b1b38..2e64765c68 100644 --- a/src/bundle/Core/Fragment/SiteAccessSerializationTrait.php +++ b/src/bundle/Core/Fragment/SiteAccessSerializationTrait.php @@ -13,6 +13,10 @@ use Symfony\Component\HttpKernel\Controller\ControllerReference; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; +/** + * @deprecated Deprecated since 4.6. Inject an instance of {@see \Ibexa\Bundle\Core\Fragment\SiteAccessSerializerInterface} + * instead. + */ trait SiteAccessSerializationTrait { use SerializerTrait; diff --git a/src/bundle/Core/Fragment/SiteAccessSerializer.php b/src/bundle/Core/Fragment/SiteAccessSerializer.php new file mode 100644 index 0000000000..8507d113dd --- /dev/null +++ b/src/bundle/Core/Fragment/SiteAccessSerializer.php @@ -0,0 +1,51 @@ +serializer = $serializer; + } + + /** + * @throws \JsonException + */ + public function serializeSiteAccessAsControllerAttributes(SiteAccess $siteAccess, ControllerReference $controller): void + { + // Serialize the SiteAccess to get it back after. @see \Ibexa\Core\MVC\Symfony\EventListener\SiteAccessMatchListener + $controller->attributes['serialized_siteaccess'] = json_encode($siteAccess, JSON_THROW_ON_ERROR); + $controller->attributes['serialized_siteaccess_matcher'] = $this->serializer->serialize( + $siteAccess->matcher, + 'json', + [AbstractNormalizer::IGNORED_ATTRIBUTES => ['request', 'container', 'matcherBuilder', 'connection']] + ); + if ($siteAccess->matcher instanceof SiteAccess\Matcher\CompoundInterface) { + $subMatchers = $siteAccess->matcher->getSubMatchers(); + foreach ($subMatchers as $subMatcher) { + $controller->attributes['serialized_siteaccess_sub_matchers'][get_class($subMatcher)] = $this->serializer->serialize( + $subMatcher, + 'json', + [AbstractNormalizer::IGNORED_ATTRIBUTES => ['request', 'container', 'matcherBuilder', 'connection']] + ); + } + } + } +} diff --git a/src/bundle/Core/Fragment/SiteAccessSerializerInterface.php b/src/bundle/Core/Fragment/SiteAccessSerializerInterface.php new file mode 100644 index 0000000000..433dffc3c6 --- /dev/null +++ b/src/bundle/Core/Fragment/SiteAccessSerializerInterface.php @@ -0,0 +1,20 @@ + $data */ $data['config'] = []; $data['matchersMap'] = []; @@ -34,7 +44,29 @@ public function supportsNormalization($data, string $format = null): bool public function supportsDenormalization($data, string $type, string $format = null): bool { - return $type === Matcher\Compound::class; + return is_a($type, Matcher\Compound::class, true); + } + + /** + * @phpstan-param class-string<\Ibexa\Core\MVC\Symfony\SiteAccess\Matcher\Compound> $type + * + * @throws \Symfony\Component\Serializer\Exception\ExceptionInterface + */ + public function denormalize($data, string $type, ?string $format = null, array $context = []): object + { + $compoundMatcher = new $type([]); + $subMatchers = []; + foreach ($context['serialized_siteaccess_sub_matchers'] ?? [] as $matcherType => $subMatcher) { + $subMatchers[$matcherType] = $this->serializer->deserialize( + $subMatcher, + $matcherType, + $format ?? 'json', + $context + ); + } + $compoundMatcher->setSubMatchers($subMatchers); + + return $compoundMatcher; } } diff --git a/src/lib/MVC/Symfony/Component/Serializer/MatcherDenormalizer.php b/src/lib/MVC/Symfony/Component/Serializer/MatcherDenormalizer.php new file mode 100644 index 0000000000..dea3b53b61 --- /dev/null +++ b/src/lib/MVC/Symfony/Component/Serializer/MatcherDenormalizer.php @@ -0,0 +1,57 @@ +registry = $registry; + } + + /** + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException + * @throws \Symfony\Component\Serializer\Exception\ExceptionInterface + */ + public function denormalize($data, string $type, ?string $format = null, array $context = []): object + { + $matcher = $this->registry->getMatcher($type); + + return $this->denormalizer->denormalize($data, $type, $format, $context + [ + AbstractNormalizer::OBJECT_TO_POPULATE => $matcher, + self::MATCHER_NORMALIZER_ALREADY_WORKED => true, + ]); + } + + public function supportsDenormalization($data, string $type, ?string $format = null, array $context = []): bool + { + if ($context[self::MATCHER_NORMALIZER_ALREADY_WORKED] ?? false) { + return false; + } + + return is_subclass_of($type, Matcher::class) && $this->registry->hasMatcher($type); + } +} diff --git a/src/lib/MVC/Symfony/Component/Serializer/SiteAccessNormalizer.php b/src/lib/MVC/Symfony/Component/Serializer/SiteAccessNormalizer.php new file mode 100644 index 0000000000..d0919dc5cc --- /dev/null +++ b/src/lib/MVC/Symfony/Component/Serializer/SiteAccessNormalizer.php @@ -0,0 +1,84 @@ +serializer->deserialize( + $matcherData, + $matcherType, + $format ?? 'json', + $context + ) + : null, + $data['provider'] ?? null, + $this->denormalizer->denormalize($data['groups'] ?? [], SiteAccessGroup::class . '[]', $format, $context) + ); + } + + public function supportsDenormalization($data, string $type, ?string $format = null, array $context = []): bool + { + return $type === SiteAccess::class; + } + + public function supportsNormalization($data, ?string $format = null): bool + { + return $data instanceof SiteAccess; + } + + /** + * @param \Ibexa\Core\MVC\Symfony\SiteAccess $object + * + * @return array{name: string, matchingType: string, matcher: array{type: class-string, data: string}|null, provider: string|null, groups: array} + */ + public function normalize($object, ?string $format = null, array $context = []): array + { + $matcherData = null; + if (is_object($object->matcher)) { + $matcherData = [ + 'type' => get_class($object->matcher), + 'data' => $this->serializer->serialize($object->matcher, $format ?? 'json', $context), + ]; + } + + return [ + 'name' => $object->name, + 'matchingType' => $object->matchingType, + 'matcher' => $matcherData, + 'provider' => $object->provider, + 'groups' => $object->groups, + ]; + } +} diff --git a/src/lib/MVC/Symfony/EventListener/SiteAccessMatchListener.php b/src/lib/MVC/Symfony/EventListener/SiteAccessMatchListener.php index e55cb6a6a0..7729a58877 100644 --- a/src/lib/MVC/Symfony/EventListener/SiteAccessMatchListener.php +++ b/src/lib/MVC/Symfony/EventListener/SiteAccessMatchListener.php @@ -6,16 +6,11 @@ */ namespace Ibexa\Core\MVC\Symfony\EventListener; -// @todo Move SiteAccessMatcherRegistryInterface to \Ibexa\Core\MVC\Symfony\SiteAccess\Matcher -use Ibexa\Bundle\Core\SiteAccess\SiteAccessMatcherRegistryInterface; -use Ibexa\Core\Base\Exceptions\InvalidArgumentException; -use Ibexa\Core\MVC\Symfony\Component\Serializer\SerializerTrait; use Ibexa\Core\MVC\Symfony\Event\PostSiteAccessMatchEvent; use Ibexa\Core\MVC\Symfony\MVCEvents; use Ibexa\Core\MVC\Symfony\Routing\SimplifiedRequest; use Ibexa\Core\MVC\Symfony\SiteAccess; use Ibexa\Core\MVC\Symfony\SiteAccess\Router as SiteAccessRouter; -use Ibexa\Core\MVC\Symfony\SiteAccessGroup; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\Request; @@ -29,28 +24,23 @@ */ class SiteAccessMatchListener implements EventSubscriberInterface { - use SerializerTrait; + protected SiteAccess\Router $siteAccessRouter; - /** @var \Ibexa\Core\MVC\Symfony\SiteAccess\Router */ - protected $siteAccessRouter; + protected EventDispatcherInterface $eventDispatcher; - /** @var \Symfony\Component\EventDispatcher\EventDispatcherInterface */ - protected $eventDispatcher; - - /** @var \Ibexa\Bundle\Core\SiteAccess\SiteAccessMatcherRegistryInterface */ - private $siteAccessMatcherRegistry; + private SerializerInterface $serializer; public function __construct( SiteAccessRouter $siteAccessRouter, EventDispatcherInterface $eventDispatcher, - SiteAccessMatcherRegistryInterface $siteAccessMatcherRegistry + SerializerInterface $serializer ) { $this->siteAccessRouter = $siteAccessRouter; $this->eventDispatcher = $eventDispatcher; - $this->siteAccessMatcherRegistry = $siteAccessMatcherRegistry; + $this->serializer = $serializer; } - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return [ // Should take place just after FragmentListener (priority 48) in order to get rebuilt request attributes in case of subrequest @@ -70,19 +60,15 @@ public function onKernelRequest(RequestEvent $event): void // We have a serialized siteaccess object from a fragment (sub-request), we need to get it back. if ($request->attributes->has('serialized_siteaccess')) { - $serializer = $this->getSerializer(); /** @var \Ibexa\Core\MVC\Symfony\SiteAccess $siteAccess */ - $siteAccess = $serializer->deserialize($request->attributes->get('serialized_siteaccess'), SiteAccess::class, 'json'); - if ($siteAccess->matcher !== null) { - $siteAccess->matcher = $this->deserializeMatcher( - $serializer, - $siteAccess->matcher, - $request->attributes->get('serialized_siteaccess_matcher'), - $request->attributes->get('serialized_siteaccess_sub_matchers') - ); - } - $siteAccess->groups = $this->buildGroups( - $siteAccess->groups + $siteAccess = $this->serializer->deserialize( + $request->attributes->get('serialized_siteaccess'), + SiteAccess::class, + 'json', + [ + 'serialized_siteaccess_matcher' => $request->attributes->get('serialized_siteaccess_matcher'), + 'serialized_siteaccess_sub_matchers' => $request->attributes->get('serialized_siteaccess_sub_matchers'), + ] ); $request->attributes->set( 'siteaccess', @@ -126,86 +112,6 @@ private function getSiteAccessFromRequest(Request $request) ) ); } - - /** - * @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException - * @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException - */ - private function deserializeMatcher( - SerializerInterface $serializer, - string $matcherFQCN, - string $serializedMatcher, - ?array $serializedSubMatchers - ): SiteAccess\Matcher { - $matcher = null; - if (in_array(SiteAccess\Matcher::class, class_implements($matcherFQCN), true)) { - $matcher = $this->buildMatcherFromSerializedClass( - $serializer, - $matcherFQCN, - $serializedMatcher - ); - } else { - throw new InvalidArgumentException( - 'matcher', - sprintf( - 'SiteAccess matcher must implement %s or %s', - SiteAccess\Matcher::class, - SiteAccess\URILexer::class - ) - ); - } - if (!empty($serializedSubMatchers)) { - $subMatchers = []; - foreach ($serializedSubMatchers as $subMatcherFQCN => $serializedData) { - $subMatchers[$subMatcherFQCN] = $this->buildMatcherFromSerializedClass( - $serializer, - $subMatcherFQCN, - $serializedData - ); - } - $matcher->setSubMatchers($subMatchers); - } - - return $matcher; - } - - /** - * @param array $serializedGroups - * - * @return \Ibexa\Core\MVC\Symfony\SiteAccessGroup[] - */ - private function buildGroups( - array $serializedGroups - ): array { - return array_map( - static function (array $serializedGroup): SiteAccessGroup { - return new SiteAccessGroup($serializedGroup['name']); - }, - $serializedGroups - ); - } - - /** - * @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException - */ - private function buildMatcherFromSerializedClass( - SerializerInterface $serializer, - string $matcherClass, - string $serializedMatcher - ): SiteAccess\Matcher { - $matcher = null; - if ($this->siteAccessMatcherRegistry->hasMatcher($matcherClass)) { - $matcher = $this->siteAccessMatcherRegistry->getMatcher($matcherClass); - } else { - $matcher = $serializer->deserialize( - $serializedMatcher, - $matcherClass, - 'json' - ); - } - - return $matcher; - } } class_alias(SiteAccessMatchListener::class, 'eZ\Publish\Core\MVC\Symfony\EventListener\SiteAccessMatchListener'); diff --git a/tests/bundle/Core/DependencyInjection/Compiler/FragmentPassTest.php b/tests/bundle/Core/DependencyInjection/Compiler/FragmentPassTest.php index f59baf694a..1098dd4fd8 100644 --- a/tests/bundle/Core/DependencyInjection/Compiler/FragmentPassTest.php +++ b/tests/bundle/Core/DependencyInjection/Compiler/FragmentPassTest.php @@ -10,6 +10,7 @@ use Ibexa\Bundle\Core\Fragment\DecoratedFragmentRenderer; use Ibexa\Bundle\Core\Fragment\FragmentListenerFactory; use Ibexa\Bundle\Core\Fragment\InlineFragmentRenderer; +use Ibexa\Bundle\Core\Fragment\SiteAccessSerializerInterface; use Matthias\SymfonyDependencyInjectionTest\PhpUnit\AbstractCompilerPassTestCase; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; @@ -65,7 +66,7 @@ public function testProcess() $decoratedInlineDef = $this->container->getDefinition('fragment.renderer.inline'); $this->assertSame(['kernel.fragment_renderer' => [[]]], $decoratedInlineDef->getTags()); $this->assertEquals( - [new Reference('fragment.renderer.inline.inner')], + [new Reference('fragment.renderer.inline.inner'), new Reference(SiteAccessSerializerInterface::class)], $decoratedInlineDef->getArguments() ); $this->assertSame(InlineFragmentRenderer::class, $decoratedInlineDef->getClass()); @@ -74,7 +75,7 @@ public function testProcess() $decoratedEsiDef = $this->container->getDefinition('fragment.renderer.esi'); $this->assertSame(['kernel.fragment_renderer' => [[]]], $decoratedEsiDef->getTags()); $this->assertEquals( - [new Reference('fragment.renderer.esi.inner')], + [new Reference('fragment.renderer.esi.inner'), new Reference(SiteAccessSerializerInterface::class)], $decoratedEsiDef->getArguments() ); @@ -82,7 +83,7 @@ public function testProcess() $decoratedHincludeDef = $this->container->getDefinition('fragment.renderer.hinclude'); $this->assertSame(['kernel.fragment_renderer' => [[]]], $decoratedHincludeDef->getTags()); $this->assertEquals( - [new Reference('fragment.renderer.hinclude.inner')], + [new Reference('fragment.renderer.hinclude.inner'), new Reference(SiteAccessSerializerInterface::class)], $decoratedHincludeDef->getArguments() ); } diff --git a/tests/bundle/Core/Fragment/DecoratedFragmentRendererTest.php b/tests/bundle/Core/Fragment/DecoratedFragmentRendererTest.php index f43f8e4cd9..15e0472faa 100644 --- a/tests/bundle/Core/Fragment/DecoratedFragmentRendererTest.php +++ b/tests/bundle/Core/Fragment/DecoratedFragmentRendererTest.php @@ -7,6 +7,8 @@ namespace Ibexa\Tests\Bundle\Core\Fragment; use Ibexa\Bundle\Core\Fragment\DecoratedFragmentRenderer; +use Ibexa\Bundle\Core\Fragment\SiteAccessSerializer; +use Ibexa\Core\MVC\Symfony\Component\Serializer\SerializerTrait; use Ibexa\Core\MVC\Symfony\SiteAccess; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ControllerReference; @@ -14,10 +16,15 @@ use Symfony\Component\HttpKernel\Fragment\RoutableFragmentRenderer; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; +/** + * @covers \Ibexa\Bundle\Core\Fragment\DecoratedFragmentRenderer + */ class DecoratedFragmentRendererTest extends FragmentRendererBaseTest { - /** @var \PHPUnit\Framework\MockObject\MockObject */ - protected $innerRenderer; + use SerializerTrait; + + /** @var \Symfony\Component\HttpKernel\Fragment\FragmentRendererInterface&\PHPUnit\Framework\MockObject\MockObject */ + protected FragmentRendererInterface $innerRenderer; protected function setUp(): void { @@ -33,9 +40,11 @@ public function testSetFragmentPathNotRoutableRenderer() ->expects($this->never()) ->method('analyseLink'); - $renderer = new DecoratedFragmentRenderer($this->innerRenderer); + $renderer = $this->getRenderer(); $renderer->setSiteAccess($siteAccess); - $renderer->setFragmentPath('foo'); + if ($renderer instanceof RoutableFragmentRenderer) { + $renderer->setFragmentPath('foo'); + } } public function testSetFragmentPath() @@ -53,7 +62,7 @@ public function testSetFragmentPath() ->expects($this->once()) ->method('setFragmentPath') ->with('/bar/foo'); - $renderer = new DecoratedFragmentRenderer($innerRenderer); + $renderer = new DecoratedFragmentRenderer($innerRenderer, new SiteAccessSerializer($this->getSerializer())); $renderer->setSiteAccess($siteAccess); $renderer->setFragmentPath('/foo'); } @@ -66,7 +75,7 @@ public function testGetName() ->method('getName') ->will($this->returnValue($name)); - $renderer = new DecoratedFragmentRenderer($this->innerRenderer); + $renderer = $this->getRenderer(); $this->assertSame($name, $renderer->getName()); } @@ -82,7 +91,7 @@ public function testRendererAbsoluteUrl() ->with($url, $request, $options) ->will($this->returnValue($expectedReturn)); - $renderer = new DecoratedFragmentRenderer($this->innerRenderer); + $renderer = $this->getRenderer(); $this->assertSame($expectedReturn, $renderer->render($url, $request, $options)); } @@ -105,7 +114,7 @@ public function testRendererControllerReference() ->with($reference, $request, $options) ->will($this->returnValue($expectedReturn)); - $renderer = new DecoratedFragmentRenderer($this->innerRenderer); + $renderer = $this->getRenderer(); $this->assertSame($expectedReturn, $renderer->render($reference, $request, $options)); $this->assertTrue(isset($reference->attributes['serialized_siteaccess'])); $serializedSiteAccess = json_encode($siteAccess); @@ -129,9 +138,12 @@ public function getRequest(SiteAccess $siteAccess): Request return $request; } + /** + * @return \Symfony\Component\HttpKernel\Fragment\FragmentRendererInterface&\Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessAware + */ public function getRenderer(): FragmentRendererInterface { - return new DecoratedFragmentRenderer($this->innerRenderer); + return new DecoratedFragmentRenderer($this->innerRenderer, new SiteAccessSerializer($this->getSerializer())); } } diff --git a/tests/bundle/Core/Fragment/InlineFragmentRendererTest.php b/tests/bundle/Core/Fragment/InlineFragmentRendererTest.php index 461504678f..a57ef663f7 100644 --- a/tests/bundle/Core/Fragment/InlineFragmentRendererTest.php +++ b/tests/bundle/Core/Fragment/InlineFragmentRendererTest.php @@ -7,14 +7,21 @@ namespace Ibexa\Tests\Bundle\Core\Fragment; use Ibexa\Bundle\Core\Fragment\InlineFragmentRenderer; +use Ibexa\Bundle\Core\Fragment\SiteAccessSerializer; +use Ibexa\Core\MVC\Symfony\Component\Serializer\SerializerTrait; use Ibexa\Core\MVC\Symfony\SiteAccess; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ControllerReference; use Symfony\Component\HttpKernel\Fragment\FragmentRendererInterface; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; +/** + * @covers \Ibexa\Bundle\Core\Fragment\InlineFragmentRenderer + */ class InlineFragmentRendererTest extends DecoratedFragmentRendererTest { + use SerializerTrait; + public function testRendererControllerReference() { $reference = new ControllerReference('FooBundle:bar:baz'); @@ -36,7 +43,7 @@ public function testRendererControllerReference() ->with($reference, $request, $options) ->will($this->returnValue($expectedReturn)); - $renderer = new InlineFragmentRenderer($this->innerRenderer); + $renderer = $this->getRenderer(); $this->assertSame($expectedReturn, $renderer->render($reference, $request, $options)); $this->assertTrue(isset($reference->attributes['serialized_siteaccess'])); $serializedSiteAccess = json_encode($siteAccess); @@ -80,7 +87,7 @@ public function getRequest(SiteAccess $siteAccess): Request public function getRenderer(): FragmentRendererInterface { - return new InlineFragmentRenderer($this->innerRenderer); + return new InlineFragmentRenderer($this->innerRenderer, new SiteAccessSerializer($this->getSerializer())); } } diff --git a/tests/bundle/Core/Fragment/SiteAccessSerializerTest.php b/tests/bundle/Core/Fragment/SiteAccessSerializerTest.php new file mode 100644 index 0000000000..c771c3483c --- /dev/null +++ b/tests/bundle/Core/Fragment/SiteAccessSerializerTest.php @@ -0,0 +1,78 @@ +createMock(SerializerInterface::class); + $siteAccessSerializer = new SiteAccessSerializer($serializerMock); + + $controllerReference = new ControllerReference('foo'); + + $serializerMock->method('serialize') + ->with( + self::isInstanceOf(SiteAccess\Matcher::class), + 'json', + self::isType('array') + )->willReturn('{"foo":"bar"}') + ; + + $siteAccessSerializer->serializeSiteAccessAsControllerAttributes($siteAccess, $controllerReference); + + self::assertJson($controllerReference->attributes['serialized_siteaccess']); + + // this just tests internal flow instead of actual serializer, covered elsewhere. Hence, comparing to mocked values + self::assertSame('{"foo":"bar"}', $controllerReference->attributes['serialized_siteaccess_matcher']); + if ($siteAccess->matcher instanceof SiteAccess\Matcher\CompoundInterface) { + foreach ($siteAccess->matcher->getSubMatchers() as $subMatcher) { + self::assertJson( + $controllerReference->attributes['serialized_siteaccess_sub_matchers'][get_class($subMatcher)] + ); + } + } + } + + /** + * @return iterable + */ + public static function getDataForTestSerializeSiteAccessAsControllerAttributes(): iterable + { + yield 'SiteAccess with simple matcher' => [ + new SiteAccess('foo', SiteAccess::DEFAULT_MATCHING_TYPE, new SiteAccess\Matcher\URIElement(1)), + ]; + + yield 'SiteAccess with compound matcher' => [ + new SiteAccess( + 'foo', + SiteAccess::DEFAULT_MATCHING_TYPE, + new CompoundStub( + [ + new SiteAccess\Matcher\HostElement(2), + new SiteAccess\Matcher\URIElement(2), + ] + ) + ), + ]; + } +} diff --git a/tests/lib/MVC/Symfony/EventListener/SiteAccessMatchListenerTest.php b/tests/lib/MVC/Symfony/EventListener/SiteAccessMatchListenerTest.php index 402cbd0c9d..0c37ea49ca 100644 --- a/tests/lib/MVC/Symfony/EventListener/SiteAccessMatchListenerTest.php +++ b/tests/lib/MVC/Symfony/EventListener/SiteAccessMatchListenerTest.php @@ -7,12 +7,24 @@ namespace Ibexa\Tests\Core\MVC\Symfony\EventListener; use Ibexa\Bundle\Core\SiteAccess\SiteAccessMatcherRegistryInterface; -use Ibexa\Core\MVC\Symfony\Component\Serializer\SerializerTrait; +use Ibexa\Core\MVC\Symfony\Component\Serializer\CompoundMatcherNormalizer; +use Ibexa\Core\MVC\Symfony\Component\Serializer\HostElementNormalizer; +use Ibexa\Core\MVC\Symfony\Component\Serializer\HostTextNormalizer; +use Ibexa\Core\MVC\Symfony\Component\Serializer\MapNormalizer; +use Ibexa\Core\MVC\Symfony\Component\Serializer\MatcherDenormalizer; +use Ibexa\Core\MVC\Symfony\Component\Serializer\RegexHostNormalizer; +use Ibexa\Core\MVC\Symfony\Component\Serializer\RegexNormalizer; +use Ibexa\Core\MVC\Symfony\Component\Serializer\RegexURINormalizer; +use Ibexa\Core\MVC\Symfony\Component\Serializer\SimplifiedRequestNormalizer; +use Ibexa\Core\MVC\Symfony\Component\Serializer\SiteAccessNormalizer; +use Ibexa\Core\MVC\Symfony\Component\Serializer\URIElementNormalizer; +use Ibexa\Core\MVC\Symfony\Component\Serializer\URITextNormalizer; use Ibexa\Core\MVC\Symfony\Event\PostSiteAccessMatchEvent; use Ibexa\Core\MVC\Symfony\EventListener\SiteAccessMatchListener; use Ibexa\Core\MVC\Symfony\MVCEvents; use Ibexa\Core\MVC\Symfony\Routing\SimplifiedRequest; use Ibexa\Core\MVC\Symfony\SiteAccess; +use Ibexa\Core\MVC\Symfony\SiteAccess\Matcher; use Ibexa\Core\MVC\Symfony\SiteAccess\Router; use Ibexa\Core\MVC\Symfony\SiteAccessGroup; use PHPUnit\Framework\TestCase; @@ -21,85 +33,109 @@ use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; +use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; +use Symfony\Component\Serializer\Normalizer\JsonSerializableNormalizer; +use Symfony\Component\Serializer\Normalizer\PropertyNormalizer; +use Symfony\Component\Serializer\Serializer; +use Symfony\Component\Serializer\SerializerInterface; -class SiteAccessMatchListenerTest extends TestCase +/** + * @covers \Ibexa\Core\MVC\Symfony\EventListener\SiteAccessMatchListener + */ +final class SiteAccessMatchListenerTest extends TestCase { - use SerializerTrait; + /** @var \PHPUnit\Framework\MockObject\MockObject&\Ibexa\Core\MVC\Symfony\SiteAccess\Router */ + private Router $saRouter; - /** @var \PHPUnit\Framework\MockObject\MockObject */ - private $saRouter; + /** @var \PHPUnit\Framework\MockObject\MockObject&\Symfony\Component\EventDispatcher\EventDispatcherInterface */ + private EventDispatcherInterface $eventDispatcher; - /** @var \PHPUnit\Framework\MockObject\MockObject */ - private $eventDispatcher; + /** @var \PHPUnit\Framework\MockObject\MockObject&\Ibexa\Bundle\Core\SiteAccess\SiteAccessMatcherRegistryInterface */ + private SiteAccessMatcherRegistryInterface $registry; - /** @var \Ibexa\Core\MVC\Symfony\EventListener\SiteAccessMatchListener */ - private $listener; + private SiteAccessMatchListener $listener; protected function setUp(): void { parent::setUp(); $this->saRouter = $this->createMock(Router::class); $this->eventDispatcher = $this->createMock(EventDispatcherInterface::class); - $matcherRegistryMock = $this->createMock(SiteAccessMatcherRegistryInterface::class); - $matcherRegistryMock->method('hasMatcher')->willReturn(false); + $this->registry = $this->createMock(SiteAccessMatcherRegistryInterface::class); $this->listener = new SiteAccessMatchListener( $this->saRouter, $this->eventDispatcher, - $matcherRegistryMock + $this->getSerializer() ); } - public function testGetSubscribedEvents() + public function testGetSubscribedEvents(): void { - $this->assertSame( + self::assertSame( [KernelEvents::REQUEST => ['onKernelRequest', 45]], SiteAccessMatchListener::getSubscribedEvents() ); } - public function testOnKernelRequestSerializedSA() - { - $matcher = new SiteAccess\Matcher\URIElement(1); - $siteAccess = new SiteAccess( + /** + * @param \Ibexa\Core\MVC\Symfony\SiteAccessGroup[] $groups + */ + protected function createSiteAccess( + ?Matcher $matcher = null, + ?string $provider = null, + array $groups = [] + ): SiteAccess { + return new SiteAccess( 'test', 'matching_type', $matcher, - null, - [new SiteAccessGroup('test_group')] + $provider, + $groups ); + } + + /** + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException + */ + public function createAndDispatchRequest(SiteAccess $siteAccess): Request + { $request = new Request(); - $request->attributes->set('serialized_siteaccess', json_encode($siteAccess)); - $request->attributes->set( - 'serialized_siteaccess_matcher', - $this->getSerializer()->serialize( - $siteAccess->matcher, - 'json', - [AbstractNormalizer::IGNORED_ATTRIBUTES => ['request', 'container', 'matcherBuilder']] - ) - ); - $event = new RequestEvent( - $this->createMock(HttpKernelInterface::class), - $request, - HttpKernelInterface::MASTER_REQUEST - ); + $request->attributes->set('serialized_siteaccess', $this->serializeSiteAccess($siteAccess)); - $this->saRouter - ->expects($this->never()) - ->method('match'); + $request->attributes->set('serialized_siteaccess_matcher', $this->serializeMatcher($siteAccess)); + if ($siteAccess->matcher instanceof Matcher\Compound) { + $request->attributes->set( + 'serialized_siteaccess_sub_matchers', + $this->serializeSubMatchers($siteAccess->matcher) + ); + } - $postSAMatchEvent = new PostSiteAccessMatchEvent($siteAccess, $request, $event->getRequestType()); - $this->eventDispatcher - ->expects($this->once()) - ->method('dispatch') - ->with($this->equalTo($postSAMatchEvent), MVCEvents::SITEACCESS); + $this->dispatchRequestEvent($request, $siteAccess); + self::assertEquals($siteAccess, $request->attributes->get('siteaccess')); - $this->listener->onKernelRequest($event); - $this->assertEquals($siteAccess, $request->attributes->get('siteaccess')); - $this->assertFalse($request->attributes->has('serialized_siteaccess')); + return $request; } - public function testOnKernelRequestSerializedSAWithCompoundMatcher() + /** + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException + */ + public function testOnKernelRequestSerializedSA(): void + { + $matcher = new SiteAccess\Matcher\URIElement(1); + $siteAccess = $this->createSiteAccess($matcher, null, [new SiteAccessGroup('test_group')]); + $request = $this->createAndDispatchRequest($siteAccess); + + self::assertFalse($request->attributes->has('serialized_siteaccess')); + } + + /** + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException + */ + public function testOnKernelRequestSerializedSAWithCompoundMatcher(): void { $compoundMatcher = new SiteAccess\Matcher\Compound\LogicalAnd([]); $subMatchers = [ @@ -112,75 +148,64 @@ public function testOnKernelRequestSerializedSAWithCompoundMatcher() 'matching_type', $compoundMatcher ); - $request = new Request(); - $request->attributes->set('serialized_siteaccess', json_encode($siteAccess)); - $request->attributes->set( - 'serialized_siteaccess_matcher', - $this->getSerializer()->serialize( - $siteAccess->matcher, - 'json', - [AbstractNormalizer::IGNORED_ATTRIBUTES => ['request', 'container', 'matcherBuilder']] - ) - ); - $serializedSubMatchers = []; - foreach ($subMatchers as $subMatcher) { - $serializedSubMatchers[get_class($subMatcher)] = $this->getSerializer()->serialize( - $subMatcher, - 'json', - [AbstractNormalizer::IGNORED_ATTRIBUTES => ['request', 'container', 'matcherBuilder']] - ); - } - $request->attributes->set( - 'serialized_siteaccess_sub_matchers', - $serializedSubMatchers - ); - $event = new RequestEvent( - $this->createMock(HttpKernelInterface::class), - $request, - HttpKernelInterface::MASTER_REQUEST - ); + $request = $this->createAndDispatchRequest($siteAccess); + self::assertFalse($request->attributes->has('serialized_siteaccess')); + } - $this->saRouter - ->expects($this->never()) - ->method('match'); + /** + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException + */ + public function testOnKernelRequestSerializedSAWithMatcherInMatcherRegistry(): void + { + $matcher = new TestMatcher([]); - $postSAMatchEvent = new PostSiteAccessMatchEvent($siteAccess, $request, $event->getRequestType()); - $this->eventDispatcher - ->expects($this->once()) - ->method('dispatch') - ->with($this->equalTo($postSAMatchEvent), MVCEvents::SITEACCESS); + $matcher2 = new TestMatcher([]); + $matcher2->setMapKey('key_foobar'); - $this->listener->onKernelRequest($event); - $this->assertEquals($siteAccess, $request->attributes->get('siteaccess')); - $this->assertFalse($request->attributes->has('serialized_siteaccess')); + $this->registry + ->expects(self::once()) + ->method('hasMatcher') + ->with(TestMatcher::class) + ->willReturn(true); + + $this->registry + ->expects(self::once()) + ->method('getMatcher') + ->with(TestMatcher::class) + ->willReturn($matcher); + + $siteAccess = $this->createSiteAccess( + $matcher2, + null, + [new SiteAccessGroup('test_group')] + ); + + $request = $this->createAndDispatchRequest($siteAccess); + /** @var \Ibexa\Tests\Core\MVC\Symfony\EventListener\TestMatcher $siteAccessMatcher */ + $siteAccessMatcher = $siteAccess->matcher; + self::assertEquals('key_foobar', $siteAccessMatcher->getMapKey()); + self::assertFalse($request->attributes->has('serialized_siteaccess')); } - public function testOnKernelRequestSiteAccessPresent() + /** + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException + */ + public function testOnKernelRequestSiteAccessPresent(): void { $siteAccess = new SiteAccess('test'); $request = new Request(); $request->attributes->set('siteaccess', $siteAccess); - $event = new RequestEvent( - $this->createMock(HttpKernelInterface::class), - $request, - HttpKernelInterface::MASTER_REQUEST - ); - - $this->saRouter - ->expects($this->never()) - ->method('match'); - - $postSAMatchEvent = new PostSiteAccessMatchEvent($siteAccess, $request, $event->getRequestType()); - $this->eventDispatcher - ->expects($this->once()) - ->method('dispatch') - ->with($this->equalTo($postSAMatchEvent), MVCEvents::SITEACCESS); - - $this->listener->onKernelRequest($event); + $this->dispatchRequestEvent($request, $siteAccess); $this->assertSame($siteAccess, $request->attributes->get('siteaccess')); } - public function testOnKernelRequest() + /** + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException + */ + public function testOnKernelRequest(): void { $siteAccess = new SiteAccess('test'); $scheme = 'https'; @@ -188,41 +213,14 @@ public function testOnKernelRequest() $port = 1234; $path = '/foo/bar'; $request = Request::create(sprintf('%s://%s:%d%s', $scheme, $host, $port, $path)); - $event = new RequestEvent( - $this->createMock(HttpKernelInterface::class), - $request, - HttpKernelInterface::MASTER_REQUEST - ); - - $simplifiedRequest = new SimplifiedRequest( - [ - 'scheme' => $request->getScheme(), - 'host' => $request->getHost(), - 'port' => $request->getPort(), - 'pathinfo' => $request->getPathInfo(), - 'queryParams' => $request->query->all(), - 'languages' => $request->getLanguages(), - 'headers' => $request->headers->all(), - ] - ); - - $this->saRouter - ->expects($this->once()) - ->method('match') - ->with($this->equalTo($simplifiedRequest)) - ->will($this->returnValue($siteAccess)); - - $postSAMatchEvent = new PostSiteAccessMatchEvent($siteAccess, $request, $event->getRequestType()); - $this->eventDispatcher - ->expects($this->once()) - ->method('dispatch') - ->with($this->equalTo($postSAMatchEvent), MVCEvents::SITEACCESS); - - $this->listener->onKernelRequest($event); - $this->assertSame($siteAccess, $request->attributes->get('siteaccess')); + $this->assertRequestHasSiteAccess($request, null, $siteAccess); } - public function testOnKernelRequestUserHashWithOriginalRequest() + /** + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException + */ + public function testOnKernelRequestUserHashWithOriginalRequest(): void { $siteAccess = new SiteAccess('test'); $scheme = 'https'; @@ -232,10 +230,28 @@ public function testOnKernelRequestUserHashWithOriginalRequest() $originalRequest = Request::create(sprintf('%s://%s:%d%s', $scheme, $host, $port, $path)); $request = Request::create('http://localhost/_fos_user_hash'); $request->attributes->set('_ez_original_request', $originalRequest); + $this->assertRequestHasSiteAccess($request, $originalRequest, $siteAccess); + } + + private function serializeSiteAccess(SiteAccess $siteAccess): string + { + return $this->getSerializer()->serialize($siteAccess, 'json'); + } + + /** + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException + */ + private function assertRequestHasSiteAccess( + Request $request, + ?Request $originalRequest, + SiteAccess $siteAccess + ): void { + $originalRequest ??= $request; $event = new RequestEvent( $this->createMock(HttpKernelInterface::class), $request, - HttpKernelInterface::MASTER_REQUEST + HttpKernelInterface::MAIN_REQUEST ); $simplifiedRequest = new SimplifiedRequest( @@ -251,20 +267,97 @@ public function testOnKernelRequestUserHashWithOriginalRequest() ); $this->saRouter - ->expects($this->once()) + ->expects(self::once()) ->method('match') - ->with($this->equalTo($simplifiedRequest)) - ->will($this->returnValue($siteAccess)); + ->with($simplifiedRequest) + ->willReturn($siteAccess) + ; $postSAMatchEvent = new PostSiteAccessMatchEvent($siteAccess, $request, $event->getRequestType()); $this->eventDispatcher ->expects($this->once()) ->method('dispatch') - ->with($this->equalTo($postSAMatchEvent), MVCEvents::SITEACCESS); + ->with($this->equalTo($postSAMatchEvent), MVCEvents::SITEACCESS) + ; $this->listener->onKernelRequest($event); $this->assertSame($siteAccess, $request->attributes->get('siteaccess')); } + + /** + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException + */ + private function dispatchRequestEvent(Request $request, SiteAccess $siteAccess): void + { + $event = new RequestEvent( + $this->createMock(HttpKernelInterface::class), + $request, + HttpKernelInterface::MAIN_REQUEST + ); + + $this->saRouter + ->expects(self::never()) + ->method('match'); + + $postSAMatchEvent = new PostSiteAccessMatchEvent($siteAccess, $request, $event->getRequestType()); + $this->eventDispatcher + ->expects(self::once()) + ->method('dispatch') + ->with($postSAMatchEvent, MVCEvents::SITEACCESS); + + $this->listener->onKernelRequest($event); + } + + private function getSerializer(): SerializerInterface + { + return new Serializer( + [ + new ArrayDenormalizer(), + new SiteAccessNormalizer(), + new MatcherDenormalizer($this->registry), + new CompoundMatcherNormalizer(), + new MapNormalizer(), + new HostElementNormalizer(), + new URITextNormalizer(), + new HostTextNormalizer(), + new RegexURINormalizer(), + new RegexHostNormalizer(), + new RegexNormalizer(), + new URIElementNormalizer(), + new SimplifiedRequestNormalizer(), + new JsonSerializableNormalizer(), + new PropertyNormalizer(), + ], + [new JsonEncoder()] + ); + } + + private function serializeMatcher(SiteAccess $siteAccess): string + { + return $this->getSerializer()->serialize( + $siteAccess->matcher, + 'json', + [AbstractNormalizer::IGNORED_ATTRIBUTES => ['request', 'container', 'matcherBuilder']] + ); + } + + /** + * @phpstan-return array, string> + */ + private function serializeSubMatchers(Matcher\Compound $compoundMatcher): array + { + $serializedSubMatchers = []; + foreach ($compoundMatcher->getSubMatchers() as $subMatcher) { + $serializedSubMatchers[get_class($subMatcher)] = $this->getSerializer()->serialize( + $subMatcher, + 'json', + [AbstractNormalizer::IGNORED_ATTRIBUTES => ['request', 'container', 'matcherBuilder']] + ); + } + + return $serializedSubMatchers; + } } class_alias(SiteAccessMatchListenerTest::class, 'eZ\Publish\Core\MVC\Symfony\EventListener\Tests\SiteAccessMatchListenerTest'); diff --git a/tests/lib/MVC/Symfony/EventListener/TestMatcher.php b/tests/lib/MVC/Symfony/EventListener/TestMatcher.php new file mode 100644 index 0000000000..cf8ea8bcc9 --- /dev/null +++ b/tests/lib/MVC/Symfony/EventListener/TestMatcher.php @@ -0,0 +1,20 @@ +serializeMatcher($matcher); - $unserializedMatcher = $this->deserializeMatcher($serializedMatcher, get_class($matcher)); + + $context = []; + // BC layer + if ($matcher instanceof Matcher\CompoundInterface) { + $subMatchers = $matcher->getSubMatchers(); + foreach ($subMatchers as $subMatcher) { + $context['serialized_siteaccess_sub_matchers'][get_class($subMatcher)] = $this->serializeMatcher($subMatcher); + } + } + // -- + $unserializedMatcher = $this->deserializeMatcher($serializedMatcher, get_class($matcher), $context); $expected = $expected ?? $matcher; $this->assertEquals($expected, $unserializedMatcher); @@ -40,29 +50,31 @@ private function serializeMatcher(Matcher $matcher) } /** - * @param string $serializedMatcher - * @param string $matcherFQCN - * - * @return \Ibexa\Core\MVC\Symfony\SiteAccess\Matcher|object + * @param array $context */ - private function deserializeMatcher($serializedMatcher, $matcherFQCN) + private function deserializeMatcher(string $serializedMatcher, string $matcherFQCN, array $context): Matcher { return $this->getSerializer()->deserialize( $serializedMatcher, $matcherFQCN, - 'json' + 'json', + $context ); } - public function matcherProvider() + /** + * @return iterable + */ + public function matcherProvider(): iterable { $subMatchers = [ - 'Map\URI' => [ - 'map' => ['key' => 'value'], - ], - 'Map\Host' => [ - 'map' => ['key' => 'value'], - ], + Matcher\Map\URI::class => new Matcher\Map\URI(['map' => ['key' => 'value']]), + Matcher\Map\Host::class => new Matcher\Map\Host(['map' => ['key' => 'value']]), + ]; + // data truncated due to https://issues.ibexa.co/browse/EZP-31810 + $expectedSubMatchers = [ + Matcher\Map\URI::class => new Matcher\Map\URI([]), + Matcher\Map\Host::class => new Matcher\Map\Host([]), ]; $logicalAnd = new Matcher\Compound\LogicalAnd( [ @@ -73,7 +85,7 @@ public function matcherProvider() ); $logicalAnd->setSubMatchers($subMatchers); $expectedLogicalAnd = new Matcher\Compound\LogicalAnd([]); - $expectedLogicalAnd->setSubMatchers($subMatchers); + $expectedLogicalAnd->setSubMatchers($expectedSubMatchers); $logicalOr = new Matcher\Compound\LogicalOr( [ @@ -84,60 +96,73 @@ public function matcherProvider() ); $logicalOr->setSubMatchers($subMatchers); $expectedLogicalOr = new Matcher\Compound\LogicalOr([]); - $expectedLogicalOr->setSubMatchers($subMatchers); + $expectedLogicalOr->setSubMatchers($expectedSubMatchers); $expectedMapURI = new Matcher\Map\URI([]); $expectedMapURI->setMapKey('site'); - return [ - 'URIText' => [ - new Matcher\URIText([ + yield 'URIText' => [ + new Matcher\URIText( + [ 'prefix' => 'foo', 'suffix' => 'bar', - ]), - ], - 'HostText' => [ - new Matcher\HostText([ + ] + ), + ]; + yield 'HostText' => [ + new Matcher\HostText( + [ 'prefix' => 'foo', 'suffix' => 'bar', - ]), - ], - 'RegexHost' => [ - new Matcher\Regex\Host([ + ] + ), + ]; + yield 'RegexHost' => [ + new Matcher\Regex\Host( + [ 'regex' => 'foo', 'itemNumber' => 2, - ]), - ], - 'RegexURI' => [ - new Matcher\Regex\URI([ + ] + ), + ]; + yield 'RegexURI' => [ + new Matcher\Regex\URI( + [ 'regex' => 'foo', 'itemNumber' => 2, - ]), - ], - 'URIElement' => [ - new Matcher\URIElement([ + ] + ), + ]; + yield 'URIElement' => [ + new Matcher\URIElement( + [ 'elementNumber' => 2, - ]), - ], - 'HostElement' => [ - new Matcher\HostElement([ + ] + ), + ]; + yield 'HostElement' => [ + new Matcher\HostElement( + [ 'elementNumber' => 2, - ]), - ], - 'MapURI' => $this->getMapURIMatcherTestCase(), - 'MapPort' => $this->getMapPortMatcherTestCase(), - 'MapHost' => $this->getMapHostMatcherTestCase(), - 'CompoundAnd' => [ - $logicalAnd, - $expectedLogicalAnd, - ], - 'CompoundOr' => [ - $logicalOr, - $expectedLogicalOr, - ], + ] + ), + ]; + yield 'MapURI' => $this->getMapURIMatcherTestCase(); + yield 'MapPort' => $this->getMapPortMatcherTestCase(); + yield 'MapHost' => $this->getMapHostMatcherTestCase(); + yield 'CompoundAnd' => [ + $logicalAnd, + $expectedLogicalAnd, + ]; + yield 'CompoundOr' => [ + $logicalOr, + $expectedLogicalOr, ]; } + /** + * @return array{\Ibexa\Core\MVC\Symfony\SiteAccess\Matcher, \Ibexa\Core\MVC\Symfony\SiteAccess\Matcher} + */ private function getMapPortMatcherTestCase(): array { $matcherBeforeSerialization = new Matcher\Map\Port(['map' => ['key' => 'value']]); @@ -149,6 +174,9 @@ private function getMapPortMatcherTestCase(): array return [$matcherBeforeSerialization, $matcherAfterDeserialization]; } + /** + * @return array{\Ibexa\Core\MVC\Symfony\SiteAccess\Matcher, \Ibexa\Core\MVC\Symfony\SiteAccess\Matcher} + */ private function getMapHostMatcherTestCase(): array { $matcherBeforeSerialization = new Matcher\Map\Host(['map' => ['key' => 'value']]); @@ -160,6 +188,9 @@ private function getMapHostMatcherTestCase(): array return [$matcherBeforeSerialization, $matcherAfterDeserialization]; } + /** + * @return array{\Ibexa\Core\MVC\Symfony\SiteAccess\Matcher, \Ibexa\Core\MVC\Symfony\SiteAccess\Matcher} + */ private function getMapURIMatcherTestCase(): array { $matcherBeforeSerialization = new Matcher\Map\URI(['map' => ['key' => 'value']]);