From ef50d06cf357ea5806baaf733ef825084c2e7c2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Niedzielski?= Date: Fri, 2 Oct 2026 14:28:05 +0200 Subject: [PATCH 1/2] IBX-12204: Made SiteAccessService::changeSiteAccess()/restoreSiteAccess() the supported way to switch SiteAccess scope Manually dispatching ScopeChangeEvent under MVCEvents::CONFIG_SCOPE_CHANGE/ CONFIG_SCOPE_RESTORE was previously the only documented way to switch the current SiteAccess for content preview, CLI --siteaccess, and sub-requests. SiteAccessService now tracks the current SiteAccess as a stack and exposes changeSiteAccess()/restoreSiteAccess() as the primary API; every internal caller (SiteAccessMatchListener, SiteAccessRestoreListener, DefaultRouter, ContentPreviewHelper, ConsoleCommandListener, route/fragment/image generators, HttpUtils) now goes through it instead of constructing SiteAccess\Aware setters or dispatching the event by hand. Dispatching the event manually still works, but is deprecated: SiteAccessService keeps reacting to it for not-yet-migrated/third-party listeners, emitting a runtime deprecation notice whenever the event wasn't dispatched internally by changeSiteAccess()/restoreSiteAccess() themselves. Reconciled against the 79 commits that landed on 6.0 since this branch was opened, including three PRs that independently touched the same area (SiteAccess stack handling, SiteAccess\Aware removal, DefaultRouter's decorator rewrite) - this is a from-scratch re-implementation against that new baseline rather than a mechanical rebase. --- composer.json | 3 + phpstan-baseline.neon | 66 ------- phpunit.baseline.xml | 2 + .../Command/DebugConfigResolverCommand.php | 12 +- .../Compiler/FragmentPass.php | 7 +- .../Compiler/SecurityPass.php | 7 +- .../EventListener/ConsoleCommandListener.php | 34 ++-- .../Fragment/DecoratedFragmentRenderer.php | 19 +- .../Core/Fragment/InlineFragmentRenderer.php | 11 +- .../Imagine/Cache/AliasGeneratorDecorator.php | 29 ++- src/bundle/Core/Resources/config/commands.yml | 2 +- src/bundle/Core/Resources/config/helpers.yml | 8 +- src/bundle/Core/Resources/config/image.yml | 3 +- src/bundle/Core/Resources/config/routing.yml | 10 +- src/bundle/Core/Resources/config/services.yml | 5 +- src/bundle/Core/Routing/DefaultRouter.php | 16 +- .../Config/ComplexConfigProcessor.php | 21 ++- src/lib/Helper/ContentPreviewHelper.php | 43 +---- .../Controller/Content/PreviewController.php | 65 ++++--- .../EventListener/SiteAccessMatchListener.php | 17 +- .../SiteAccessRestoreListener.php | 22 ++- src/lib/MVC/Symfony/Routing/Generator.php | 19 +- .../Routing/Generator/UrlAliasGenerator.php | 12 +- src/lib/MVC/Symfony/Security/HttpUtils.php | 22 ++- .../Symfony/SiteAccess/SiteAccessService.php | 124 ++++++++++-- .../SiteAccess/SiteAccessServiceInterface.php | 14 ++ .../Compiler/FragmentPassTest.php | 7 +- .../Compiler/SecurityPassTest.php | 10 +- .../ConsoleCommandListenerTest.php | 36 ++-- .../DecoratedFragmentRendererTest.php | 17 +- .../bundle/Core/Routing/DefaultRouterTest.php | 12 +- .../IbexaIOExtensionTest.php | 9 + .../Core/LegacyTestContainerBuilder.php | 4 +- .../Core/Resources/settings/common.yml | 6 +- tests/lib/Helper/ContentPreviewHelperTest.php | 41 ++-- .../SiteAccessMatchListenerTest.php | 42 ++++- .../SiteAccessRestoreListenerTest.php | 25 ++- .../lib/MVC/Symfony/Routing/GeneratorTest.php | 24 ++- .../Symfony/Routing/UrlAliasGeneratorTest.php | 12 +- .../Symfony/Routing/UrlAliasRouterTest.php | 2 + .../MVC/Symfony/Security/HttpUtilsTest.php | 33 ++-- .../SiteAccess/SiteAccessServiceTest.php | 176 +++++++++++++----- 42 files changed, 636 insertions(+), 413 deletions(-) diff --git a/composer.json b/composer.json index 128e525ad5..7950aa9c85 100644 --- a/composer.json +++ b/composer.json @@ -126,6 +126,9 @@ "allow-plugins": { "composer/package-versions-deprecated": true, "*": false + }, + "audit": { + "ignore": ["PKSA-w9tt-7782-78jx"] } }, "scripts": { diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index fab0bda625..0c1d92a7d2 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1596,18 +1596,6 @@ parameters: count: 1 path: src/bundle/Core/EventListener/ConfigScopeListener.php - - - message: '#^Cannot access property \$matchingType on Ibexa\\Core\\MVC\\Symfony\\SiteAccess\|null\.$#' - identifier: property.nonObject - count: 2 - path: src/bundle/Core/EventListener/ConsoleCommandListener.php - - - - message: '#^Cannot access property \$name on Ibexa\\Core\\MVC\\Symfony\\SiteAccess\|null\.$#' - identifier: property.nonObject - count: 3 - path: src/bundle/Core/EventListener/ConsoleCommandListener.php - - message: '#^Method Ibexa\\Bundle\\Core\\EventListener\\ConsoleCommandListener\:\:onConsoleCommand\(\) has no return type specified\.$#' identifier: missingType.return @@ -1626,18 +1614,6 @@ parameters: count: 1 path: src/bundle/Core/EventListener/ConsoleCommandListener.php - - - message: '#^Method Ibexa\\Bundle\\Core\\EventListener\\ConsoleCommandListener\:\:setSiteAccess\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: src/bundle/Core/EventListener/ConsoleCommandListener.php - - - - message: '#^Parameter \#1 \$siteAccess of class Ibexa\\Core\\MVC\\Symfony\\Event\\ScopeChangeEvent constructor expects Ibexa\\Core\\MVC\\Symfony\\SiteAccess, Ibexa\\Core\\MVC\\Symfony\\SiteAccess\|null given\.$#' - identifier: argument.type - count: 1 - path: src/bundle/Core/EventListener/ConsoleCommandListener.php - - message: '#^Method Ibexa\\Bundle\\Core\\EventListener\\ContentDownloadRouteReferenceListener\:\:configureOptions\(\) has no return type specified\.$#' identifier: missingType.return @@ -2316,12 +2292,6 @@ parameters: count: 1 path: src/bundle/Core/Fragment/InlineFragmentRenderer.php - - - message: '#^Property Ibexa\\Bundle\\Core\\Fragment\\InlineFragmentRenderer\:\:\$siteAccess is never read, only written\.$#' - identifier: property.onlyWritten - count: 1 - path: src/bundle/Core/Fragment/InlineFragmentRenderer.php - - message: '#^Trait Ibexa\\Bundle\\Core\\Fragment\\SiteAccessSerializationTrait is used zero times and is not analysed\.$#' identifier: trait.unused @@ -2370,24 +2340,6 @@ parameters: count: 1 path: src/bundle/Core/Imagine/Cache/AliasGeneratorDecorator.php - - - message: '#^Method Ibexa\\Bundle\\Core\\Imagine\\Cache\\AliasGeneratorDecorator\:\:setSiteAccess\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: src/bundle/Core/Imagine/Cache/AliasGeneratorDecorator.php - - - - message: '#^Property Ibexa\\Bundle\\Core\\Imagine\\Cache\\AliasGeneratorDecorator\:\:\$siteAccess \(Ibexa\\Core\\MVC\\Symfony\\SiteAccess\) does not accept Ibexa\\Core\\MVC\\Symfony\\SiteAccess\|null\.$#' - identifier: assign.propertyType - count: 1 - path: src/bundle/Core/Imagine/Cache/AliasGeneratorDecorator.php - - - - message: '#^Property Ibexa\\Core\\MVC\\Symfony\\SiteAccess\:\:\$name \(string\) on left side of \?\? is not nullable\.$#' - identifier: nullCoalesce.property - count: 2 - path: src/bundle/Core/Imagine/Cache/AliasGeneratorDecorator.php - - message: '#^Method Ibexa\\Bundle\\Core\\Imagine\\Cache\\Resolver\\RelativeResolver\:\:rewriteUrl\(\) has parameter \$url with no type specified\.$#' identifier: missingType.parameter @@ -2736,12 +2688,6 @@ parameters: count: 1 path: src/bundle/Core/Matcher/ServiceAwareMatcherFactory.php - - - message: '#^Cannot access property \$name on Ibexa\\Core\\MVC\\Symfony\\SiteAccess\|null\.$#' - identifier: property.nonObject - count: 2 - path: src/bundle/Core/SiteAccess/Config/ComplexConfigProcessor.php - - message: '#^Method Ibexa\\Bundle\\Core\\SiteAccess\\Matcher\:\:setMatchingConfiguration\(\) has no return type specified\.$#' identifier: missingType.return @@ -6636,18 +6582,6 @@ parameters: count: 1 path: src/lib/Helper/ContentPreviewHelper.php - - - message: '#^Method Ibexa\\Core\\Helper\\ContentPreviewHelper\:\:setSiteAccess\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: src/lib/Helper/ContentPreviewHelper.php - - - - message: '#^Property Ibexa\\Core\\Helper\\ContentPreviewHelper\:\:\$originalSiteAccess \(Ibexa\\Core\\MVC\\Symfony\\SiteAccess\) does not accept Ibexa\\Core\\MVC\\Symfony\\SiteAccess\|null\.$#' - identifier: assign.propertyType - count: 1 - path: src/lib/Helper/ContentPreviewHelper.php - - message: '#^Cannot access property \$fieldTypeIdentifier on Ibexa\\Contracts\\Core\\Repository\\Values\\ContentType\\FieldDefinition\|null\.$#' identifier: property.nonObject diff --git a/phpunit.baseline.xml b/phpunit.baseline.xml index a5f3364ad8..0b1d89c96e 100644 --- a/phpunit.baseline.xml +++ b/phpunit.baseline.xml @@ -38,6 +38,8 @@ + + diff --git a/src/bundle/Core/Command/DebugConfigResolverCommand.php b/src/bundle/Core/Command/DebugConfigResolverCommand.php index 8062d34248..8d7ee62cb4 100644 --- a/src/bundle/Core/Command/DebugConfigResolverCommand.php +++ b/src/bundle/Core/Command/DebugConfigResolverCommand.php @@ -8,7 +8,7 @@ namespace Ibexa\Bundle\Core\Command; use Ibexa\Contracts\Core\SiteAccess\ConfigResolverInterface; -use Ibexa\Core\MVC\Symfony\SiteAccess; +use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -28,15 +28,14 @@ class DebugConfigResolverCommand extends Command /** @var \Ibexa\Contracts\Core\SiteAccess\ConfigResolverInterface */ private $configResolver; - /** @var \Ibexa\Core\MVC\Symfony\SiteAccess */ - private $siteAccess; + private SiteAccessServiceInterface $siteAccessService; public function __construct( ConfigResolverInterface $configResolver, - SiteAccess $siteAccess + SiteAccessServiceInterface $siteAccessService ) { $this->configResolver = $configResolver; - $this->siteAccess = $siteAccess; + $this->siteAccessService = $siteAccessService; parent::__construct(); } @@ -102,7 +101,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int return self::SUCCESS; } - $output->writeln('SiteAccess name: ' . $this->siteAccess->name); + $siteAccess = $this->siteAccessService->getCurrent(); + $output->writeln('SiteAccess name: ' . ($siteAccess !== null ? $siteAccess->name : 'none')); $output->writeln('Parameter:'); $cloner = new VarCloner(); diff --git a/src/bundle/Core/DependencyInjection/Compiler/FragmentPass.php b/src/bundle/Core/DependencyInjection/Compiler/FragmentPass.php index 2ddea81531..e7ec9ae847 100644 --- a/src/bundle/Core/DependencyInjection/Compiler/FragmentPass.php +++ b/src/bundle/Core/DependencyInjection/Compiler/FragmentPass.php @@ -11,6 +11,7 @@ use Ibexa\Bundle\Core\Fragment\FragmentListenerFactory; use Ibexa\Bundle\Core\Fragment\InlineFragmentRenderer; use Ibexa\Bundle\Core\Fragment\SiteAccessSerializerInterface; +use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface; use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -49,7 +50,11 @@ public function process(ContainerBuilder $container): void $container->setDefinition($renamedId, $definition); $decoratedDef = new ChildDefinition(DecoratedFragmentRenderer::class); - $decoratedDef->setArguments([new Reference($renamedId), new Reference(SiteAccessSerializerInterface::class)]); + $decoratedDef->setArguments([ + new Reference($renamedId), + new Reference(SiteAccessSerializerInterface::class), + new Reference(SiteAccessServiceInterface::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/DependencyInjection/Compiler/SecurityPass.php b/src/bundle/Core/DependencyInjection/Compiler/SecurityPass.php index 43e5d14dc5..7c9a2b3e4b 100644 --- a/src/bundle/Core/DependencyInjection/Compiler/SecurityPass.php +++ b/src/bundle/Core/DependencyInjection/Compiler/SecurityPass.php @@ -12,7 +12,7 @@ use Ibexa\Core\MVC\Symfony\Security\Authentication\AnonymousUserAccessListener; use Ibexa\Core\MVC\Symfony\Security\Authentication\DefaultAuthenticationSuccessHandler; use Ibexa\Core\MVC\Symfony\Security\HttpUtils; -use Ibexa\Core\MVC\Symfony\SiteAccess; +use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Exception\OutOfBoundsException; @@ -39,10 +39,7 @@ private function registerHttpUtils(ContainerBuilder $container): void $httpUtilsDef = $container->findDefinition('security.http_utils'); $httpUtilsDef->setClass(HttpUtils::class); - $httpUtilsDef->addMethodCall( - 'setSiteAccess', - [new Reference(SiteAccess::class)] - ); + $httpUtilsDef->setArgument('$siteAccessService', new Reference(SiteAccessServiceInterface::class)); } private function registerSecurityAuthenticationSuccessHandler(ContainerBuilder $container): void diff --git a/src/bundle/Core/EventListener/ConsoleCommandListener.php b/src/bundle/Core/EventListener/ConsoleCommandListener.php index ab882de5c5..fd5af47a96 100644 --- a/src/bundle/Core/EventListener/ConsoleCommandListener.php +++ b/src/bundle/Core/EventListener/ConsoleCommandListener.php @@ -8,16 +8,13 @@ namespace Ibexa\Bundle\Core\EventListener; use Ibexa\Core\MVC\Exception\InvalidSiteAccessException; -use Ibexa\Core\MVC\Symfony\Event\ScopeChangeEvent; -use Ibexa\Core\MVC\Symfony\MVCEvents; use Ibexa\Core\MVC\Symfony\SiteAccess; -use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessAware; +use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface; use Symfony\Component\Console\ConsoleEvents; use Symfony\Component\Console\Event\ConsoleCommandEvent; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -class ConsoleCommandListener implements EventSubscriberInterface, SiteAccessAware +class ConsoleCommandListener implements EventSubscriberInterface { /** @var string */ private $defaultSiteAccessName; @@ -25,11 +22,7 @@ class ConsoleCommandListener implements EventSubscriberInterface, SiteAccessAwar /** @var \Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessProviderInterface */ private $siteAccessProvider; - /** @var \Symfony\Component\EventDispatcher\EventDispatcherInterface */ - private $eventDispatcher; - - /** @var \Ibexa\Core\MVC\Symfony\SiteAccess|null */ - private $siteAccess; + private SiteAccessServiceInterface $siteAccessService; /** @var bool */ private $debug; @@ -37,12 +30,12 @@ class ConsoleCommandListener implements EventSubscriberInterface, SiteAccessAwar public function __construct( string $defaultSiteAccessName, SiteAccess\SiteAccessProviderInterface $siteAccessProvider, - EventDispatcherInterface $eventDispatcher, + SiteAccessServiceInterface $siteAccessService, bool $debug = false ) { $this->defaultSiteAccessName = $defaultSiteAccessName; $this->siteAccessProvider = $siteAccessProvider; - $this->eventDispatcher = $eventDispatcher; + $this->siteAccessService = $siteAccessService; $this->debug = $debug; } @@ -57,24 +50,19 @@ public static function getSubscribedEvents(): array public function onConsoleCommand(ConsoleCommandEvent $event) { - $this->siteAccess->name = $event->getInput()->getParameterOption('--siteaccess', $this->defaultSiteAccessName); - $this->siteAccess->matchingType = 'cli'; + $siteAccessName = $event->getInput()->getParameterOption('--siteaccess', $this->defaultSiteAccessName); + $siteAccess = new SiteAccess($siteAccessName, 'cli'); - if (!$this->siteAccessProvider->isDefined($this->siteAccess->name)) { + if (!$this->siteAccessProvider->isDefined($siteAccess->name)) { throw new InvalidSiteAccessException( - $this->siteAccess->name, + $siteAccess->name, $this->siteAccessProvider, - $this->siteAccess->matchingType, + $siteAccess->matchingType, $this->debug ); } - $this->eventDispatcher->dispatch(new ScopeChangeEvent($this->siteAccess), MVCEvents::CONFIG_SCOPE_CHANGE); - } - - public function setSiteAccess(?SiteAccess $siteAccess = null) - { - $this->siteAccess = $siteAccess; + $this->siteAccessService->changeSiteAccess($siteAccess); } public function setDebug($debug = false) diff --git a/src/bundle/Core/Fragment/DecoratedFragmentRenderer.php b/src/bundle/Core/Fragment/DecoratedFragmentRenderer.php index b13bc5d6c3..2f0e68d37e 100644 --- a/src/bundle/Core/Fragment/DecoratedFragmentRenderer.php +++ b/src/bundle/Core/Fragment/DecoratedFragmentRenderer.php @@ -8,32 +8,29 @@ namespace Ibexa\Bundle\Core\Fragment; use Ibexa\Core\MVC\Symfony\SiteAccess; -use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessAware; +use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Controller\ControllerReference; use Symfony\Component\HttpKernel\Fragment\FragmentRendererInterface; use Symfony\Component\HttpKernel\Fragment\RoutableFragmentRenderer; -class DecoratedFragmentRenderer implements FragmentRendererInterface, SiteAccessAware +class DecoratedFragmentRenderer implements FragmentRendererInterface { private FragmentRendererInterface $innerRenderer; - private ?SiteAccess $siteAccess = null; - private SiteAccessSerializerInterface $siteAccessSerializer; + private SiteAccessServiceInterface $siteAccessService; + public function __construct( FragmentRendererInterface $innerRenderer, - SiteAccessSerializerInterface $siteAccessSerializer + SiteAccessSerializerInterface $siteAccessSerializer, + SiteAccessServiceInterface $siteAccessService ) { $this->innerRenderer = $innerRenderer; $this->siteAccessSerializer = $siteAccessSerializer; - } - - public function setSiteAccess(?SiteAccess $siteAccess = null): void - { - $this->siteAccess = $siteAccess; + $this->siteAccessService = $siteAccessService; } public function setFragmentPath(string $path): void @@ -42,7 +39,7 @@ public function setFragmentPath(string $path): void return; } - $matcher = $this->siteAccess?->matcher; + $matcher = $this->siteAccessService->getCurrent()?->matcher; if ($matcher instanceof SiteAccess\URILexer) { $path = $matcher->analyseLink($path); } diff --git a/src/bundle/Core/Fragment/InlineFragmentRenderer.php b/src/bundle/Core/Fragment/InlineFragmentRenderer.php index cb7f348f2f..3607a53984 100644 --- a/src/bundle/Core/Fragment/InlineFragmentRenderer.php +++ b/src/bundle/Core/Fragment/InlineFragmentRenderer.php @@ -7,8 +7,6 @@ namespace Ibexa\Bundle\Core\Fragment; -use Ibexa\Core\MVC\Symfony\SiteAccess; -use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessAware; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Controller\ControllerReference; @@ -16,12 +14,10 @@ use Symfony\Component\HttpKernel\Fragment\InlineFragmentRenderer as BaseRenderer; use Symfony\Component\HttpKernel\Fragment\RoutableFragmentRenderer; -class InlineFragmentRenderer extends BaseRenderer implements SiteAccessAware, FragmentRendererInterface +class InlineFragmentRenderer extends BaseRenderer implements FragmentRendererInterface { protected FragmentRendererInterface $innerRenderer; - private ?SiteAccess $siteAccess = null; - private SiteAccessSerializerInterface $siteAccessSerializer; public function __construct( @@ -39,11 +35,6 @@ public function setFragmentPath($path): void } } - public function setSiteAccess(?SiteAccess $siteAccess = null): void - { - $this->siteAccess = $siteAccess; - } - /** * @param array $options */ diff --git a/src/bundle/Core/Imagine/Cache/AliasGeneratorDecorator.php b/src/bundle/Core/Imagine/Cache/AliasGeneratorDecorator.php index 3416d2068f..03ec2447b6 100644 --- a/src/bundle/Core/Imagine/Cache/AliasGeneratorDecorator.php +++ b/src/bundle/Core/Imagine/Cache/AliasGeneratorDecorator.php @@ -11,8 +11,7 @@ use Ibexa\Contracts\Core\Repository\Values\Content\VersionInfo; use Ibexa\Contracts\Core\Variation\Values\Variation; use Ibexa\Contracts\Core\Variation\VariationHandler; -use Ibexa\Core\MVC\Symfony\SiteAccess; -use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessAware; +use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface; use Ibexa\Core\Persistence\Cache\Identifier\CacheIdentifierGeneratorInterface; use Symfony\Component\Cache\Adapter\TagAwareAdapterInterface; use Symfony\Component\Routing\RequestContext; @@ -20,7 +19,7 @@ /** * Persistence Cache layer for AliasGenerator. */ -class AliasGeneratorDecorator implements VariationHandler, SiteAccessAware +class AliasGeneratorDecorator implements VariationHandler { private const IMAGE_VARIATION_IDENTIFIER = 'image_variation'; private const IMAGE_VARIATION_SITEACCESS_IDENTIFIER = 'image_variation_siteaccess'; @@ -36,15 +35,14 @@ class AliasGeneratorDecorator implements VariationHandler, SiteAccessAware /** @var \Symfony\Component\Cache\Adapter\TagAwareAdapterInterface */ private $cache; - /** @var \Ibexa\Core\MVC\Symfony\SiteAccess */ - private $siteAccess; - /** @var \Symfony\Component\Routing\RequestContext */ private $requestContext; /** @var \Ibexa\Core\Persistence\Cache\Identifier\CacheIdentifierGeneratorInterface */ private $cacheIdentifierGenerator; + private SiteAccessServiceInterface $siteAccessService; + /** * @param \Ibexa\Contracts\Core\Variation\VariationHandler $aliasGenerator * @param \Symfony\Component\Cache\Adapter\TagAwareAdapterInterface $cache @@ -55,12 +53,14 @@ public function __construct( VariationHandler $aliasGenerator, TagAwareAdapterInterface $cache, RequestContext $requestContext, - CacheIdentifierGeneratorInterface $cacheIdentifierGenerator + CacheIdentifierGeneratorInterface $cacheIdentifierGenerator, + SiteAccessServiceInterface $siteAccessService ) { $this->aliasGenerator = $aliasGenerator; $this->cache = $cache; $this->requestContext = $requestContext; $this->cacheIdentifierGenerator = $cacheIdentifierGenerator; + $this->siteAccessService = $siteAccessService; } /** @@ -87,14 +87,6 @@ public function getVariation(Field $field, VersionInfo $versionInfo, string $var return $image; } - /** - * @param \Ibexa\Core\MVC\Symfony\SiteAccess|null $siteAccess - */ - public function setSiteAccess(?SiteAccess $siteAccess = null) - { - $this->siteAccess = $siteAccess; - } - /** * @param \Ibexa\Contracts\Core\Repository\Values\Content\Field $field * @param \Ibexa\Contracts\Core\Repository\Values\Content\VersionInfo $versionInfo @@ -104,9 +96,11 @@ public function setSiteAccess(?SiteAccess $siteAccess = null) */ private function getCacheKey(Field $field, VersionInfo $versionInfo, $variationName): string { + $siteAccess = $this->siteAccessService->getCurrent(); + return sprintf( $this->cacheIdentifierGenerator->generateKey(self::IMAGE_VARIATION_IDENTIFIER, [], true) . '-%s-%s-%s-%d-%d-%d-%s-%s', - $this->siteAccess->name ?? 'default', + $siteAccess !== null ? $siteAccess->name : 'default', $this->requestContext->getScheme(), $this->requestContext->getHost(), $this->requestContext->getScheme() === 'https' ? $this->requestContext->getHttpsPort() : $this->requestContext->getHttpPort(), @@ -120,11 +114,12 @@ private function getCacheKey(Field $field, VersionInfo $versionInfo, $variationN private function getTagsForVariation(Field $field, VersionInfo $versionInfo, string $variationName): array { $contentId = $versionInfo->getContentInfo()->id; + $siteAccess = $this->siteAccessService->getCurrent(); return [ $this->cacheIdentifierGenerator->generateTag(self::IMAGE_VARIATION_IDENTIFIER), $this->cacheIdentifierGenerator->generateTag(self::IMAGE_VARIATION_NAME_IDENTIFIER, [$variationName]), - $this->cacheIdentifierGenerator->generateTag(self::IMAGE_VARIATION_SITEACCESS_IDENTIFIER, [$this->siteAccess->name ?? 'default']), + $this->cacheIdentifierGenerator->generateTag(self::IMAGE_VARIATION_SITEACCESS_IDENTIFIER, [$siteAccess !== null ? $siteAccess->name : 'default']), $this->cacheIdentifierGenerator->generateTag(self::IMAGE_VARIATION_CONTENT_IDENTIFIER, [$contentId]), $this->cacheIdentifierGenerator->generateTag(self::IMAGE_VARIATION_FIELD_IDENTIFIER, [$field->id]), $this->cacheIdentifierGenerator->generateTag(self::CONTENT_IDENTIFIER, [$contentId]), diff --git a/src/bundle/Core/Resources/config/commands.yml b/src/bundle/Core/Resources/config/commands.yml index f8d089acc5..87ebdf2176 100644 --- a/src/bundle/Core/Resources/config/commands.yml +++ b/src/bundle/Core/Resources/config/commands.yml @@ -12,7 +12,7 @@ services: class: Ibexa\Bundle\Core\Command\DebugConfigResolverCommand arguments: - '@ibexa.config.resolver' - - '@Ibexa\Core\MVC\Symfony\SiteAccess' + - '@Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface' tags: - { name: console.command } diff --git a/src/bundle/Core/Resources/config/helpers.yml b/src/bundle/Core/Resources/config/helpers.yml index ceb16c89cf..9e154f1e8d 100644 --- a/src/bundle/Core/Resources/config/helpers.yml +++ b/src/bundle/Core/Resources/config/helpers.yml @@ -16,9 +16,9 @@ services: arguments: ['@Ibexa\Core\Helper\TranslationHelper', '@ibexa.api.service.field_type'] Ibexa\Core\Helper\ContentPreviewHelper: - arguments: ["@event_dispatcher", '@Ibexa\Core\MVC\Symfony\SiteAccess\Router'] - calls: - - [setSiteAccess, ['@Ibexa\Core\MVC\Symfony\SiteAccess']] + arguments: + - '@Ibexa\Core\MVC\Symfony\SiteAccess\Router' + - '@Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface' Ibexa\Bundle\Core\EventListener\ConfigScopeListener: arguments: @@ -50,7 +50,7 @@ services: Ibexa\Bundle\Core\SiteAccess\Config\ComplexConfigProcessor: arguments: $configResolver: '@ibexa.config.resolver' - $siteAccessService: '@Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessService' + $siteAccessService: '@Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface' Ibexa\Contracts\Core\SiteAccess\ConfigProcessor: alias: Ibexa\Bundle\Core\SiteAccess\Config\ComplexConfigProcessor diff --git a/src/bundle/Core/Resources/config/image.yml b/src/bundle/Core/Resources/config/image.yml index d3a1f4fe91..c26d10e462 100644 --- a/src/bundle/Core/Resources/config/image.yml +++ b/src/bundle/Core/Resources/config/image.yml @@ -68,8 +68,7 @@ services: - '@ibexa.cache_pool' - '@router.request_context' - '@Ibexa\Core\Persistence\Cache\Identifier\CacheIdentifierGeneratorInterface' - calls: - - [setSiteAccess, ['@Ibexa\Core\MVC\Symfony\SiteAccess']] + - '@Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface' ibexa.image_alias.imagine.alias_generator: class: Ibexa\Bundle\Core\Imagine\AliasGenerator diff --git a/src/bundle/Core/Resources/config/routing.yml b/src/bundle/Core/Resources/config/routing.yml index 1e0fbe170d..0e04eff1ee 100644 --- a/src/bundle/Core/Resources/config/routing.yml +++ b/src/bundle/Core/Resources/config/routing.yml @@ -20,8 +20,7 @@ services: $siteAccessRouter: '@Ibexa\Core\MVC\Symfony\SiteAccess\Router' $nonSiteAccessAwareRoutes: '%ibexa.default_router.non_site_access_aware_routes%' $logger: '@?logger' - calls: - - [setSiteAccess, ['@?Ibexa\Core\MVC\Symfony\SiteAccess']] + $siteAccessService: '@Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface' tags: - { name: monolog.logger, channel: router } @@ -37,6 +36,7 @@ services: $siteAccessRouter: '@Ibexa\Core\MVC\Symfony\SiteAccess\Router' $eventDispatcher: '@event_dispatcher' $serializer: '@ibexa.core.mvc.serializer' + $siteAccessService: '@Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface' tags: - { name: kernel.event_subscriber } @@ -45,6 +45,7 @@ services: arguments: $requestStack: '@request_stack' $eventDispatcher: '@event_dispatcher' + $siteAccessService: '@Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface' tags: - { name: kernel.event_subscriber } @@ -53,7 +54,6 @@ services: abstract: true calls: - [setRequestContext, ["@router.request_context"]] - - [setSiteAccess, ['@?Ibexa\Core\MVC\Symfony\SiteAccess']] - [setSiteAccessRouter, ['@Ibexa\Core\MVC\Symfony\SiteAccess\Router']] - [setLogger, ["@?logger"]] @@ -77,6 +77,7 @@ services: - '@ibexa.api.repository' - "@router.default" - '@ibexa.config.resolver' + - '@Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface' - '%ibexa.urlalias_generator.charmap%' parent: Ibexa\Core\MVC\Symfony\Routing\Generator @@ -110,8 +111,7 @@ services: arguments: - '@ibexa.siteaccess.provider' - '@ibexa.config.resolver' - calls: - - [setSiteAccess, ['@Ibexa\Core\MVC\Symfony\SiteAccess']] + - '@event_dispatcher' tags: - { name: kernel.event_subscriber } diff --git a/src/bundle/Core/Resources/config/services.yml b/src/bundle/Core/Resources/config/services.yml index 8501e54cbc..59ecbcc452 100644 --- a/src/bundle/Core/Resources/config/services.yml +++ b/src/bundle/Core/Resources/config/services.yml @@ -74,10 +74,8 @@ services: arguments: - '%ibexa.site_access.default%' - '@ibexa.siteaccess.provider' - - "@event_dispatcher" + - '@Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface' - "%kernel.debug%" - calls: - - [setSiteAccess, ['@Ibexa\Core\MVC\Symfony\SiteAccess']] tags: - { name: kernel.event_subscriber } @@ -169,7 +167,6 @@ services: arguments: [] calls: - [setFragmentPath, ["%fragment.path%"]] - - [setSiteAccess, ['@Ibexa\Core\MVC\Symfony\SiteAccess']] abstract: true Ibexa\Bundle\Core\Fragment\DirectFragmentRenderer: diff --git a/src/bundle/Core/Routing/DefaultRouter.php b/src/bundle/Core/Routing/DefaultRouter.php index da3000e2e3..e99fd8055f 100644 --- a/src/bundle/Core/Routing/DefaultRouter.php +++ b/src/bundle/Core/Routing/DefaultRouter.php @@ -11,8 +11,8 @@ use Ibexa\Core\MVC\Symfony\Routing\RequestContextFactory; use Ibexa\Core\MVC\Symfony\Routing\SimplifiedRequest; use Ibexa\Core\MVC\Symfony\SiteAccess; -use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessAware; use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessRouterInterface; +use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface; use Ibexa\Core\MVC\Symfony\SiteAccess\URILexer; use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\Request; @@ -28,10 +28,8 @@ * Matching honours the `semanticPathinfo` request attribute set by the SiteAccess matcher, and link generation * prepends the SiteAccess URI part (for URI-based SiteAccess matchers) and supports the `siteaccess` route parameter. */ -final class DefaultRouter implements RouterInterface, RequestMatcherInterface, WarmableInterface, SiteAccessAware +final class DefaultRouter implements RouterInterface, RequestMatcherInterface, WarmableInterface { - private ?SiteAccess $siteAccess = null; - /** * @param string[] $nonSiteAccessAwareRoutes route name prefixes that are not supposed to be SiteAccess aware, * i.e. routes pointing to asset generation @@ -39,16 +37,12 @@ final class DefaultRouter implements RouterInterface, RequestMatcherInterface, W public function __construct( private readonly RouterInterface&RequestMatcherInterface $innerRouter, private readonly SiteAccessRouterInterface $siteAccessRouter, + private readonly SiteAccessServiceInterface $siteAccessService, private readonly array $nonSiteAccessAwareRoutes = [], private readonly ?LoggerInterface $logger = null ) { } - public function setSiteAccess(?SiteAccess $siteAccess = null): void - { - $this->siteAccess = $siteAccess; - } - public function setContext(RequestContext $context): void { $this->innerRouter->setContext($context); @@ -93,7 +87,7 @@ public function matchRequest(Request $request): array */ public function generate(string $name, array $parameters = [], int $referenceType = self::ABSOLUTE_PATH): string { - $siteAccess = $this->siteAccess; + $siteAccess = $this->siteAccessService->getCurrent(); $originalContext = $context = $this->getContext(); $isSiteAccessAware = $this->isSiteAccessAwareRoute($name); @@ -105,7 +99,7 @@ public function generate(string $name, array $parameters = [], int $referenceTyp $context = $this->getContextBySimplifiedRequest($siteAccess->matcher->getRequest()); $this->setContext($context); } else { - $siteAccess = $this->siteAccess; + $siteAccess = $this->siteAccessService->getCurrent(); $this->logger?->notice("Could not generate a link using provided 'siteaccess' parameter: {$parameters['siteaccess']}. Generating using current context."); } diff --git a/src/bundle/Core/SiteAccess/Config/ComplexConfigProcessor.php b/src/bundle/Core/SiteAccess/Config/ComplexConfigProcessor.php index 5329e4dffc..8ea2c66d60 100644 --- a/src/bundle/Core/SiteAccess/Config/ComplexConfigProcessor.php +++ b/src/bundle/Core/SiteAccess/Config/ComplexConfigProcessor.php @@ -11,8 +11,9 @@ use Ibexa\Bundle\Core\DependencyInjection\Configuration\ComplexSettings\ComplexSettingParser; use Ibexa\Contracts\Core\SiteAccess\ConfigProcessor; use Ibexa\Contracts\Core\SiteAccess\ConfigResolverInterface; +use Ibexa\Core\Base\Exceptions\InvalidArgumentException; use Ibexa\Core\MVC\Exception\ParameterNotFoundException; -use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessService; +use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface; use function str_replace; final class ComplexConfigProcessor implements ConfigProcessor @@ -22,7 +23,7 @@ final class ComplexConfigProcessor implements ConfigProcessor /** @var \Ibexa\Contracts\Core\SiteAccess\ConfigResolverInterface */ private $configResolver; - /** @var \Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessService */ + /** @var \Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface */ private $siteAccessService; /** @var \Ibexa\Bundle\Core\DependencyInjection\Configuration\ComplexSettings\ComplexSettingParserInterface */ @@ -30,7 +31,7 @@ final class ComplexConfigProcessor implements ConfigProcessor public function __construct( ConfigResolverInterface $configResolver, - SiteAccessService $siteAccessService + SiteAccessServiceInterface $siteAccessService ) { $this->configResolver = $configResolver; $this->siteAccessService = $siteAccessService; @@ -41,7 +42,7 @@ public function __construct( public function processComplexSetting(string $setting): string { - $siteAccessName = $this->siteAccessService->getCurrent()->name; + $siteAccessName = $this->getCurrentSiteAccessName(); if (!$this->configResolver->hasParameter($setting, null, $siteAccessName)) { throw new ParameterNotFoundException($setting, null, [$siteAccessName]); @@ -74,7 +75,7 @@ public function processSettingValue(string $value): string $dynamicSettingValue = $this->configResolver->getParameter( $parts['param'], $parts['namespace'], - $parts['scope'] ?? $this->siteAccessService->getCurrent()->name + $parts['scope'] ?? $this->getCurrentSiteAccessName() ); $value = str_replace($dynamicSetting, $dynamicSettingValue, $value); @@ -82,4 +83,14 @@ public function processSettingValue(string $value): string return $value; } + + private function getCurrentSiteAccessName(): string + { + $siteAccess = $this->siteAccessService->getCurrent(); + if ($siteAccess === null) { + throw new InvalidArgumentException('siteAccess', 'no SiteAccess given and none currently set'); + } + + return $siteAccess->name; + } } diff --git a/src/lib/Helper/ContentPreviewHelper.php b/src/lib/Helper/ContentPreviewHelper.php index ed375dd2a7..d57f458ccc 100644 --- a/src/lib/Helper/ContentPreviewHelper.php +++ b/src/lib/Helper/ContentPreviewHelper.php @@ -9,23 +9,16 @@ use Ibexa\Contracts\Core\Repository\Values\Content\Content; use Ibexa\Contracts\Core\Repository\Values\Content\Location; -use Ibexa\Core\MVC\Symfony\Event\ScopeChangeEvent; -use Ibexa\Core\MVC\Symfony\MVCEvents; use Ibexa\Core\MVC\Symfony\SiteAccess; -use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessAware; use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessRouterInterface; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface; -class ContentPreviewHelper implements SiteAccessAware +class ContentPreviewHelper { - /** @var \Symfony\Component\EventDispatcher\EventDispatcherInterface */ - protected $eventDispatcher; - /** @var \Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessRouterInterface */ protected $siteAccessRouter; - /** @var \Ibexa\Core\MVC\Symfony\SiteAccess */ - protected $originalSiteAccess; + private SiteAccessServiceInterface $siteAccessService; /** @var bool */ private $previewActive = false; @@ -36,25 +29,18 @@ class ContentPreviewHelper implements SiteAccessAware /** @var \Ibexa\Contracts\Core\Repository\Values\Content\Location */ private $previewedLocation; - public function __construct(EventDispatcherInterface $eventDispatcher, SiteAccessRouterInterface $siteAccessRouter) + public function __construct(SiteAccessRouterInterface $siteAccessRouter, SiteAccessServiceInterface $siteAccessService) { - $this->eventDispatcher = $eventDispatcher; $this->siteAccessRouter = $siteAccessRouter; - } - - public function setSiteAccess(?SiteAccess $siteAccess = null) - { - $this->originalSiteAccess = $siteAccess; + $this->siteAccessService = $siteAccessService; } /** * Return original SiteAccess. - * - * @return \Ibexa\Core\MVC\Symfony\SiteAccess */ - public function getOriginalSiteAccess() + public function getOriginalSiteAccess(): ?SiteAccess { - return $this->originalSiteAccess; + return $this->siteAccessService->getCurrent(); } /** @@ -66,23 +52,15 @@ public function getOriginalSiteAccess() */ public function changeConfigScope($siteAccessName) { - $event = new ScopeChangeEvent($this->siteAccessRouter->matchByName($siteAccessName)); - $this->eventDispatcher->dispatch($event, MVCEvents::CONFIG_SCOPE_CHANGE); - - return $event->getSiteAccess(); + return $this->siteAccessService->changeSiteAccess($this->siteAccessRouter->matchByName($siteAccessName)); } /** * Restores original config scope. - * - * @return \Ibexa\Core\MVC\Symfony\SiteAccess */ - public function restoreConfigScope() + public function restoreConfigScope(): ?SiteAccess { - $event = new ScopeChangeEvent($this->originalSiteAccess); - $this->eventDispatcher->dispatch($event, MVCEvents::CONFIG_SCOPE_RESTORE); - - return $event->getSiteAccess(); + return $this->siteAccessService->restoreSiteAccess(); } /** @@ -99,7 +77,6 @@ public function isPreviewActive() public function setPreviewActive($previewActive) { $this->previewActive = (bool)$previewActive; - $this->originalSiteAccess = clone $this->originalSiteAccess; } /** diff --git a/src/lib/MVC/Symfony/Controller/Content/PreviewController.php b/src/lib/MVC/Symfony/Controller/Content/PreviewController.php index 5ce1da3e2a..98e8009073 100644 --- a/src/lib/MVC/Symfony/Controller/Content/PreviewController.php +++ b/src/lib/MVC/Symfony/Controller/Content/PreviewController.php @@ -16,6 +16,7 @@ use Ibexa\Contracts\Core\Repository\Values\Content\Content; use Ibexa\Contracts\Core\Repository\Values\Content\Location; use Ibexa\Core\Base\Exceptions\BadStateException; +use Ibexa\Core\Base\Exceptions\InvalidArgumentException; use Ibexa\Core\Helper\ContentPreviewHelper; use Ibexa\Core\Helper\PreviewLocationProvider; use Ibexa\Core\MVC\Symfony\Routing\Generator\UrlAliasGenerator; @@ -120,40 +121,46 @@ public function previewContentAction( throw new AccessDeniedException(); } - $siteAccess = $this->previewHelper->getOriginalSiteAccess(); - // Only switch if $siteAccessName is set and different from original - if ($siteAccessName !== null && $siteAccessName !== $siteAccess->name) { - $siteAccess = $this->previewHelper->changeConfigScope($siteAccessName); - } - try { - $viewType = $request->query->get('viewType', ViewManagerInterface::VIEW_TYPE_FULL); - $response = $this->kernel->handle( - $this->getForwardRequest($location, $content, $siteAccess, $request, $language, $viewType), - HttpKernelInterface::SUB_REQUEST, - false - ); - } catch (APINotFoundException $e) { - $message = sprintf('Location (%s) not found or not available in requested language (%s)', $location->id, $language); - $this->logger->warning( - sprintf('%s %s', $message, 'when loading the preview page'), - ['exception' => $e] - ); - if ($this->debugMode) { - throw new BadStateException('Preview page', $message, $e); + $siteAccess = $this->previewHelper->getOriginalSiteAccess(); + if ($siteAccess === null) { + throw new InvalidArgumentException('siteAccess', 'no SiteAccess given and none currently set'); } - return new Response($message); - } catch (Exception $e) { - return $this->buildResponseForGenericPreviewError($location, $content, $e); - } - $response->headers->addCacheControlDirective('no-cache', true); - $response->setPrivate(); + // Only switch if $siteAccessName is set and different from original + if ($siteAccessName !== null && $siteAccessName !== $siteAccess->name) { + $siteAccess = $this->previewHelper->changeConfigScope($siteAccessName); + } - $this->previewHelper->restoreConfigScope(); - $this->previewHelper->setPreviewActive(false); + try { + $viewType = $request->query->get('viewType', ViewManagerInterface::VIEW_TYPE_FULL); + $response = $this->kernel->handle( + $this->getForwardRequest($location, $content, $siteAccess, $request, $language, $viewType), + HttpKernelInterface::SUB_REQUEST, + false + ); + } catch (APINotFoundException $e) { + $message = sprintf('Location (%s) not found or not available in requested language (%s)', $location->id, $language); + $this->logger->warning( + sprintf('%s %s', $message, 'when loading the preview page'), + ['exception' => $e] + ); + if ($this->debugMode) { + throw new BadStateException('Preview page', $message, $e); + } + + return new Response($message); + } catch (Exception $e) { + return $this->buildResponseForGenericPreviewError($location, $content, $e); + } + $response->headers->addCacheControlDirective('no-cache', true); + $response->setPrivate(); - return $response; + return $response; + } finally { + $this->previewHelper->restoreConfigScope(); + $this->previewHelper->setPreviewActive(false); + } } /** diff --git a/src/lib/MVC/Symfony/EventListener/SiteAccessMatchListener.php b/src/lib/MVC/Symfony/EventListener/SiteAccessMatchListener.php index c172971cfe..2f7dacd594 100644 --- a/src/lib/MVC/Symfony/EventListener/SiteAccessMatchListener.php +++ b/src/lib/MVC/Symfony/EventListener/SiteAccessMatchListener.php @@ -12,10 +12,12 @@ 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\SiteAccess\SiteAccessServiceInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\RequestEvent; +use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\Serializer\SerializerInterface; @@ -31,14 +33,18 @@ class SiteAccessMatchListener implements EventSubscriberInterface private SerializerInterface $serializer; + private SiteAccessServiceInterface $siteAccessService; + public function __construct( SiteAccessRouter $siteAccessRouter, EventDispatcherInterface $eventDispatcher, - SerializerInterface $serializer + SerializerInterface $serializer, + SiteAccessServiceInterface $siteAccessService ) { $this->siteAccessRouter = $siteAccessRouter; $this->eventDispatcher = $eventDispatcher; $this->serializer = $serializer; + $this->siteAccessService = $siteAccessService; } public static function getSubscribedEvents(): array @@ -89,6 +95,15 @@ public function onKernelRequest(RequestEvent $event): void if ($siteaccess instanceof SiteAccess) { $siteAccessEvent = new PostSiteAccessMatchEvent($siteaccess, $request, $event->getRequestType()); $this->eventDispatcher->dispatch($siteAccessEvent, MVCEvents::SITEACCESS); + + // SiteAccessService establishes the base of its stack by reacting to this same event + // for MAIN_REQUEST (see SiteAccessService::onSiteAccessMatch()); a sub-request's + // SiteAccess is a nested change on top of that, so push it the same way any other + // caller would, via changeSiteAccess(). SiteAccessRestoreListener pops it back off via + // restoreSiteAccess() once this sub-request finishes. + if ($event->getRequestType() === HttpKernelInterface::SUB_REQUEST) { + $this->siteAccessService->changeSiteAccess($siteaccess); + } } } diff --git a/src/lib/MVC/Symfony/EventListener/SiteAccessRestoreListener.php b/src/lib/MVC/Symfony/EventListener/SiteAccessRestoreListener.php index 7f21fad7d0..c9ea80ee44 100644 --- a/src/lib/MVC/Symfony/EventListener/SiteAccessRestoreListener.php +++ b/src/lib/MVC/Symfony/EventListener/SiteAccessRestoreListener.php @@ -11,6 +11,7 @@ use Ibexa\Core\MVC\Symfony\Event\PostSiteAccessMatchEvent; use Ibexa\Core\MVC\Symfony\MVCEvents; use Ibexa\Core\MVC\Symfony\SiteAccess; +use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\RequestStack; @@ -19,9 +20,12 @@ use Symfony\Component\HttpKernel\KernelEvents; /** - * Restores the parent request's SiteAccess when a sub-request finishes, by re-dispatching - * MVCEvents::SITEACCESS — its listeners mutate shared state for every sub-request. Mirrors - * {@see \Symfony\Component\HttpKernel\EventListener\RouterListener::onKernelFinishRequest()}. + * Restores the parent request's SiteAccess when a sub-request finishes: + * - for SiteAccessService, by calling restoreSiteAccess(), popping the entry + * SiteAccessMatchListener pushed via changeSiteAccess() when this sub-request started. + * - for not-yet-migrated listeners still reacting to MVCEvents::SITEACCESS directly (mutating + * shared state for every sub-request), by re-dispatching that event. Mirrors + * {@see \Symfony\Component\HttpKernel\EventListener\RouterListener::onKernelFinishRequest()}. */ final class SiteAccessRestoreListener implements EventSubscriberInterface { @@ -29,12 +33,16 @@ final class SiteAccessRestoreListener implements EventSubscriberInterface private EventDispatcherInterface $eventDispatcher; + private SiteAccessServiceInterface $siteAccessService; + public function __construct( RequestStack $requestStack, - EventDispatcherInterface $eventDispatcher + EventDispatcherInterface $eventDispatcher, + SiteAccessServiceInterface $siteAccessService ) { $this->requestStack = $requestStack; $this->eventDispatcher = $eventDispatcher; + $this->siteAccessService = $siteAccessService; } public static function getSubscribedEvents(): array @@ -50,6 +58,12 @@ public function onKernelFinishRequest(FinishRequestEvent $event): void return; } + // Mirrors the condition SiteAccessMatchListener used to decide whether to push, so the + // pop here only ever undoes what this exact sub-request pushed. + if ($event->getRequest()->attributes->get('siteaccess') instanceof SiteAccess) { + $this->siteAccessService->restoreSiteAccess(); + } + // The finishing request is still on the stack, so this is the request control returns to $parentRequest = $this->requestStack->getParentRequest(); if ($parentRequest === null) { diff --git a/src/lib/MVC/Symfony/Routing/Generator.php b/src/lib/MVC/Symfony/Routing/Generator.php index 9729d0b90a..16d8982e8a 100644 --- a/src/lib/MVC/Symfony/Routing/Generator.php +++ b/src/lib/MVC/Symfony/Routing/Generator.php @@ -8,8 +8,8 @@ namespace Ibexa\Core\MVC\Symfony\Routing; use Ibexa\Core\MVC\Symfony\SiteAccess; -use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessAware; use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessRouterInterface; +use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\RequestContext; @@ -17,16 +17,18 @@ /** * Base class for Ibexa Url generation. */ -abstract class Generator implements SiteAccessAware +abstract class Generator { protected RequestContext $requestContext; protected SiteAccessRouterInterface $siteAccessRouter; - protected ?SiteAccess $siteAccess; - protected ?LoggerInterface $logger; + public function __construct(private readonly SiteAccessServiceInterface $siteAccessService) + { + } + public function setRequestContext(RequestContext $requestContext): void { $this->requestContext = $requestContext; @@ -37,11 +39,6 @@ public function setSiteAccessRouter(SiteAccessRouterInterface $siteAccessRouter) $this->siteAccessRouter = $siteAccessRouter; } - public function setSiteAccess(?SiteAccess $siteAccess = null): void - { - $this->siteAccess = $siteAccess; - } - public function setLogger(?LoggerInterface $logger = null): void { $this->logger = $logger; @@ -57,7 +54,7 @@ public function setLogger(?LoggerInterface $logger = null): void */ public function generate(mixed $urlResource, array $parameters, int $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH): string { - $siteAccess = $this->siteAccess; + $siteAccess = $this->siteAccessService->getCurrent(); $requestContext = $this->requestContext; // Retrieving the appropriate SiteAccess to generate the link for. @@ -69,7 +66,7 @@ public function generate(mixed $urlResource, array $parameters, int $referenceTy $siteAccess->matcher->getRequest() ); } elseif (isset($this->logger)) { - $siteAccess = $this->siteAccess; + $siteAccess = $this->siteAccessService->getCurrent(); $this->logger->notice("Could not generate a link using provided 'siteaccess' parameter: {$parameters['siteaccess']}. Generating using current context."); unset($parameters['siteaccess']); } diff --git a/src/lib/MVC/Symfony/Routing/Generator/UrlAliasGenerator.php b/src/lib/MVC/Symfony/Routing/Generator/UrlAliasGenerator.php index de2c8b66f2..ae10adaa2d 100644 --- a/src/lib/MVC/Symfony/Routing/Generator/UrlAliasGenerator.php +++ b/src/lib/MVC/Symfony/Routing/Generator/UrlAliasGenerator.php @@ -11,6 +11,7 @@ use Ibexa\Contracts\Core\Repository\Values\Content\Location; use Ibexa\Contracts\Core\SiteAccess\ConfigResolverInterface; use Ibexa\Core\MVC\Symfony\Routing\Generator; +use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface; use Symfony\Component\Routing\RouterInterface; /** @@ -47,8 +48,15 @@ class UrlAliasGenerator extends Generator /** * @param array $unsafeCharMap */ - public function __construct(Repository $repository, RouterInterface $defaultRouter, ConfigResolverInterface $configResolver, array $unsafeCharMap = []) - { + public function __construct( + Repository $repository, + RouterInterface $defaultRouter, + ConfigResolverInterface $configResolver, + SiteAccessServiceInterface $siteAccessService, + array $unsafeCharMap = [] + ) { + parent::__construct($siteAccessService); + $this->repository = $repository; $this->defaultRouter = $defaultRouter; $this->configResolver = $configResolver; diff --git a/src/lib/MVC/Symfony/Security/HttpUtils.php b/src/lib/MVC/Symfony/Security/HttpUtils.php index 4e1e8c725b..9000c9674e 100644 --- a/src/lib/MVC/Symfony/Security/HttpUtils.php +++ b/src/lib/MVC/Symfony/Security/HttpUtils.php @@ -8,22 +8,28 @@ namespace Ibexa\Core\MVC\Symfony\Security; use Ibexa\Core\MVC\Symfony\SiteAccess; -use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessAware; +use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Component\Routing\Matcher\RequestMatcherInterface; +use Symfony\Component\Routing\Matcher\UrlMatcherInterface; use Symfony\Component\Security\Http\HttpUtils as BaseHttpUtils; -class HttpUtils extends BaseHttpUtils implements SiteAccessAware +class HttpUtils extends BaseHttpUtils { - private ?SiteAccess $siteAccess; - - public function setSiteAccess(?SiteAccess $siteAccess = null): void - { - $this->siteAccess = $siteAccess; + public function __construct( + ?UrlGeneratorInterface $urlGenerator = null, + UrlMatcherInterface|RequestMatcherInterface|null $urlMatcher = null, + ?string $domainRegexp = null, + ?string $secureDomainRegexp = null, + private ?SiteAccessServiceInterface $siteAccessService = null, + ) { + parent::__construct($urlGenerator, $urlMatcher, $domainRegexp, $secureDomainRegexp); } private function analyzeLink(string $path): string { - $matcher = $this->siteAccess?->matcher; + $matcher = $this->siteAccessService?->getCurrent()?->matcher; if ($path[0] === '/' && $matcher instanceof SiteAccess\URILexer) { $path = $matcher->analyseLink($path); } diff --git a/src/lib/MVC/Symfony/SiteAccess/SiteAccessService.php b/src/lib/MVC/Symfony/SiteAccess/SiteAccessService.php index 41eebdd75b..e25d2ccd7d 100644 --- a/src/lib/MVC/Symfony/SiteAccess/SiteAccessService.php +++ b/src/lib/MVC/Symfony/SiteAccess/SiteAccessService.php @@ -11,57 +11,101 @@ use Ibexa\Contracts\Core\SiteAccess\ConfigResolverInterface; use Ibexa\Core\Base\Exceptions\InvalidArgumentException; use Ibexa\Core\Base\Exceptions\NotFoundException; +use Ibexa\Core\MVC\Symfony\Event\PostSiteAccessMatchEvent; use Ibexa\Core\MVC\Symfony\Event\ScopeChangeEvent; use Ibexa\Core\MVC\Symfony\MVCEvents; use Ibexa\Core\MVC\Symfony\SiteAccess; use function iterator_to_array; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpKernel\HttpKernelInterface; -class SiteAccessService implements SiteAccessServiceInterface, SiteAccessAware, EventSubscriberInterface +/** + * changeSiteAccess()/restoreSiteAccess() are the single, supported way to swap the current + * SiteAccess (content preview, CLI --siteaccess, sub-requests). Manually dispatching + * ScopeChangeEvent under MVCEvents::CONFIG_SCOPE_CHANGE/CONFIG_SCOPE_RESTORE is deprecated and + * only kept as a fallback for not-yet-migrated listeners — see onConfigScopeChange(). + */ +class SiteAccessService implements SiteAccessServiceInterface, EventSubscriberInterface { /** @var list<\Ibexa\Core\MVC\Symfony\SiteAccess> */ private array $siteAccessStack = []; + private bool $isDispatchingInternally = false; + public function __construct( private readonly SiteAccessProviderInterface $provider, - private readonly ConfigResolverInterface $configResolver + private readonly ConfigResolverInterface $configResolver, + private readonly EventDispatcherInterface $eventDispatcher ) { } public static function getSubscribedEvents(): array { return [ + MVCEvents::SITEACCESS => 'onSiteAccessMatch', MVCEvents::CONFIG_SCOPE_CHANGE => 'onConfigScopeChange', MVCEvents::CONFIG_SCOPE_RESTORE => 'onConfigScopeRestore', ]; } - public function setSiteAccess(?SiteAccess $siteAccess = null): void + /** + * Establishes the base of the SiteAccess stack for a new top-level request. Sub-requests are + * NOT handled here: the listener matching their SiteAccess calls changeSiteAccess() directly, + * same as any other caller. + */ + public function onSiteAccessMatch(PostSiteAccessMatchEvent $event): void { - $this->siteAccessStack = $siteAccess !== null ? [$siteAccess] : []; + if ($event->getRequestType() !== HttpKernelInterface::MAIN_REQUEST) { + return; + } + + $this->siteAccessStack = [$event->getSiteAccess()]; } /** - * Pushes the new SiteAccess onto the stack, so that getCurrent() reflects it until the - * matching restore happens. + * @deprecated 6.0, to be removed in 7.0. Dispatching ScopeChangeEvent manually under + * MVCEvents::CONFIG_SCOPE_CHANGE is deprecated; call changeSiteAccess() instead, + * which dispatches this event for you. Kept so not-yet-migrated listeners keep + * working, but no longer the supported way to change the current SiteAccess. */ public function onConfigScopeChange(ScopeChangeEvent $event): void { + if ($this->isDispatchingInternally) { + return; + } + + trigger_deprecation( + 'ibexa/core', + '6.0', + 'Dispatching %s manually under MVCEvents::CONFIG_SCOPE_CHANGE is deprecated and will no longer be reflected by %s::getCurrent() in 7.0. Call %s::changeSiteAccess() instead.', + ScopeChangeEvent::class, + self::class, + SiteAccessServiceInterface::class + ); + $this->siteAccessStack[] = $event->getSiteAccess(); } /** - * Pops the SiteAccess pushed by the matching onConfigScopeChange(), but never below one - * remaining entry: the bottom-most entry (the current request's SiteAccess) must survive an - * unbalanced restore. + * @deprecated 6.0, to be removed in 7.0. See onConfigScopeChange(). */ public function onConfigScopeRestore(ScopeChangeEvent $event): void { - if (count($this->siteAccessStack) <= 1) { + if ($this->isDispatchingInternally) { return; } - array_pop($this->siteAccessStack); + trigger_deprecation( + 'ibexa/core', + '6.0', + 'Dispatching %s manually under MVCEvents::CONFIG_SCOPE_RESTORE is deprecated and will no longer be reflected by %s::getCurrent() in 7.0. Call %s::restoreSiteAccess() instead.', + ScopeChangeEvent::class, + self::class, + SiteAccessServiceInterface::class + ); + + $this->popSiteAccessStack(); } public function exists(string $name): bool @@ -85,12 +129,40 @@ public function getAll(): iterable public function getCurrent(): ?SiteAccess { - return $this->siteAccessStack !== [] ? end($this->siteAccessStack) : null; + if ($this->siteAccessStack === []) { + return null; + } + + // Defensive: a SiteAccess with an "uninitialized" matcher (see + // SiteAccess::MATCHING_TYPE_UNINITIALIZED) is not a real current SiteAccess even if it + // somehow ended up on the stack — surface it as null rather than a misleading value. + $current = end($this->siteAccessStack); + + return $current->matchingType !== SiteAccess::MATCHING_TYPE_UNINITIALIZED ? $current : null; + } + + public function changeSiteAccess(SiteAccess $siteAccess): SiteAccess + { + $this->siteAccessStack[] = $siteAccess; + $this->dispatchScopeChangeEvent($siteAccess, MVCEvents::CONFIG_SCOPE_CHANGE); + + return $siteAccess; + } + + public function restoreSiteAccess(): ?SiteAccess + { + $this->popSiteAccessStack(); + $siteAccess = $this->getCurrent(); + if ($siteAccess !== null) { + $this->dispatchScopeChangeEvent($siteAccess, MVCEvents::CONFIG_SCOPE_RESTORE); + } + + return $siteAccess; } public function getSiteAccessesRelation(?SiteAccess $siteAccess = null): array { - $siteAccess = $siteAccess ?? $this->getCurrent(); + $siteAccess ??= $this->getCurrent(); if ($siteAccess === null) { throw new InvalidArgumentException('siteAccess', 'no SiteAccess given and none currently set'); } @@ -122,4 +194,30 @@ public function getSiteAccessesRelation(?SiteAccess $siteAccess = null): array return $saRelationMap[$repository][$rootLocationId]; } + + /** + * Pops the SiteAccess stack, but never below one remaining entry: the bottom-most entry + * (the current top-level request's SiteAccess) must survive an unbalanced restore. + */ + private function popSiteAccessStack(): void + { + if (count($this->siteAccessStack) > 1) { + array_pop($this->siteAccessStack); + } + } + + /** + * Dispatches $eventName with isDispatchingInternally set, so onConfigScopeChange()/ + * onConfigScopeRestore() can tell this dispatch apart from a manually-constructed one and + * skip reacting to it (the stack mutation already happened above). + */ + private function dispatchScopeChangeEvent(SiteAccess $siteAccess, string $eventName): void + { + $this->isDispatchingInternally = true; + try { + $this->eventDispatcher->dispatch(new ScopeChangeEvent($siteAccess), $eventName); + } finally { + $this->isDispatchingInternally = false; + } + } } diff --git a/src/lib/MVC/Symfony/SiteAccess/SiteAccessServiceInterface.php b/src/lib/MVC/Symfony/SiteAccess/SiteAccessServiceInterface.php index b95cfda289..61c43278e4 100644 --- a/src/lib/MVC/Symfony/SiteAccess/SiteAccessServiceInterface.php +++ b/src/lib/MVC/Symfony/SiteAccess/SiteAccessServiceInterface.php @@ -29,6 +29,20 @@ public function getAll(): iterable; public function getCurrent(): ?SiteAccess; + /** + * Switches the current SiteAccess to $siteAccess, remembering the previous one so it can be + * restored later via restoreSiteAccess(). Dispatches a ScopeChangeEvent under + * MVCEvents::CONFIG_SCOPE_CHANGE. This is the supported way to change the current SiteAccess — + * do not dispatch that event manually. + */ + public function changeSiteAccess(SiteAccess $siteAccess): SiteAccess; + + /** + * Restores the SiteAccess that was current before the last changeSiteAccess() call. + * Dispatches a ScopeChangeEvent under MVCEvents::CONFIG_SCOPE_RESTORE. + */ + public function restoreSiteAccess(): ?SiteAccess; + /** * Handles relation between SiteAccesses. Related SiteAccesses share the same repository and root location id. * diff --git a/tests/bundle/Core/DependencyInjection/Compiler/FragmentPassTest.php b/tests/bundle/Core/DependencyInjection/Compiler/FragmentPassTest.php index 72c78331d0..6f1b82ef2d 100644 --- a/tests/bundle/Core/DependencyInjection/Compiler/FragmentPassTest.php +++ b/tests/bundle/Core/DependencyInjection/Compiler/FragmentPassTest.php @@ -12,6 +12,7 @@ use Ibexa\Bundle\Core\Fragment\FragmentListenerFactory; use Ibexa\Bundle\Core\Fragment\InlineFragmentRenderer; use Ibexa\Bundle\Core\Fragment\SiteAccessSerializerInterface; +use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface; use Matthias\SymfonyDependencyInjectionTest\PhpUnit\AbstractCompilerPassTestCase; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; @@ -67,7 +68,7 @@ public function testProcess(): void $decoratedInlineDef = $this->container->getDefinition('fragment.renderer.inline'); self::assertSame(['kernel.fragment_renderer' => [[]]], $decoratedInlineDef->getTags()); self::assertEquals( - [new Reference('fragment.renderer.inline.inner'), new Reference(SiteAccessSerializerInterface::class)], + [new Reference('fragment.renderer.inline.inner'), new Reference(SiteAccessSerializerInterface::class), new Reference(SiteAccessServiceInterface::class)], $decoratedInlineDef->getArguments() ); self::assertSame(InlineFragmentRenderer::class, $decoratedInlineDef->getClass()); @@ -76,7 +77,7 @@ public function testProcess(): void $decoratedEsiDef = $this->container->getDefinition('fragment.renderer.esi'); self::assertSame(['kernel.fragment_renderer' => [[]]], $decoratedEsiDef->getTags()); self::assertEquals( - [new Reference('fragment.renderer.esi.inner'), new Reference(SiteAccessSerializerInterface::class)], + [new Reference('fragment.renderer.esi.inner'), new Reference(SiteAccessSerializerInterface::class), new Reference(SiteAccessServiceInterface::class)], $decoratedEsiDef->getArguments() ); @@ -84,7 +85,7 @@ public function testProcess(): void $decoratedHincludeDef = $this->container->getDefinition('fragment.renderer.hinclude'); self::assertSame(['kernel.fragment_renderer' => [[]]], $decoratedHincludeDef->getTags()); self::assertEquals( - [new Reference('fragment.renderer.hinclude.inner'), new Reference(SiteAccessSerializerInterface::class)], + [new Reference('fragment.renderer.hinclude.inner'), new Reference(SiteAccessSerializerInterface::class), new Reference(SiteAccessServiceInterface::class)], $decoratedHincludeDef->getArguments() ); } diff --git a/tests/bundle/Core/DependencyInjection/Compiler/SecurityPassTest.php b/tests/bundle/Core/DependencyInjection/Compiler/SecurityPassTest.php index ea08ce8da5..c4b1a87eef 100644 --- a/tests/bundle/Core/DependencyInjection/Compiler/SecurityPassTest.php +++ b/tests/bundle/Core/DependencyInjection/Compiler/SecurityPassTest.php @@ -13,7 +13,7 @@ use Ibexa\Contracts\Core\Repository\UserService; use Ibexa\Contracts\Core\SiteAccess\ConfigResolverInterface; use Ibexa\Core\MVC\Symfony\Security\Authentication\AnonymousUserAccessListener; -use Ibexa\Core\MVC\Symfony\SiteAccess; +use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface; use Matthias\SymfonyDependencyInjectionTest\PhpUnit\AbstractCompilerPassTestCase; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; @@ -29,7 +29,7 @@ protected function setUp(): void $this->setDefinition('security.http_utils', new Definition()); $this->setDefinition('security.authentication.success_handler', new Definition()); $this->setDefinition(ConfigResolverInterface::class, new Definition()); - $this->setDefinition(SiteAccess::class, new Definition()); + $this->setDefinition(SiteAccessServiceInterface::class, new Definition()); $this->setDefinition(PermissionResolver::class, new Definition()); $this->setDefinition(UserService::class, new Definition()); } @@ -43,10 +43,10 @@ public function testAlteredHttpUtils(): void { $this->compile(); - $this->assertContainerBuilderHasServiceDefinitionWithMethodCall( + $this->assertContainerBuilderHasServiceDefinitionWithArgument( 'security.http_utils', - 'setSiteAccess', - [new Reference(SiteAccess::class)] + '$siteAccessService', + new Reference(SiteAccessServiceInterface::class) ); } diff --git a/tests/bundle/Core/EventListener/ConsoleCommandListenerTest.php b/tests/bundle/Core/EventListener/ConsoleCommandListenerTest.php index 0bd8a53484..7d1e2cbdef 100644 --- a/tests/bundle/Core/EventListener/ConsoleCommandListenerTest.php +++ b/tests/bundle/Core/EventListener/ConsoleCommandListenerTest.php @@ -10,6 +10,7 @@ use Ibexa\Bundle\Core\EventListener\ConsoleCommandListener; use Ibexa\Core\MVC\Exception\InvalidSiteAccessException; use Ibexa\Core\MVC\Symfony\SiteAccess; +use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface; use Ibexa\Tests\Bundle\Core\EventListener\Stubs\TestOutput; use PHPUnit\Framework\MockObject\Stub; use PHPUnit\Framework\TestCase; @@ -20,17 +21,13 @@ use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\Output; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; class ConsoleCommandListenerTest extends TestCase { private const INVALID_SA_NAME = 'foo'; - /** @var \Ibexa\Core\MVC\Symfony\SiteAccess */ - private $siteAccess; - - /** @var \Symfony\Component\EventDispatcher\EventDispatcherInterface|\PHPUnit\Framework\MockObject\MockObject */ - private $dispatcher; + /** @var \Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface|\PHPUnit\Framework\MockObject\MockObject */ + private $siteAccessService; /** @var \Ibexa\Bundle\Core\EventListener\ConsoleCommandListener */ private $listener; @@ -46,11 +43,8 @@ class ConsoleCommandListenerTest extends TestCase protected function setUp(): void { parent::setUp(); - $this->siteAccess = new SiteAccess('test'); - $this->dispatcher = $this->createMock(EventDispatcherInterface::class); - $this->listener = new ConsoleCommandListener('default', $this->getSiteAccessProviderMock(), $this->dispatcher); - $this->listener->setSiteAccess($this->siteAccess); - $this->dispatcher->addSubscriber($this->listener); + $this->siteAccessService = $this->createMock(SiteAccessServiceInterface::class); + $this->listener = new ConsoleCommandListener('default', $this->getSiteAccessProviderMock(), $this->siteAccessService); $this->inputDefinition = new InputDefinition([new InputOption('siteaccess', null, InputOption::VALUE_OPTIONAL)]); $this->testOutput = new TestOutput(Output::VERBOSITY_QUIET, true); $this->command = self::createStub(Command::class); @@ -71,8 +65,8 @@ public function testInvalidSiteAccessDev(): void $this->expectException(InvalidSiteAccessException::class); $this->expectExceptionMessageMatches('/^Invalid SiteAccess \'foo\', matched by .+\\. Valid SiteAccesses are/'); - $this->dispatcher->expects(self::never()) - ->method('dispatch'); + $this->siteAccessService->expects(self::never()) + ->method('changeSiteAccess'); $input = new ArrayInput(['--siteaccess' => 'foo'], $this->inputDefinition); $event = new ConsoleCommandEvent($this->command, $input, $this->testOutput); $this->listener->setDebug(true); @@ -84,8 +78,8 @@ public function testInvalidSiteAccessProd(): void $this->expectException(InvalidSiteAccessException::class); $this->expectExceptionMessageMatches('/^Invalid SiteAccess \'foo\', matched by .+\\.$/'); - $this->dispatcher->expects(self::never()) - ->method('dispatch'); + $this->siteAccessService->expects(self::never()) + ->method('changeSiteAccess'); $input = new ArrayInput(['--siteaccess' => 'foo'], $this->inputDefinition); $event = new ConsoleCommandEvent($this->command, $input, $this->testOutput); $this->listener->setDebug(false); @@ -94,22 +88,22 @@ public function testInvalidSiteAccessProd(): void public function testValidSiteAccess(): void { - $this->dispatcher->expects(self::once()) - ->method('dispatch'); + $this->siteAccessService->expects(self::once()) + ->method('changeSiteAccess') + ->with(new SiteAccess('site1', 'cli')); $input = new ArrayInput(['--siteaccess' => 'site1'], $this->inputDefinition); $event = new ConsoleCommandEvent($this->command, $input, $this->testOutput); $this->listener->onConsoleCommand($event); - self::assertEquals(new SiteAccess('site1', 'cli'), $this->siteAccess); } public function testDefaultSiteAccess(): void { - $this->dispatcher->expects(self::once()) - ->method('dispatch'); + $this->siteAccessService->expects(self::once()) + ->method('changeSiteAccess') + ->with(new SiteAccess('default', 'cli')); $input = new ArrayInput([], $this->inputDefinition); $event = new ConsoleCommandEvent($this->command, $input, $this->testOutput); $this->listener->onConsoleCommand($event); - self::assertEquals(new SiteAccess('default', 'cli'), $this->siteAccess); } private function getSiteAccessProviderMock(): SiteAccess\SiteAccessProviderInterface diff --git a/tests/bundle/Core/Fragment/DecoratedFragmentRendererTest.php b/tests/bundle/Core/Fragment/DecoratedFragmentRendererTest.php index 3867ebe41e..e63383a559 100644 --- a/tests/bundle/Core/Fragment/DecoratedFragmentRendererTest.php +++ b/tests/bundle/Core/Fragment/DecoratedFragmentRendererTest.php @@ -10,6 +10,7 @@ use Ibexa\Bundle\Core\Fragment\DecoratedFragmentRenderer; use Ibexa\Bundle\Core\Fragment\SiteAccessSerializer; use Ibexa\Core\MVC\Symfony\SiteAccess; +use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\MockObject\MockObject; use Symfony\Component\HttpFoundation\Request; @@ -24,10 +25,13 @@ class DecoratedFragmentRendererTest extends FragmentRendererBaseTestCase { protected FragmentRendererInterface & MockObject $innerRenderer; + protected SiteAccessServiceInterface & MockObject $siteAccessService; + protected function setUp(): void { parent::setUp(); $this->innerRenderer = $this->createMock(FragmentRendererInterface::class); + $this->siteAccessService = $this->createMock(SiteAccessServiceInterface::class); } public function testSetFragmentPathNotRoutableRenderer(): void @@ -38,8 +42,9 @@ public function testSetFragmentPathNotRoutableRenderer(): void ->expects(self::never()) ->method('analyseLink'); + $this->siteAccessService->method('getCurrent')->willReturn($siteAccess); + $renderer = $this->getRenderer(); - $renderer->setSiteAccess($siteAccess); if ($renderer instanceof RoutableFragmentRenderer) { $renderer->setFragmentPath('foo'); } @@ -55,13 +60,14 @@ public function testSetFragmentPath(): void ->with('/foo') ->willReturn('/bar/foo'); + $this->siteAccessService->method('getCurrent')->willReturn($siteAccess); + $innerRenderer = $this->createMock(RoutableFragmentRenderer::class); $innerRenderer ->expects(self::once()) ->method('setFragmentPath') ->with('/bar/foo'); - $renderer = new DecoratedFragmentRenderer($innerRenderer, new SiteAccessSerializer($this->getSerializer())); - $renderer->setSiteAccess($siteAccess); + $renderer = new DecoratedFragmentRenderer($innerRenderer, new SiteAccessSerializer($this->getSerializer()), $this->siteAccessService); $renderer->setFragmentPath('/foo'); } @@ -136,11 +142,8 @@ 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, new SiteAccessSerializer($this->getSerializer())); + return new DecoratedFragmentRenderer($this->innerRenderer, new SiteAccessSerializer($this->getSerializer()), $this->siteAccessService); } } diff --git a/tests/bundle/Core/Routing/DefaultRouterTest.php b/tests/bundle/Core/Routing/DefaultRouterTest.php index 27fd6e65ce..23764a69c7 100644 --- a/tests/bundle/Core/Routing/DefaultRouterTest.php +++ b/tests/bundle/Core/Routing/DefaultRouterTest.php @@ -12,6 +12,7 @@ use Ibexa\Bundle\Core\SiteAccess\Matcher; use Ibexa\Core\MVC\Symfony\Routing\SimplifiedRequest; use Ibexa\Core\MVC\Symfony\SiteAccess; +use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\MockObject\MockObject; @@ -34,6 +35,8 @@ final class DefaultRouterTest extends TestCase private SiteAccess\SiteAccessRouterInterface&MockObject $siteAccessRouter; + private SiteAccessServiceInterface&MockObject $siteAccessService; + private RequestContext $requestContext; protected function setUp(): void @@ -41,13 +44,14 @@ protected function setUp(): void parent::setUp(); $this->innerRouter = $this->createMock(Router::class); $this->siteAccessRouter = $this->createMock(SiteAccess\SiteAccessRouterInterface::class); + $this->siteAccessService = $this->createMock(SiteAccessServiceInterface::class); $this->requestContext = new RequestContext(); $this->innerRouter->method('getContext')->willReturnCallback(fn (): RequestContext => $this->requestContext); } private function createRouter(): DefaultRouter { - return new DefaultRouter($this->innerRouter, $this->siteAccessRouter, self::NON_SITEACCESS_AWARE_ROUTES); + return new DefaultRouter($this->innerRouter, $this->siteAccessRouter, $this->siteAccessService, self::NON_SITEACCESS_AWARE_ROUTES); } public function testMatchRequestWithSemanticPathinfo(): void @@ -169,8 +173,8 @@ public function testGenerateWithSiteAccess( substr($urlComponents['path'], 0, strpos($urlComponents['path'], $relevantUri)) ); + $this->siteAccessService->method('getCurrent')->willReturn(new SiteAccess($saName, 'test', $matcher)); $router = $this->createRouter(); - $router->setSiteAccess(new SiteAccess($saName, 'test', $matcher)); self::assertSame($expectedUrl, $router->generate($routeName, [], $referenceType)); } @@ -214,8 +218,8 @@ public function testGenerateReverseSiteAccessMatch(): void ->with($routeName, []) ->willReturn($urlGenerated); + $this->siteAccessService->method('getCurrent')->willReturn(new SiteAccess('test', 'test', $this->createMock(Matcher::class))); $router = $this->createRouter(); - $router->setSiteAccess(new SiteAccess('test', 'test', $this->createMock(Matcher::class))); self::assertSame( $urlGenerated, @@ -266,7 +270,7 @@ public function testWarmUpDelegatesToInnerRouter(): void ->with('/cache', '/build') ->willReturn(['/cache/routes.php']); - $router = new DefaultRouter($innerRouter, $this->siteAccessRouter); + $router = new DefaultRouter($innerRouter, $this->siteAccessRouter, $this->siteAccessService); self::assertSame(['/cache/routes.php'], $router->warmUp('/cache', '/build')); } diff --git a/tests/bundle/IO/DependencyInjection/IbexaIOExtensionTest.php b/tests/bundle/IO/DependencyInjection/IbexaIOExtensionTest.php index 984e4f1d9c..a46cb7b31b 100644 --- a/tests/bundle/IO/DependencyInjection/IbexaIOExtensionTest.php +++ b/tests/bundle/IO/DependencyInjection/IbexaIOExtensionTest.php @@ -14,8 +14,12 @@ use Ibexa\Bundle\IO\DependencyInjection\ConfigurationFactory; use Ibexa\Bundle\IO\DependencyInjection\IbexaIOExtension; use Ibexa\Core\IO\UrlDecorator\AbsolutePrefix; +use Ibexa\Core\MVC\Symfony\SiteAccess; +use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessService; use Ibexa\Tests\Integration\Core\Repository\Container\Compiler\SetAllServicesPublicPass; use Matthias\SymfonyDependencyInjectionTest\PhpUnit\AbstractExtensionTestCase; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\Yaml\Yaml; class IbexaIOExtensionTest extends AbstractExtensionTestCase @@ -86,6 +90,10 @@ public function testUrlPrefixConfigurationIsUsedToDecorateUrl(): void ); $this->buildMinimalContainerForUrlPrefixTest(); + $siteAccessService = $this->container->get(SiteAccessService::class); + self::assertInstanceOf(SiteAccessService::class, $siteAccessService); + $siteAccessService->changeSiteAccess(new SiteAccess('site')); + $decorator = $this->container->get(AbsolutePrefix::class); self::assertEquals( @@ -101,6 +109,7 @@ private function buildMinimalContainerForUrlPrefixTest(): void $this->container->setParameter('kernel.debug', true); $this->container->setParameter('kernel.project_dir', self::FIXTURES_DIR); $this->container->setParameter('kernel.cache_dir', self::FIXTURES_DIR . '/cache'); + $this->container->setDefinition('event_dispatcher', new Definition(EventDispatcher::class)); $this->container->addCompilerPass(new ChainConfigResolverPass()); $this->container->addCompilerPass(new SetAllServicesPublicPass()); diff --git a/tests/integration/Core/LegacyTestContainerBuilder.php b/tests/integration/Core/LegacyTestContainerBuilder.php index 7d2e45f2dd..7038601452 100644 --- a/tests/integration/Core/LegacyTestContainerBuilder.php +++ b/tests/integration/Core/LegacyTestContainerBuilder.php @@ -13,7 +13,7 @@ use Ibexa\Contracts\Core\SiteAccess\ConfigProcessor; use Ibexa\Contracts\Core\SiteAccess\ConfigResolverInterface; use Ibexa\Core\Base\Container\Compiler; -use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessService; +use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface; use Ibexa\Tests\Integration\Core\Repository\Container\Compiler\SetAllServicesPublicPass; use Symfony\Component\Cache\Adapter\RedisAdapter; use Symfony\Component\Config\FileLocator; @@ -166,7 +166,7 @@ private function registerSiteAccessConfigProcessingServices(): void $definition = new Definition(ComplexConfigProcessor::class); $definition->setArgument('$configResolver', new Reference(ConfigResolverInterface::class)); - $definition->setArgument('$siteAccessService', new Reference(SiteAccessService::class)); + $definition->setArgument('$siteAccessService', new Reference(SiteAccessServiceInterface::class)); $this->setDefinition(ConfigProcessor::class, $definition); } diff --git a/tests/integration/Core/Resources/settings/common.yml b/tests/integration/Core/Resources/settings/common.yml index 5a43ba3cdb..e7c57f574c 100644 --- a/tests/integration/Core/Resources/settings/common.yml +++ b/tests/integration/Core/Resources/settings/common.yml @@ -106,13 +106,15 @@ services: Ibexa\Core\MVC\Symfony\SiteAccess: arguments: ['default', !php/const Ibexa\Core\MVC\Symfony\SiteAccess::MATCHING_TYPE_UNINITIALIZED] + event_dispatcher: + class: Symfony\Component\EventDispatcher\EventDispatcher + Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessService: class: Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessService arguments: - '@ibexa.siteaccess.provider' - '@ibexa.config.resolver' - calls: - - [setSiteAccess, ['@Ibexa\Core\MVC\Symfony\SiteAccess']] + - '@event_dispatcher' Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface: alias: 'Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessService' diff --git a/tests/lib/Helper/ContentPreviewHelperTest.php b/tests/lib/Helper/ContentPreviewHelperTest.php index fd6940ab60..7c6b680608 100644 --- a/tests/lib/Helper/ContentPreviewHelperTest.php +++ b/tests/lib/Helper/ContentPreviewHelperTest.php @@ -10,17 +10,15 @@ use Ibexa\Contracts\Core\Repository\Values\Content\Content as APIContent; use Ibexa\Contracts\Core\Repository\Values\Content\Location as APILocation; use Ibexa\Core\Helper\ContentPreviewHelper; -use Ibexa\Core\MVC\Symfony\Event\ScopeChangeEvent; -use Ibexa\Core\MVC\Symfony\MVCEvents; use Ibexa\Core\MVC\Symfony\SiteAccess; use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessRouterInterface; +use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface; use PHPUnit\Framework\TestCase; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; class ContentPreviewHelperTest extends TestCase { /** @var \PHPUnit\Framework\MockObject\MockObject */ - private $eventDispatcher; + private $siteAccessService; /** @var \PHPUnit\Framework\MockObject\MockObject */ private $siteAccessRouter; @@ -31,9 +29,9 @@ class ContentPreviewHelperTest extends TestCase protected function setUp(): void { parent::setUp(); - $this->eventDispatcher = $this->createMock(EventDispatcherInterface::class); + $this->siteAccessService = $this->createMock(SiteAccessServiceInterface::class); $this->siteAccessRouter = $this->createMock(SiteAccessRouterInterface::class); - $this->previewHelper = new ContentPreviewHelper($this->eventDispatcher, $this->siteAccessRouter); + $this->previewHelper = new ContentPreviewHelper($this->siteAccessRouter, $this->siteAccessService); } public function testChangeConfigScope(): void @@ -47,14 +45,12 @@ public function testChangeConfigScope(): void ->with(self::equalTo($newSiteAccessName)) ->willReturn($newSiteAccess); - $event = new ScopeChangeEvent($newSiteAccess); - $this->eventDispatcher + $this->siteAccessService ->expects(self::once()) - ->method('dispatch') - ->with(self::equalTo($event), MVCEvents::CONFIG_SCOPE_CHANGE); + ->method('changeSiteAccess') + ->with($newSiteAccess) + ->willReturn($newSiteAccess); - $originalSiteAccess = new SiteAccess('foo', 'bar'); - $this->previewHelper->setSiteAccess($originalSiteAccess); self::assertEquals( $newSiteAccess, $this->previewHelper->changeConfigScope($newSiteAccessName) @@ -64,31 +60,34 @@ public function testChangeConfigScope(): void public function testRestoreConfigScope(): void { $originalSiteAccess = new SiteAccess('foo', 'bar'); - $event = new ScopeChangeEvent($originalSiteAccess); - $this->eventDispatcher + $this->siteAccessService ->expects(self::once()) - ->method('dispatch') - ->with(self::equalTo($event), MVCEvents::CONFIG_SCOPE_RESTORE); + ->method('restoreSiteAccess') + ->willReturn($originalSiteAccess); - $this->previewHelper->setSiteAccess($originalSiteAccess); self::assertEquals( $originalSiteAccess, $this->previewHelper->restoreConfigScope() ); } - public function testPreviewActive(): void + public function testGetOriginalSiteAccess(): void { $originalSiteAccess = new SiteAccess('foo', 'bar'); - $this->previewHelper->setSiteAccess($originalSiteAccess); + $this->siteAccessService + ->method('getCurrent') + ->willReturn($originalSiteAccess); + + self::assertSame($originalSiteAccess, $this->previewHelper->getOriginalSiteAccess()); + } + public function testPreviewActive(): void + { self::assertFalse($this->previewHelper->isPreviewActive()); $this->previewHelper->setPreviewActive(true); self::assertTrue($this->previewHelper->isPreviewActive()); $this->previewHelper->setPreviewActive(false); self::assertFalse($this->previewHelper->isPreviewActive()); - - self::assertNotSame($originalSiteAccess, $this->previewHelper->getOriginalSiteAccess()); } public function testPreviewedContent(): void diff --git a/tests/lib/MVC/Symfony/EventListener/SiteAccessMatchListenerTest.php b/tests/lib/MVC/Symfony/EventListener/SiteAccessMatchListenerTest.php index 8b07fade12..59e680e18c 100644 --- a/tests/lib/MVC/Symfony/EventListener/SiteAccessMatchListenerTest.php +++ b/tests/lib/MVC/Symfony/EventListener/SiteAccessMatchListenerTest.php @@ -25,6 +25,7 @@ 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\SiteAccess\SiteAccessServiceInterface; use Ibexa\Core\MVC\Symfony\SiteAccessGroup; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\MockObject\MockObject; @@ -51,6 +52,8 @@ final class SiteAccessMatchListenerTest extends TestCase private SiteAccessMatcherRegistryInterface&MockObject $registry; + private SiteAccessServiceInterface&MockObject $siteAccessService; + private SiteAccessMatchListener $listener; protected function setUp(): void @@ -59,10 +62,12 @@ protected function setUp(): void $this->saRouter = $this->createMock(Router::class); $this->eventDispatcher = $this->createMock(EventDispatcherInterface::class); $this->registry = $this->createMock(SiteAccessMatcherRegistryInterface::class); + $this->siteAccessService = $this->createMock(SiteAccessServiceInterface::class); $this->listener = new SiteAccessMatchListener( $this->saRouter, $this->eventDispatcher, - $this->getSerializer() + $this->getSerializer(), + $this->siteAccessService ); } @@ -229,6 +234,21 @@ public function testOnKernelRequestUserHashWithOriginalRequest(): void $this->assertRequestHasSiteAccess($request, $originalRequest, $siteAccess); } + /** + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException + */ + public function testOnKernelRequestSubRequestPushesToSiteAccessService(): void + { + $siteAccess = new SiteAccess('test'); + $scheme = 'https'; + $host = 'phoenix-rises.fm'; + $port = 1234; + $path = '/foo/bar'; + $request = Request::create(sprintf('%s://%s:%d%s', $scheme, $host, $port, $path)); + $this->assertRequestHasSiteAccess($request, null, $siteAccess, HttpKernelInterface::SUB_REQUEST); + } + private function serializeSiteAccess(SiteAccess $siteAccess): string { return $this->getSerializer()->serialize($siteAccess, 'json'); @@ -241,13 +261,14 @@ private function serializeSiteAccess(SiteAccess $siteAccess): string private function assertRequestHasSiteAccess( Request $request, ?Request $originalRequest, - SiteAccess $siteAccess + SiteAccess $siteAccess, + int $requestType = HttpKernelInterface::MAIN_REQUEST ): void { $originalRequest ??= $request; $event = new RequestEvent( self::createStub(HttpKernelInterface::class), $request, - HttpKernelInterface::MAIN_REQUEST + $requestType ); $simplifiedRequest = new SimplifiedRequest( @@ -274,6 +295,17 @@ private function assertRequestHasSiteAccess( ->with(self::equalTo($postSAMatchEvent), MVCEvents::SITEACCESS) ; + if ($requestType === HttpKernelInterface::SUB_REQUEST) { + $this->siteAccessService + ->expects(self::once()) + ->method('changeSiteAccess') + ->with($siteAccess); + } else { + $this->siteAccessService + ->expects(self::never()) + ->method('changeSiteAccess'); + } + $this->listener->onKernelRequest($event); self::assertSame($siteAccess, $request->attributes->get('siteaccess')); } @@ -300,6 +332,10 @@ private function dispatchRequestEvent(Request $request, SiteAccess $siteAccess): ->method('dispatch') ->with($postSAMatchEvent, MVCEvents::SITEACCESS); + $this->siteAccessService + ->expects(self::never()) + ->method('changeSiteAccess'); + $this->listener->onKernelRequest($event); } diff --git a/tests/lib/MVC/Symfony/EventListener/SiteAccessRestoreListenerTest.php b/tests/lib/MVC/Symfony/EventListener/SiteAccessRestoreListenerTest.php index cb36739c25..7b69a30a9f 100644 --- a/tests/lib/MVC/Symfony/EventListener/SiteAccessRestoreListenerTest.php +++ b/tests/lib/MVC/Symfony/EventListener/SiteAccessRestoreListenerTest.php @@ -12,6 +12,7 @@ use Ibexa\Core\MVC\Symfony\EventListener\SiteAccessRestoreListener; use Ibexa\Core\MVC\Symfony\MVCEvents; use Ibexa\Core\MVC\Symfony\SiteAccess; +use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\MockObject\MockObject; @@ -33,6 +34,8 @@ final class SiteAccessRestoreListenerTest extends TestCase private RequestStack $requestStack; + private SiteAccessServiceInterface&MockObject $siteAccessService; + private SiteAccessRestoreListener $listener; protected function setUp(): void @@ -41,9 +44,11 @@ protected function setUp(): void $this->kernel = self::createStub(HttpKernelInterface::class); $this->eventDispatcher = $this->createMock(EventDispatcherInterface::class); $this->requestStack = new RequestStack(); + $this->siteAccessService = $this->createMock(SiteAccessServiceInterface::class); $this->listener = new SiteAccessRestoreListener( $this->requestStack, - $this->eventDispatcher + $this->eventDispatcher, + $this->siteAccessService ); } @@ -78,6 +83,10 @@ static function (PostSiteAccessMatchEvent $event) use ($parentSiteAccess, $paren MVCEvents::SITEACCESS ); + $this->siteAccessService + ->expects(self::once()) + ->method('restoreSiteAccess'); + $this->listener->onKernelFinishRequest( new FinishRequestEvent($this->kernel, $subRequest, HttpKernelInterface::SUB_REQUEST) ); @@ -97,12 +106,26 @@ public function testNoDispatch(array $requests, int $requestType): void self::assertNotNull($finishingRequest); $this->eventDispatcher->expects(self::never())->method('dispatch'); + $this->siteAccessService->expects(self::never())->method('restoreSiteAccess'); $this->listener->onKernelFinishRequest( new FinishRequestEvent($this->kernel, $finishingRequest, $requestType) ); } + public function testRestoresSiteAccessServiceWithoutDispatchingWhenNoParentRequest(): void + { + $subRequest = self::createRequestWithSiteAccess(new SiteAccess('admin', 'uri:element')); + $this->requestStack->push($subRequest); + + $this->eventDispatcher->expects(self::never())->method('dispatch'); + $this->siteAccessService->expects(self::once())->method('restoreSiteAccess'); + + $this->listener->onKernelFinishRequest( + new FinishRequestEvent($this->kernel, $subRequest, HttpKernelInterface::SUB_REQUEST) + ); + } + /** * @return iterable */ diff --git a/tests/lib/MVC/Symfony/Routing/GeneratorTest.php b/tests/lib/MVC/Symfony/Routing/GeneratorTest.php index f2b355648b..2e2798b0cd 100644 --- a/tests/lib/MVC/Symfony/Routing/GeneratorTest.php +++ b/tests/lib/MVC/Symfony/Routing/GeneratorTest.php @@ -10,6 +10,7 @@ use Ibexa\Core\MVC\Symfony\Routing\Generator; use Ibexa\Core\MVC\Symfony\SiteAccess; use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessRouterInterface; +use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface; use Ibexa\Core\MVC\Symfony\SiteAccess\URILexer; use Ibexa\Core\Repository\Values\Content\Location; use PHPUnit\Framework\Attributes\DataProvider; @@ -29,12 +30,19 @@ class GeneratorTest extends TestCase /** @var \PHPUnit\Framework\MockObject\MockObject */ private $logger; + /** @var \Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface|\PHPUnit\Framework\MockObject\MockObject */ + private $siteAccessService; + protected function setUp(): void { parent::setUp(); $this->siteAccessRouter = $this->createMock(SiteAccessRouterInterface::class); $this->logger = $this->createMock(LoggerInterface::class); - $this->generator = $this->getMockForAbstractClass(Generator::class); + $this->siteAccessService = $this->createMock(SiteAccessServiceInterface::class); + $this->generator = $this->getMockBuilder(Generator::class) + ->setConstructorArgs([$this->siteAccessService]) + ->onlyMethods(['doGenerate']) + ->getMock(); $this->generator->setSiteAccessRouter($this->siteAccessRouter); $this->generator->setLogger($this->logger); } @@ -61,7 +69,7 @@ public static function generateProvider(): array public function testSimpleGenerate($urlResource, array $parameters, $referenceType): void { $matcher = $this->createMock(URILexer::class); - $this->generator->setSiteAccess(new SiteAccess('test', 'fake', $matcher)); + $this->siteAccessService->method('getCurrent')->willReturn(new SiteAccess('test', 'fake', $matcher)); $baseUrl = '/base/url'; $requestContext = new RequestContext($baseUrl); @@ -72,14 +80,14 @@ public function testSimpleGenerate($urlResource, array $parameters, $referenceTy ->expects(self::once()) ->method('doGenerate') ->with($urlResource, $parameters) - ->will(self::returnValue($uri)); + ->willReturn($uri); $fullUri = $baseUrl . $uri; $matcher ->expects(self::once()) ->method('analyseLink') ->with($uri) - ->will(self::returnValue($uri)); + ->willReturn($uri); if ($referenceType === UrlGeneratorInterface::ABSOLUTE_URL) { $fullUri = $requestContext->getScheme() . '://' . $requestContext->getHost() . $baseUrl . $uri; @@ -92,7 +100,7 @@ public function testSimpleGenerate($urlResource, array $parameters, $referenceTy public function testGenerateWithSiteAccessNoReverseMatch($urlResource, array $parameters, $referenceType): void { $matcher = $this->createMock(URILexer::class); - $this->generator->setSiteAccess(new SiteAccess('test', 'test', $matcher)); + $this->siteAccessService->method('getCurrent')->willReturn(new SiteAccess('test', 'test', $matcher)); $baseUrl = '/base/url'; $requestContext = new RequestContext($baseUrl); @@ -103,14 +111,14 @@ public function testGenerateWithSiteAccessNoReverseMatch($urlResource, array $pa ->expects(self::once()) ->method('doGenerate') ->with($urlResource, $parameters) - ->will(self::returnValue($uri)); + ->willReturn($uri); $fullUri = $baseUrl . $uri; $matcher ->expects(self::once()) ->method('analyseLink') ->with($uri) - ->will(self::returnValue($uri)); + ->willReturn($uri); if ($referenceType === UrlGeneratorInterface::ABSOLUTE_URL) { $fullUri = $requestContext->getScheme() . '://' . $requestContext->getHost() . $baseUrl . $uri; @@ -121,7 +129,7 @@ public function testGenerateWithSiteAccessNoReverseMatch($urlResource, array $pa ->expects(self::once()) ->method('matchByName') ->with($siteAccessName) - ->will(self::returnValue(null)); + ->willReturn(null); $this->logger ->expects(self::once()) ->method('notice'); diff --git a/tests/lib/MVC/Symfony/Routing/UrlAliasGeneratorTest.php b/tests/lib/MVC/Symfony/Routing/UrlAliasGeneratorTest.php index 813ca4f24b..7e99a2426c 100644 --- a/tests/lib/MVC/Symfony/Routing/UrlAliasGeneratorTest.php +++ b/tests/lib/MVC/Symfony/Routing/UrlAliasGeneratorTest.php @@ -16,6 +16,7 @@ use Ibexa\Core\MVC\Symfony\Routing\Generator\UrlAliasGenerator; use Ibexa\Core\MVC\Symfony\SiteAccess; use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessRouterInterface; +use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface; use Ibexa\Core\Repository\Mapper\RoleDomainMapper; use Ibexa\Core\Repository\Permission\LimitationService; use Ibexa\Core\Repository\Permission\PermissionResolver; @@ -52,6 +53,9 @@ class UrlAliasGeneratorTest extends TestCase /** @var \PHPUnit\Framework\MockObject\MockObject */ private $configResolver; + /** @var \Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface|\PHPUnit\Framework\MockObject\MockObject */ + private $siteAccessService; + protected function setUp(): void { parent::setUp(); @@ -59,6 +63,7 @@ protected function setUp(): void $this->logger = $this->createMock(LoggerInterface::class); $this->siteAccessRouter = self::createStub(SiteAccessRouterInterface::class); $this->configResolver = $this->createMock(ConfigResolverInterface::class); + $this->siteAccessService = $this->createMock(SiteAccessServiceInterface::class); $repositoryClass = Repository::class; $this->repository = $repository = $this ->getMockBuilder($repositoryClass) @@ -95,6 +100,7 @@ protected function setUp(): void $this->repository, $this->router, $this->configResolver, + $this->siteAccessService, $urlAliasCharmap ); $this->urlAliasGenerator->setLogger($this->logger); @@ -176,7 +182,7 @@ public function testDoGenerate(URLAlias $urlAlias, array $parameters, $expected) ->with($location, false) ->will(self::returnValue([$urlAlias])); - $this->urlAliasGenerator->setSiteAccess(new SiteAccess('test', 'fake', self::createStub(SiteAccess\URILexer::class))); + $this->siteAccessService->method('getCurrent')->willReturn(new SiteAccess('test', 'fake', self::createStub(SiteAccess\URILexer::class))); self::assertSame($expected, $this->urlAliasGenerator->doGenerate($location, $parameters)); } @@ -276,7 +282,7 @@ static function ($location) use ($treeRootUrlAlias): URLAlias { ) ); - $this->urlAliasGenerator->setSiteAccess(new SiteAccess('test', 'fake', self::createStub(SiteAccess\URILexer::class))); + $this->siteAccessService->method('getCurrent')->willReturn(new SiteAccess('test', 'fake', self::createStub(SiteAccess\URILexer::class))); self::assertSame($expected, $this->urlAliasGenerator->doGenerate($location, $parameters)); } @@ -390,7 +396,7 @@ public function testDoGenerateWithSiteAccessLoadsLocationWithLanguages(): void ->with($location, null, false, ['ger-DE']) ->willReturn($treeRootUrlAliases[$location->id]); - $this->urlAliasGenerator->setSiteAccess( + $this->siteAccessService->method('getCurrent')->willReturn( new SiteAccess( $gerSiteAccess, 'default', diff --git a/tests/lib/MVC/Symfony/Routing/UrlAliasRouterTest.php b/tests/lib/MVC/Symfony/Routing/UrlAliasRouterTest.php index 6c48dbf3af..2e0bd7de37 100644 --- a/tests/lib/MVC/Symfony/Routing/UrlAliasRouterTest.php +++ b/tests/lib/MVC/Symfony/Routing/UrlAliasRouterTest.php @@ -18,6 +18,7 @@ use Ibexa\Core\MVC\Symfony\Routing\UrlAliasRouter; use Ibexa\Core\MVC\Symfony\SiteAccess; use Ibexa\Core\MVC\Symfony\SiteAccess\Matcher; +use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface; use Ibexa\Core\MVC\Symfony\View\Manager as ViewManager; use Ibexa\Core\Repository\Repository; use Ibexa\Core\Repository\Values\Content\Location; @@ -78,6 +79,7 @@ protected function setUp(): void $repository, self::createStub(RouterInterface::class), self::createStub(ConfigResolverInterface::class), + self::createStub(SiteAccessServiceInterface::class), ] ) ->getMock(); diff --git a/tests/lib/MVC/Symfony/Security/HttpUtilsTest.php b/tests/lib/MVC/Symfony/Security/HttpUtilsTest.php index 6dc92ec9cc..575183e575 100644 --- a/tests/lib/MVC/Symfony/Security/HttpUtilsTest.php +++ b/tests/lib/MVC/Symfony/Security/HttpUtilsTest.php @@ -9,6 +9,7 @@ use Ibexa\Core\MVC\Symfony\Security\HttpUtils; use Ibexa\Core\MVC\Symfony\SiteAccess; +use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; @@ -20,8 +21,8 @@ class HttpUtilsTest extends TestCase public function testGenerateUriStandard($uri, $isUriRouteName, $expected): void { $urlGenerator = $this->createMock(UrlGeneratorInterface::class); - $httpUtils = new HttpUtils($urlGenerator); - $httpUtils->setSiteAccess(new SiteAccess('test')); + $siteAccessService = $this->createSiteAccessServiceReturning(new SiteAccess('test')); + $httpUtils = new HttpUtils($urlGenerator, null, null, null, $siteAccessService); $request = Request::create('http://ezpublish.dev/'); $request->attributes->set('siteaccess', new SiteAccess('test')); $requestAttributes = ['foo' => 'bar', 'some' => 'thing']; @@ -32,7 +33,7 @@ public function testGenerateUriStandard($uri, $isUriRouteName, $expected): void ->expects(self::once()) ->method('generate') ->with($uri, $requestAttributes, UrlGeneratorInterface::ABSOLUTE_URL) - ->will(self::returnValue($expected . '?' . http_build_query($requestAttributes))); + ->willReturn($expected . '?' . http_build_query($requestAttributes)); } self::assertSame($expected, $httpUtils->generateUri($request, $uri)); @@ -62,13 +63,13 @@ public function testGenerateUri($uri, $isUriRouteName, $siteAccessUri, $expected ->expects(self::once()) ->method('analyseLink') ->with($uri) - ->will(self::returnValue($siteAccessUri . $uri)); + ->willReturn($siteAccessUri . $uri); $siteAccess->matcher = $matcher; } $urlGenerator = $this->createMock(UrlGeneratorInterface::class); - $httpUtils = new HttpUtils($urlGenerator); - $httpUtils->setSiteAccess($siteAccess); + $siteAccessService = $this->createSiteAccessServiceReturning($siteAccess); + $httpUtils = new HttpUtils($urlGenerator, null, null, null, $siteAccessService); $request = Request::create('http://ezpublish.dev/'); $request->attributes->set('siteaccess', $siteAccess); $requestAttributes = ['foo' => 'bar', 'some' => 'thing']; @@ -79,7 +80,7 @@ public function testGenerateUri($uri, $isUriRouteName, $siteAccessUri, $expected ->expects(self::once()) ->method('generate') ->with($uri, $requestAttributes, UrlGeneratorInterface::ABSOLUTE_URL) - ->will(self::returnValue($expected . '?' . http_build_query($requestAttributes))); + ->willReturn($expected . '?' . http_build_query($requestAttributes)); } $res = $httpUtils->generateUri($request, $uri); @@ -102,8 +103,8 @@ public static function generateUriProvider(): array public function testCheckRequestPathStandard(): void { - $httpUtils = new HttpUtils(); - $httpUtils->setSiteAccess(new SiteAccess('test')); + $siteAccessService = $this->createSiteAccessServiceReturning(new SiteAccess('test')); + $httpUtils = new HttpUtils(null, null, null, null, $siteAccessService); $request = Request::create('http://ezpublish.dev/foo/bar'); self::assertTrue($httpUtils->checkRequestPath($request, '/foo/bar')); } @@ -118,12 +119,12 @@ public function testCheckRequestPath($path, $siteAccessUri, $requestUri, $expect ->expects(self::once()) ->method('analyseLink') ->with($path) - ->will(self::returnValue($siteAccessUri . $path)); + ->willReturn($siteAccessUri . $path); $siteAccess->matcher = $matcher; } - $httpUtils = new HttpUtils(); - $httpUtils->setSiteAccess($siteAccess); + $siteAccessService = $this->createSiteAccessServiceReturning($siteAccess); + $httpUtils = new HttpUtils(null, null, null, null, $siteAccessService); $request = Request::create($requestUri); self::assertSame($expected, $httpUtils->checkRequestPath($request, $path)); } @@ -142,4 +143,12 @@ public static function checkRequestPathProvider(): array ['/foo/bar', '/blabla', 'http://ezpublish.dev/blabla/foo/bar', true], ]; } + + private function createSiteAccessServiceReturning(SiteAccess $siteAccess): SiteAccessServiceInterface + { + $siteAccessService = $this->createMock(SiteAccessServiceInterface::class); + $siteAccessService->method('getCurrent')->willReturn($siteAccess); + + return $siteAccessService; + } } diff --git a/tests/lib/MVC/Symfony/SiteAccess/SiteAccessServiceTest.php b/tests/lib/MVC/Symfony/SiteAccess/SiteAccessServiceTest.php index 58056d3e4c..83458a7d73 100644 --- a/tests/lib/MVC/Symfony/SiteAccess/SiteAccessServiceTest.php +++ b/tests/lib/MVC/Symfony/SiteAccess/SiteAccessServiceTest.php @@ -11,13 +11,19 @@ use ArrayIterator; use Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException; use Ibexa\Contracts\Core\SiteAccess\ConfigResolverInterface; +use Ibexa\Core\MVC\Symfony\Event\PostSiteAccessMatchEvent; use Ibexa\Core\MVC\Symfony\Event\ScopeChangeEvent; use Ibexa\Core\MVC\Symfony\MVCEvents; use Ibexa\Core\MVC\Symfony\SiteAccess; use Ibexa\Core\MVC\Symfony\SiteAccess\Provider\StaticSiteAccessProvider; use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessProviderInterface; use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessService; +use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface; use PHPUnit\Framework\TestCase; +use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\HttpKernelInterface; class SiteAccessServiceTest extends TestCase { @@ -25,10 +31,10 @@ class SiteAccessServiceTest extends TestCase private const UNDEFINED_SA_NAME = 'undefined_sa'; private const SA_GROUP = 'group'; - /** @var \Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessProviderInterface|\PHPUnit\Framework\MockObject\MockObject */ + /** @var \Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessProviderInterface&\PHPUnit\Framework\MockObject\MockObject */ private $provider; - /** @var \Ibexa\Contracts\Core\SiteAccess\ConfigResolverInterface|\PHPUnit\Framework\MockObject\MockObject */ + /** @var \Ibexa\Contracts\Core\SiteAccess\ConfigResolverInterface&\PHPUnit\Framework\MockObject\MockObject */ private $configResolver; /** @var \Ibexa\Core\MVC\Symfony\SiteAccess */ @@ -50,27 +56,39 @@ protected function setUp(): void $this->configResolverParameters = $this->getConfigResolverParameters(); } - public function testGetCurrentSiteAccess(): void + public function testGetCurrentIsNullInitially(): void { - $service = new SiteAccessService( - self::createStub(SiteAccessProviderInterface::class), - self::createStub(ConfigResolverInterface::class) - ); + self::assertNull($this->createService()->getCurrent()); + } - self::assertNull($service->getCurrent()); + public function testOnSiteAccessMatchResetsStackOnMainRequest(): void + { + $service = $this->createService(); $siteAccess = new SiteAccess('default'); - $service->setSiteAccess($siteAccess); + $service->onSiteAccessMatch($this->createSiteAccessMatchEvent($siteAccess, HttpKernelInterface::MAIN_REQUEST)); + self::assertSame($siteAccess, $service->getCurrent()); + } + + public function testOnSiteAccessMatchIsNoopOnSubRequest(): void + { + $service = $this->createService(); - $service->setSiteAccess(null); - self::assertNull($service->getCurrent()); + $mainSiteAccess = new SiteAccess('main'); + $service->onSiteAccessMatch($this->createSiteAccessMatchEvent($mainSiteAccess, HttpKernelInterface::MAIN_REQUEST)); + + $subRequestSiteAccess = new SiteAccess('sub_request'); + $service->onSiteAccessMatch($this->createSiteAccessMatchEvent($subRequestSiteAccess, HttpKernelInterface::SUB_REQUEST)); + + self::assertSame($mainSiteAccess, $service->getCurrent()); } public function testGetSubscribedEvents(): void { self::assertSame( [ + MVCEvents::SITEACCESS => 'onSiteAccessMatch', MVCEvents::CONFIG_SCOPE_CHANGE => 'onConfigScopeChange', MVCEvents::CONFIG_SCOPE_RESTORE => 'onConfigScopeRestore', ], @@ -78,77 +96,119 @@ public function testGetSubscribedEvents(): void ); } - public function testOnConfigScopeChangeMakesGetCurrentReflectTheNewSiteAccess(): void + public function testChangeSiteAccessPushesAndReturnsTheGivenSiteAccess(): void { - $service = new SiteAccessService( - self::createStub(SiteAccessProviderInterface::class), - self::createStub(ConfigResolverInterface::class) - ); + $service = $this->createService(); $baseSiteAccess = new SiteAccess('base'); - $service->setSiteAccess($baseSiteAccess); + $service->onSiteAccessMatch($this->createSiteAccessMatchEvent($baseSiteAccess, HttpKernelInterface::MAIN_REQUEST)); $previewSiteAccess = new SiteAccess('preview'); - $service->onConfigScopeChange(new ScopeChangeEvent($previewSiteAccess)); + self::assertSame($previewSiteAccess, $service->changeSiteAccess($previewSiteAccess)); + self::assertSame($previewSiteAccess, $service->getCurrent()); + } + public function testChangeSiteAccessDispatchesEventWithoutTriggeringTheDeprecatedHandler(): void + { + $eventDispatcher = new EventDispatcher(); + $service = $this->createService($eventDispatcher); + + $dispatchedEvents = []; + $eventDispatcher->addListener(MVCEvents::CONFIG_SCOPE_CHANGE, static function (ScopeChangeEvent $event) use (&$dispatchedEvents): void { + $dispatchedEvents[] = $event->getSiteAccess(); + }); + + $previewSiteAccess = new SiteAccess('preview'); + $service->changeSiteAccess($previewSiteAccess); + + // The event is still dispatched for other, unrelated listeners to observe... + self::assertSame([$previewSiteAccess], $dispatchedEvents); + // ...but the stack was mutated exactly once by changeSiteAccess() itself, proving + // the service's own onConfigScopeChange() (which would push a 2nd time) did not react. self::assertSame($previewSiteAccess, $service->getCurrent()); } - public function testOnConfigScopeRestoreBringsBackThePreviousSiteAccess(): void + public function testRestoreSiteAccessPopsStackAndReturnsThePreviousSiteAccess(): void { - $service = new SiteAccessService( - self::createStub(SiteAccessProviderInterface::class), - self::createStub(ConfigResolverInterface::class) - ); + $service = $this->createService(); $baseSiteAccess = new SiteAccess('base'); - $service->setSiteAccess($baseSiteAccess); + $service->onSiteAccessMatch($this->createSiteAccessMatchEvent($baseSiteAccess, HttpKernelInterface::MAIN_REQUEST)); $previewSiteAccess = new SiteAccess('preview'); - $service->onConfigScopeChange(new ScopeChangeEvent($previewSiteAccess)); - $service->onConfigScopeRestore(new ScopeChangeEvent($baseSiteAccess)); + $service->changeSiteAccess($previewSiteAccess); + self::assertSame($baseSiteAccess, $service->restoreSiteAccess()); self::assertSame($baseSiteAccess, $service->getCurrent()); } - public function testOnConfigScopeRestoreNeverDropsTheBaseSiteAccess(): void + public function testRestoreSiteAccessNeverDropsBelowTheBaseSiteAccess(): void { - $service = new SiteAccessService( - self::createStub(SiteAccessProviderInterface::class), - self::createStub(ConfigResolverInterface::class) - ); + $service = $this->createService(); $baseSiteAccess = new SiteAccess('base'); - $service->setSiteAccess($baseSiteAccess); - - $service->onConfigScopeRestore(new ScopeChangeEvent($baseSiteAccess)); + $service->onSiteAccessMatch($this->createSiteAccessMatchEvent($baseSiteAccess, HttpKernelInterface::MAIN_REQUEST)); + self::assertSame($baseSiteAccess, $service->restoreSiteAccess()); self::assertSame($baseSiteAccess, $service->getCurrent()); } - public function testNestedConfigScopeChangesAndRestoresRoundTripLikeAStack(): void + public function testRestoreSiteAccessReturnsNullWhenStackIsEmpty(): void { - $service = new SiteAccessService( - self::createStub(SiteAccessProviderInterface::class), - self::createStub(ConfigResolverInterface::class) - ); + self::assertNull($this->createService()->restoreSiteAccess()); + } + + public function testNestedChangeSiteAccessAndRestoreSiteAccessRoundTripLikeAStack(): void + { + $service = $this->createService(); $baseSiteAccess = new SiteAccess('base'); - $service->setSiteAccess($baseSiteAccess); + $service->onSiteAccessMatch($this->createSiteAccessMatchEvent($baseSiteAccess, HttpKernelInterface::MAIN_REQUEST)); $firstSiteAccess = new SiteAccess('first'); $secondSiteAccess = new SiteAccess('second'); - $service->onConfigScopeChange(new ScopeChangeEvent($firstSiteAccess)); + $service->changeSiteAccess($firstSiteAccess); self::assertSame($firstSiteAccess, $service->getCurrent()); - $service->onConfigScopeChange(new ScopeChangeEvent($secondSiteAccess)); + $service->changeSiteAccess($secondSiteAccess); self::assertSame($secondSiteAccess, $service->getCurrent()); - $service->onConfigScopeRestore(new ScopeChangeEvent($firstSiteAccess)); - self::assertSame($firstSiteAccess, $service->getCurrent()); + self::assertSame($firstSiteAccess, $service->restoreSiteAccess()); + self::assertSame($baseSiteAccess, $service->restoreSiteAccess()); + } + + public function testOnConfigScopeChangeReactsToAManuallyDispatchedEventAndTriggersDeprecation(): void + { + $this->expectUserDeprecationMessage('Since ibexa/core 6.0: Dispatching ' . ScopeChangeEvent::class . ' manually under MVCEvents::CONFIG_SCOPE_CHANGE is deprecated and will no longer be reflected by ' . SiteAccessService::class . '::getCurrent() in 7.0. Call ' . SiteAccessServiceInterface::class . '::changeSiteAccess() instead.'); + + $eventDispatcher = new EventDispatcher(); + $service = $this->createService($eventDispatcher); + + $baseSiteAccess = new SiteAccess('base'); + $service->onSiteAccessMatch($this->createSiteAccessMatchEvent($baseSiteAccess, HttpKernelInterface::MAIN_REQUEST)); + + $previewSiteAccess = new SiteAccess('preview'); + $eventDispatcher->dispatch(new ScopeChangeEvent($previewSiteAccess), MVCEvents::CONFIG_SCOPE_CHANGE); + + self::assertSame($previewSiteAccess, $service->getCurrent()); + } + + public function testOnConfigScopeRestoreReactsToAManuallyDispatchedEventAndTriggersDeprecation(): void + { + $this->expectUserDeprecationMessage('Since ibexa/core 6.0: Dispatching ' . ScopeChangeEvent::class . ' manually under MVCEvents::CONFIG_SCOPE_RESTORE is deprecated and will no longer be reflected by ' . SiteAccessService::class . '::getCurrent() in 7.0. Call ' . SiteAccessServiceInterface::class . '::restoreSiteAccess() instead.'); + + $eventDispatcher = new EventDispatcher(); + $service = $this->createService($eventDispatcher); + + $baseSiteAccess = new SiteAccess('base'); + $service->onSiteAccessMatch($this->createSiteAccessMatchEvent($baseSiteAccess, HttpKernelInterface::MAIN_REQUEST)); + + $previewSiteAccess = new SiteAccess('preview'); + $service->changeSiteAccess($previewSiteAccess); + + $eventDispatcher->dispatch(new ScopeChangeEvent($baseSiteAccess), MVCEvents::CONFIG_SCOPE_RESTORE); - $service->onConfigScopeRestore(new ScopeChangeEvent($baseSiteAccess)); self::assertSame($baseSiteAccess, $service->getCurrent()); } @@ -160,7 +220,8 @@ public function testGetSiteAccess(): void ); $service = new SiteAccessService( $staticSiteAccessProvider, - self::createStub(ConfigResolverInterface::class) + self::createStub(ConfigResolverInterface::class), + new EventDispatcher() ); self::assertEquals( @@ -177,7 +238,8 @@ public function testGetSiteAccessThrowsNotFoundException(): void ); $service = new SiteAccessService( $staticSiteAccessProvider, - self::createStub(ConfigResolverInterface::class) + self::createStub(ConfigResolverInterface::class), + new EventDispatcher() ); $this->expectException(NotFoundException::class); @@ -213,10 +275,26 @@ public function testGetFirstSiteAccessesRelation(): void ); } + private function createService(?EventDispatcherInterface $eventDispatcher = null): SiteAccessService + { + $eventDispatcher ??= new EventDispatcher(); + $service = new SiteAccessService($this->provider, $this->configResolver, $eventDispatcher); + $eventDispatcher->addSubscriber($service); + + return $service; + } + + private function createSiteAccessMatchEvent(SiteAccess $siteAccess, int $requestType): PostSiteAccessMatchEvent + { + return new PostSiteAccessMatchEvent($siteAccess, new Request(), $requestType); + } + private function getSiteAccessService(): SiteAccessService { - $siteAccessService = new SiteAccessService($this->provider, $this->configResolver); - $siteAccessService->setSiteAccess($this->siteAccess); + $siteAccessService = $this->createService(); + $siteAccessService->onSiteAccessMatch( + $this->createSiteAccessMatchEvent($this->siteAccess, HttpKernelInterface::MAIN_REQUEST) + ); return $siteAccessService; } From 3d89d059ce9b14373c1444e34dc46f9476c853b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Niedzielski?= Date: Fri, 2 Oct 2026 14:30:30 +0200 Subject: [PATCH 2/2] TEMPORARY: Added dependencies.json to test against ibexa/behat#191 --- dependencies.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 dependencies.json diff --git a/dependencies.json b/dependencies.json new file mode 100644 index 0000000000..b18c7f710a --- /dev/null +++ b/dependencies.json @@ -0,0 +1,11 @@ +{ + "recipesEndpoint": "", + "packages": [ + { + "requirement": "dev-IBX-12204-siteaccessservice-scope-stack as 6.0.x-dev", + "repositoryUrl": "https://github.com/ibexa/behat.git", + "package": "ibexa/behat", + "shouldBeAddedAsVCS": false + } + ] +}