From abce25aa67d5cd76b35a7426d7b0a8e57dfb0a14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Bia=C5=82czak?= Date: Mon, 10 Aug 2026 07:19:17 +0200 Subject: [PATCH 1/2] IBX-12102: Fixed SiteAccess context being lost after fragment sub-requests --- src/bundle/Core/Resources/config/routing.yml | 8 ++ .../Serializer/URIElementNormalizer.php | 12 ++ .../SiteAccessRestoreListener.php | 69 +++++++++ .../Serializer/URIElementNormalizerTest.php | 22 ++- .../SiteAccessRestoreListenerTest.php | 135 ++++++++++++++++++ 5 files changed, 242 insertions(+), 4 deletions(-) create mode 100644 src/lib/MVC/Symfony/EventListener/SiteAccessRestoreListener.php create mode 100644 tests/lib/MVC/Symfony/EventListener/SiteAccessRestoreListenerTest.php diff --git a/src/bundle/Core/Resources/config/routing.yml b/src/bundle/Core/Resources/config/routing.yml index a065e1be81..9cfd1eded3 100644 --- a/src/bundle/Core/Resources/config/routing.yml +++ b/src/bundle/Core/Resources/config/routing.yml @@ -26,6 +26,14 @@ services: tags: - { name: kernel.event_subscriber } + ibexa.siteaccess_restore_listener: + class: Ibexa\Core\MVC\Symfony\EventListener\SiteAccessRestoreListener + arguments: + $requestStack: '@request_stack' + $eventDispatcher: '@event_dispatcher' + tags: + - { name: kernel.event_subscriber } + Ibexa\Core\MVC\Symfony\Component\Serializer\CompoundMatcherNormalizer: autoconfigure: false diff --git a/src/lib/MVC/Symfony/Component/Serializer/URIElementNormalizer.php b/src/lib/MVC/Symfony/Component/Serializer/URIElementNormalizer.php index 39f60b0792..e78b03328b 100644 --- a/src/lib/MVC/Symfony/Component/Serializer/URIElementNormalizer.php +++ b/src/lib/MVC/Symfony/Component/Serializer/URIElementNormalizer.php @@ -17,6 +17,18 @@ public function supportsNormalization($data, ?string $format = null) return $data instanceof URIElement; } + public function normalize($object, ?string $format = null, array $context = []) + { + // Trigger lazy initialization of URIElement::$uriElements from the matched request, + // so the serialized representation stays usable after deserialization, where + // the request is not available (IBX-12102). + if ($object instanceof URIElement) { + $object->match(); + } + + return parent::normalize($object, $format, $context); + } + /** * @see \Ibexa\Core\MVC\Symfony\SiteAccess\Matcher\URIElement::__sleep */ diff --git a/src/lib/MVC/Symfony/EventListener/SiteAccessRestoreListener.php b/src/lib/MVC/Symfony/EventListener/SiteAccessRestoreListener.php new file mode 100644 index 0000000000..7f21fad7d0 --- /dev/null +++ b/src/lib/MVC/Symfony/EventListener/SiteAccessRestoreListener.php @@ -0,0 +1,69 @@ +requestStack = $requestStack; + $this->eventDispatcher = $eventDispatcher; + } + + public static function getSubscribedEvents(): array + { + return [ + KernelEvents::FINISH_REQUEST => ['onKernelFinishRequest', 0], + ]; + } + + public function onKernelFinishRequest(FinishRequestEvent $event): void + { + if ($event->isMainRequest()) { + return; + } + + // The finishing request is still on the stack, so this is the request control returns to + $parentRequest = $this->requestStack->getParentRequest(); + if ($parentRequest === null) { + return; + } + + $siteAccess = $parentRequest->attributes->get('siteaccess'); + if (!$siteAccess instanceof SiteAccess) { + return; + } + + $this->eventDispatcher->dispatch( + new PostSiteAccessMatchEvent($siteAccess, $parentRequest, HttpKernelInterface::SUB_REQUEST), + MVCEvents::SITEACCESS + ); + } +} diff --git a/tests/lib/MVC/Symfony/Component/Serializer/URIElementNormalizerTest.php b/tests/lib/MVC/Symfony/Component/Serializer/URIElementNormalizerTest.php index 3bf5477672..cd10bd4b44 100644 --- a/tests/lib/MVC/Symfony/Component/Serializer/URIElementNormalizerTest.php +++ b/tests/lib/MVC/Symfony/Component/Serializer/URIElementNormalizerTest.php @@ -17,15 +17,19 @@ final class URIElementNormalizerTest extends TestCase { - public function testNormalization(): void + /** + * @dataProvider provideForTestNormalization + */ + public function testNormalization(bool $initializeUriElements): void { $normalizer = new URIElementNormalizer(); $normalizer->setSerializer(new SerializerStub()); $matcher = new URIElement(2); - // Set request and invoke match to initialize HostElement::$hostElements - $matcher->setRequest(SimplifiedRequest::fromUrl('http://ezpublish.dev/foo/bar')); - $matcher->match(); + $matcher->setRequest(SimplifiedRequest::fromUrl('https://ezpublish.dev/foo/bar')); + if ($initializeUriElements) { + $matcher->match(); + } $this->assertEquals( [ @@ -36,6 +40,16 @@ public function testNormalization(): void ); } + /** + * @return iterable + */ + public static function provideForTestNormalization(): iterable + { + yield 'uriElements initialized by match()' => [true]; + // uriElements must be computed from the request during normalization (IBX-12102) + yield 'uriElements not yet initialized' => [false]; + } + public function testSupportsNormalization(): void { $normalizer = new URIElementNormalizer(); diff --git a/tests/lib/MVC/Symfony/EventListener/SiteAccessRestoreListenerTest.php b/tests/lib/MVC/Symfony/EventListener/SiteAccessRestoreListenerTest.php new file mode 100644 index 0000000000..fc56def369 --- /dev/null +++ b/tests/lib/MVC/Symfony/EventListener/SiteAccessRestoreListenerTest.php @@ -0,0 +1,135 @@ +kernel = $this->createMock(HttpKernelInterface::class); + $this->eventDispatcher = $this->createMock(EventDispatcherInterface::class); + $this->requestStack = new RequestStack(); + $this->listener = new SiteAccessRestoreListener( + $this->requestStack, + $this->eventDispatcher + ); + } + + public function testGetSubscribedEvents(): void + { + self::assertSame( + [KernelEvents::FINISH_REQUEST => ['onKernelFinishRequest', 0]], + SiteAccessRestoreListener::getSubscribedEvents() + ); + } + + public function testRestoresParentSiteAccessWhenSubRequestFinishes(): void + { + $parentSiteAccess = new SiteAccess('site_fr', 'uri:element'); + $parentRequest = self::createRequestWithSiteAccess($parentSiteAccess); + $subRequest = self::createRequestWithSiteAccess(new SiteAccess('admin', 'uri:element')); + + $this->requestStack->push($parentRequest); + $this->requestStack->push($subRequest); + + $this->eventDispatcher + ->expects(self::once()) + ->method('dispatch') + ->with( + self::callback( + static function (PostSiteAccessMatchEvent $event) use ($parentSiteAccess, $parentRequest): bool { + return $event->getSiteAccess() === $parentSiteAccess + && $event->getRequest() === $parentRequest + && $event->getRequestType() === HttpKernelInterface::SUB_REQUEST; + } + ), + MVCEvents::SITEACCESS + ); + + $this->listener->onKernelFinishRequest( + new FinishRequestEvent($this->kernel, $subRequest, HttpKernelInterface::SUB_REQUEST) + ); + } + + /** + * @dataProvider provideNoDispatchCases + * + * @param \Symfony\Component\HttpFoundation\Request[] $requests + */ + public function testNoDispatch(array $requests, int $requestType): void + { + $finishingRequest = null; + foreach ($requests as $request) { + $this->requestStack->push($request); + $finishingRequest = $request; + } + self::assertNotNull($finishingRequest); + + $this->eventDispatcher->expects(self::never())->method('dispatch'); + + $this->listener->onKernelFinishRequest( + new FinishRequestEvent($this->kernel, $finishingRequest, $requestType) + ); + } + + /** + * @return iterable + */ + public static function provideNoDispatchCases(): iterable + { + yield 'main request' => [ + [self::createRequestWithSiteAccess(new SiteAccess('site'))], + HttpKernelInterface::MAIN_REQUEST, + ]; + + yield 'sub-request without parent request' => [ + [Request::create('/_fragment')], + HttpKernelInterface::SUB_REQUEST, + ]; + + yield 'parent request without siteaccess attribute' => [ + [Request::create('/'), Request::create('/_fragment')], + HttpKernelInterface::SUB_REQUEST, + ]; + } + + private static function createRequestWithSiteAccess(SiteAccess $siteAccess): Request + { + $request = Request::create('/'); + $request->attributes->set('siteaccess', $siteAccess); + + return $request; + } +} From c62673c885b06ff8bccd0d3e9e965fc9a7cf3254 Mon Sep 17 00:00:00 2001 From: konradoboza Date: Mon, 10 Aug 2026 15:29:45 +0200 Subject: [PATCH 2/2] cr remark --- .../Symfony/Component/Serializer/URIElementNormalizerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lib/MVC/Symfony/Component/Serializer/URIElementNormalizerTest.php b/tests/lib/MVC/Symfony/Component/Serializer/URIElementNormalizerTest.php index cd10bd4b44..da225e1f33 100644 --- a/tests/lib/MVC/Symfony/Component/Serializer/URIElementNormalizerTest.php +++ b/tests/lib/MVC/Symfony/Component/Serializer/URIElementNormalizerTest.php @@ -26,7 +26,7 @@ public function testNormalization(bool $initializeUriElements): void $normalizer->setSerializer(new SerializerStub()); $matcher = new URIElement(2); - $matcher->setRequest(SimplifiedRequest::fromUrl('https://ezpublish.dev/foo/bar')); + $matcher->setRequest(SimplifiedRequest::fromUrl('https://ibexa.dev/foo/bar')); if ($initializeUriElements) { $matcher->match(); }