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 + } + ] +} diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index a3ed987b63..9fa19e53e7 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1608,18 +1608,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 @@ -1638,18 +1626,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 @@ -2982,12 +2958,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 @@ -3036,24 +3006,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 @@ -3408,12 +3360,6 @@ parameters: count: 1 path: src/bundle/Core/Routing/DefaultRouter.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 @@ -7362,18 +7308,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/src/bundle/Core/Command/DebugConfigResolverCommand.php b/src/bundle/Core/Command/DebugConfigResolverCommand.php index 8062d34248..ee8cd02155 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 : '')); $output->writeln('Parameter:'); $cloner = new VarCloner(); diff --git a/src/bundle/Core/DependencyInjection/Compiler/ChainRoutingPass.php b/src/bundle/Core/DependencyInjection/Compiler/ChainRoutingPass.php index 76d9769a08..adc4b29a68 100644 --- a/src/bundle/Core/DependencyInjection/Compiler/ChainRoutingPass.php +++ b/src/bundle/Core/DependencyInjection/Compiler/ChainRoutingPass.php @@ -8,8 +8,8 @@ namespace Ibexa\Bundle\Core\DependencyInjection\Compiler; use Ibexa\Core\MVC\Symfony\Routing\ChainRouter; -use Ibexa\Core\MVC\Symfony\SiteAccess; use Ibexa\Core\MVC\Symfony\SiteAccess\Router; +use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; @@ -28,7 +28,7 @@ public function process(ContainerBuilder $container): void // The default router will be given the highest priority so that it will be used by default if ($container->hasDefinition('router.default')) { $defaultRouter = $container->getDefinition('router.default'); - $defaultRouter->addMethodCall('setSiteAccess', [new Reference(SiteAccess::class)]); + $defaultRouter->setArgument('$siteAccessService', new Reference(SiteAccessServiceInterface::class)); $defaultRouter->addMethodCall('setConfigResolver', [new Reference('ibexa.config.resolver')]); $defaultRouter->addMethodCall( 'setNonSiteAccessAwareRoutes', diff --git a/src/bundle/Core/DependencyInjection/Compiler/FragmentPass.php b/src/bundle/Core/DependencyInjection/Compiler/FragmentPass.php index 2ddea81531..dbb86c6478 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,10 +50,16 @@ 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) + // Note: InlineFragmentRenderer's constructor only declares the first 2 arguments (it has + // no use for SiteAccessServiceInterface); PHP silently ignores the extra 3rd argument. if ($id === 'fragment.renderer.inline') { $decoratedDef->setClass(InlineFragmentRenderer::class); } 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 e74fe3208d..fb1366bcbe 100644 --- a/src/bundle/Core/Resources/config/routing.yml +++ b/src/bundle/Core/Resources/config/routing.yml @@ -31,7 +31,6 @@ services: abstract: true calls: - [setRequestContext, ["@router.request_context"]] - - [setSiteAccess, ['@?Ibexa\Core\MVC\Symfony\SiteAccess']] - [setSiteAccessRouter, ['@Ibexa\Core\MVC\Symfony\SiteAccess\Router']] - [setLogger, ["@?logger"]] @@ -55,6 +54,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 @@ -88,8 +88,9 @@ services: arguments: - '@ibexa.siteaccess.provider' - '@ibexa.config.resolver' - calls: - - [setSiteAccess, ['@Ibexa\Core\MVC\Symfony\SiteAccess']] + - '@event_dispatcher' + tags: + - { name: kernel.event_subscriber } Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface: alias: 'Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessService' diff --git a/src/bundle/Core/Resources/config/services.yml b/src/bundle/Core/Resources/config/services.yml index 129414be41..b55c1a37df 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 } @@ -165,7 +163,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 b8999d2460..b0f3f06a45 100644 --- a/src/bundle/Core/Routing/DefaultRouter.php +++ b/src/bundle/Core/Routing/DefaultRouter.php @@ -11,9 +11,11 @@ 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\Container\ContainerInterface; +use Psr\Log\LoggerInterface; use Symfony\Bundle\FrameworkBundle\Routing\Router; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Exception\RouteNotFoundException; @@ -22,10 +24,8 @@ /** * Extension of Symfony default router implementing RequestMatcherInterface. */ -class DefaultRouter extends Router implements SiteAccessAware +class DefaultRouter extends Router { - protected ?SiteAccess $siteAccess = null; - /** @var string[] */ protected array $nonSiteAccessAwareRoutes = []; @@ -33,14 +33,25 @@ class DefaultRouter extends Router implements SiteAccessAware protected SiteAccessRouterInterface $siteAccessRouter; - public function setConfigResolver(ConfigResolverInterface $configResolver): void - { - $this->configResolver = $configResolver; + /** + * @param array $options + */ + public function __construct( + ContainerInterface $container, + mixed $resource, + array $options = [], + ?RequestContext $context = null, + ?ContainerInterface $parameters = null, + ?LoggerInterface $logger = null, + ?string $defaultLocale = null, + private ?SiteAccessServiceInterface $siteAccessService = null, + ) { + parent::__construct($container, $resource, $options, $context, $parameters, $logger, $defaultLocale); } - public function setSiteAccess(?SiteAccess $siteAccess = null): void + public function setConfigResolver(ConfigResolverInterface $configResolver): void { - $this->siteAccess = $siteAccess; + $this->configResolver = $configResolver; } /** @@ -80,7 +91,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); @@ -92,7 +103,7 @@ public function generate(string $name, array $parameters = [], int $referenceTyp $context = $this->getContextBySimplifiedRequest($siteAccess->matcher->getRequest()); $this->setContext($context); } elseif ($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."); } 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/contracts/Test/IbexaTestKernel.php b/src/contracts/Test/IbexaTestKernel.php index cae200d744..4b473592cf 100644 --- a/src/contracts/Test/IbexaTestKernel.php +++ b/src/contracts/Test/IbexaTestKernel.php @@ -17,6 +17,8 @@ use Ibexa\Contracts\Core\Persistence\TransactionHandler; use Ibexa\Contracts\Core\Repository; use Ibexa\Contracts\Core\Test\Persistence\Fixture\YamlFixture; +use Ibexa\Core\MVC\Symfony\SiteAccess; +use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessService; use Ibexa\Tests\Integration\Core\IO\FlysystemTestAdapter; use Ibexa\Tests\Integration\Core\IO\FlysystemTestAdapterInterface; use JMS\TranslationBundle\JMSTranslationBundle; @@ -28,6 +30,7 @@ use Symfony\Bundle\SecurityBundle\SecurityBundle; use Symfony\Bundle\TwigBundle\TwigBundle; use Symfony\Component\Config\Loader\LoaderInterface; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; @@ -168,6 +171,21 @@ public function registerContainerConfiguration(LoaderInterface $loader): void }); } + public function build(ContainerBuilder $container): void + { + parent::build($container); + + // Registered as a compiler pass (rather than done directly in registerContainerConfiguration()) + // because SiteAccessService's definition doesn't exist yet at that point: bundle extensions + // (which register it) only run once the container compiles. + $container->addCompilerPass(new class() implements CompilerPassInterface { + public function process(ContainerBuilder $container): void + { + IbexaTestKernel::seedDefaultSiteAccess($container); + } + }); + } + /** * @throws \Exception */ @@ -270,6 +288,23 @@ private static function setUpTestLogger(ContainerBuilder $container): void $container->setDefinition('logger', new Definition(NullLogger::class)); } + /** + * Test cases using this kernel never dispatch a real request, so SiteAccessService would + * otherwise never have a current SiteAccess. Seed it the same way ConsoleCommandListener does + * for CLI: a fresh SiteAccess passed to changeSiteAccess(), not a shared/mutated singleton. + */ + public static function seedDefaultSiteAccess(ContainerBuilder $container): void + { + if (!$container->hasDefinition(SiteAccessService::class)) { + return; + } + + $container->getDefinition(SiteAccessService::class)->addMethodCall( + 'changeSiteAccess', + [new Definition(SiteAccess::class, ['default'])] + ); + } + /** * Creates synthetic services in container, allowing compilation of container when some services are missing. * Additionally, those services can be replaced with mock implementations at runtime, allowing integration testing. diff --git a/src/lib/Helper/ContentPreviewHelper.php b/src/lib/Helper/ContentPreviewHelper.php index ed375dd2a7..af92128409 100644 --- a/src/lib/Helper/ContentPreviewHelper.php +++ b/src/lib/Helper/ContentPreviewHelper.php @@ -9,23 +9,15 @@ 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 +28,20 @@ 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 + * @return \Ibexa\Core\MVC\Symfony\SiteAccess|null */ public function getOriginalSiteAccess() { - return $this->originalSiteAccess; + return $this->siteAccessService->getCurrent(); } /** @@ -66,23 +53,17 @@ 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 + * @return \Ibexa\Core\MVC\Symfony\SiteAccess|null */ public function restoreConfigScope() { - $event = new ScopeChangeEvent($this->originalSiteAccess); - $this->eventDispatcher->dispatch($event, MVCEvents::CONFIG_SCOPE_RESTORE); - - return $event->getSiteAccess(); + return $this->siteAccessService->restoreSiteAccess(); } /** @@ -99,7 +80,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..193f0ff08f 100644 --- a/src/lib/MVC/Symfony/Controller/Content/PreviewController.php +++ b/src/lib/MVC/Symfony/Controller/Content/PreviewController.php @@ -121,39 +121,51 @@ public function previewContentAction( } $siteAccess = $this->previewHelper->getOriginalSiteAccess(); + if ($siteAccess === null) { + throw new BadStateException('siteAccess', 'no SiteAccess currently set, cannot preview'); + } + // 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); + 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); } - - return new Response($message); - } catch (Exception $e) { - return $this->buildResponseForGenericPreviewError($location, $content, $e); + $response->headers->addCacheControlDirective('no-cache', true); + $response->setPrivate(); + + return $response; + } finally { + // Guarantee the scope is restored and the preview flag is cleared however this method + // exits (success, a caught exception's early return, or an uncaught one) — otherwise + // SiteAccessService and ConfigResolver's scope can be left disagreeing once the request + // finishes and the stack silently unwinds without a CONFIG_SCOPE_RESTORE. Safe to call + // unconditionally even when the scope was never changed above: restoreSiteAccess()'s + // floor guard never pops past the request-matched entry, so it's a no-op in that case. + $this->previewHelper->restoreConfigScope(); + $this->previewHelper->setPreviewActive(false); } - $response->headers->addCacheControlDirective('no-cache', true); - $response->setPrivate(); - - $this->previewHelper->restoreConfigScope(); - $this->previewHelper->setPreviewActive(false); - - return $response; } /** 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/SiteAccessAware.php b/src/lib/MVC/Symfony/SiteAccess/SiteAccessAware.php index d3f09d2a6f..ae8c9549bf 100644 --- a/src/lib/MVC/Symfony/SiteAccess/SiteAccessAware.php +++ b/src/lib/MVC/Symfony/SiteAccess/SiteAccessAware.php @@ -11,6 +11,8 @@ /** * Interface for SiteAccess aware services. + * + * @deprecated 6.0, to be removed in 7.0. Use \Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface::getCurrent() instead. */ interface SiteAccessAware { diff --git a/src/lib/MVC/Symfony/SiteAccess/SiteAccessService.php b/src/lib/MVC/Symfony/SiteAccess/SiteAccessService.php index 0676707544..9411580f31 100644 --- a/src/lib/MVC/Symfony/SiteAccess/SiteAccessService.php +++ b/src/lib/MVC/Symfony/SiteAccess/SiteAccessService.php @@ -11,22 +11,56 @@ 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\Event\FinishRequestEvent; +use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\HttpKernel\KernelEvents; -class SiteAccessService implements SiteAccessServiceInterface, SiteAccessAware +class SiteAccessService implements SiteAccessServiceInterface, EventSubscriberInterface { - private ?SiteAccess $siteAccess = null; + /** @var \Ibexa\Core\MVC\Symfony\SiteAccess[] */ + private array $siteAccessStack = []; public function __construct( private readonly SiteAccessProviderInterface $provider, - private readonly ConfigResolverInterface $configResolver + private readonly ConfigResolverInterface $configResolver, + private readonly EventDispatcherInterface $eventDispatcher ) { } - public function setSiteAccess(?SiteAccess $siteAccess = null): void + public static function getSubscribedEvents(): array { - $this->siteAccess = $siteAccess; + return [ + MVCEvents::SITEACCESS => 'onSiteAccessMatch', + KernelEvents::FINISH_REQUEST => 'onKernelFinishRequest', + ]; + } + + /** + * Establishes the base of the SiteAccess stack for a top-level request, or pushes an + * additional entry for a sub-request (e.g. content preview, fragments, ESI). + */ + public function onSiteAccessMatch(PostSiteAccessMatchEvent $event): void + { + if ($event->getRequestType() === HttpKernelInterface::MAIN_REQUEST) { + $this->siteAccessStack = [$event->getSiteAccess()]; + } else { + $this->siteAccessStack[] = $event->getSiteAccess(); + } + } + + /** + * Undoes the push performed by onSiteAccessMatch() for the request/sub-request that just finished. + */ + public function onKernelFinishRequest(FinishRequestEvent $event): void + { + $this->popSiteAccessStack(); } public function exists(string $name): bool @@ -50,12 +84,40 @@ public function getAll(): iterable public function getCurrent(): ?SiteAccess { - return $this->siteAccess ?? 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->eventDispatcher->dispatch(new ScopeChangeEvent($siteAccess), MVCEvents::CONFIG_SCOPE_CHANGE); + + return $siteAccess; + } + + public function restoreSiteAccess(): ?SiteAccess + { + $this->popSiteAccessStack(); + $siteAccess = $this->getCurrent(); + if ($siteAccess !== null) { + $this->eventDispatcher->dispatch(new ScopeChangeEvent($siteAccess), MVCEvents::CONFIG_SCOPE_RESTORE); + } + + return $siteAccess; } public function getSiteAccessesRelation(?SiteAccess $siteAccess = null): array { - $siteAccess = $siteAccess ?? $this->siteAccess; + $siteAccess ??= $this->getCurrent(); if ($siteAccess === null) { throw new InvalidArgumentException('siteAccess', 'no SiteAccess given and none currently set'); } @@ -87,4 +149,16 @@ 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, or the CLI SiteAccess established via + * changeSiteAccess()) must survive an unbalanced restore or an unrelated sub-request finishing. + */ + private function popSiteAccessStack(): void + { + if (count($this->siteAccessStack) > 1) { + array_pop($this->siteAccessStack); + } + } } diff --git a/src/lib/MVC/Symfony/SiteAccess/SiteAccessServiceInterface.php b/src/lib/MVC/Symfony/SiteAccess/SiteAccessServiceInterface.php index b95cfda289..feb664114e 100644 --- a/src/lib/MVC/Symfony/SiteAccess/SiteAccessServiceInterface.php +++ b/src/lib/MVC/Symfony/SiteAccess/SiteAccessServiceInterface.php @@ -37,4 +37,17 @@ public function getCurrent(): ?SiteAccess; * @throws \Ibexa\Core\Base\Exceptions\InvalidArgumentException if no SiteAccess is given and none is currently set */ public function getSiteAccessesRelation(?SiteAccess $siteAccess = null): array; + + /** + * 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. + */ + 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; } diff --git a/src/lib/Resources/settings/roles.yml b/src/lib/Resources/settings/roles.yml index 57b59122de..062081dfe2 100644 --- a/src/lib/Resources/settings/roles.yml +++ b/src/lib/Resources/settings/roles.yml @@ -71,7 +71,7 @@ services: Ibexa\Core\Limitation\SiteAccessLimitationType: class: Ibexa\Core\Limitation\SiteAccessLimitationType - arguments: ['@Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessService'] + arguments: ['@Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface'] tags: - {name: ibexa.permissions.limitation_type, alias: SiteAccess} diff --git a/tests/bundle/Core/DependencyInjection/Compiler/ChainRoutingPassTest.php b/tests/bundle/Core/DependencyInjection/Compiler/ChainRoutingPassTest.php index a97122ffd3..77c6d26888 100644 --- a/tests/bundle/Core/DependencyInjection/Compiler/ChainRoutingPassTest.php +++ b/tests/bundle/Core/DependencyInjection/Compiler/ChainRoutingPassTest.php @@ -9,8 +9,8 @@ use Ibexa\Bundle\Core\DependencyInjection\Compiler\ChainRoutingPass; use Ibexa\Core\MVC\Symfony\Routing\ChainRouter; -use Ibexa\Core\MVC\Symfony\SiteAccess; use Ibexa\Core\MVC\Symfony\SiteAccess\Router; +use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface; use Matthias\SymfonyDependencyInjectionTest\PhpUnit\AbstractCompilerPassTestCase; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; @@ -74,7 +74,7 @@ public function testAddRouterWithDefaultRouter($declaredPriority, $expectedPrior { $defaultRouter = new Definition(); $this->setDefinition('router.default', $defaultRouter); - $this->setDefinition(SiteAccess::class, new Definition()); + $this->setDefinition(SiteAccessServiceInterface::class, new Definition()); $this->setDefinition('ibexa.config.resolver', new Definition()); $this->setDefinition(Router::class, new Definition()); @@ -90,10 +90,10 @@ public function testAddRouterWithDefaultRouter($declaredPriority, $expectedPrior $this->compile(); // Assertion for default router - $this->assertContainerBuilderHasServiceDefinitionWithMethodCall( + $this->assertContainerBuilderHasServiceDefinitionWithArgument( 'router.default', - 'setSiteAccess', - [new Reference(SiteAccess::class)] + '$siteAccessService', + new Reference(SiteAccessServiceInterface::class) ); $this->assertContainerBuilderHasServiceDefinitionWithMethodCall( 'router.default', diff --git a/tests/bundle/Core/DependencyInjection/Compiler/FragmentPassTest.php b/tests/bundle/Core/DependencyInjection/Compiler/FragmentPassTest.php index 6616520948..2dc18c5a98 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,11 @@ public function testProcess() $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 +81,11 @@ public function testProcess() $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 +93,11 @@ public function testProcess() $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 511a6547db..d947d01f6c 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\TestCase; use Symfony\Component\Console\Command\Command; @@ -19,17 +20,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,12 @@ 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 = $this->createMock(Command::class); @@ -71,8 +69,8 @@ public function testInvalidSiteAccessDev() $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 +82,8 @@ public function testInvalidSiteAccessProd() $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 +92,22 @@ public function testInvalidSiteAccessProd() public function testValidSiteAccess() { - $this->dispatcher->expects(self::once()) - ->method('dispatch'); + $this->siteAccessService->expects(self::once()) + ->method('changeSiteAccess') + ->with(self::equalTo(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() { - $this->dispatcher->expects(self::once()) - ->method('dispatch'); + $this->siteAccessService->expects(self::once()) + ->method('changeSiteAccess') + ->with(self::equalTo(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 eda709b056..907cd26546 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\MockObject\MockObject; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -25,10 +26,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 @@ -39,8 +43,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'); } @@ -56,13 +61,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'); } @@ -137,11 +143,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 cd3a45a3a3..58410d3962 100644 --- a/tests/bundle/Core/Routing/DefaultRouterTest.php +++ b/tests/bundle/Core/Routing/DefaultRouterTest.php @@ -12,6 +12,7 @@ use Ibexa\Contracts\Core\SiteAccess\ConfigResolverInterface; use Ibexa\Core\MVC\Symfony\Routing\SimplifiedRequest; use Ibexa\Core\MVC\Symfony\SiteAccess; +use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface; use PHPUnit\Framework\TestCase; use ReflectionObject; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -52,12 +53,12 @@ protected function getRouterClass(): string * * @return \PHPUnit\Framework\MockObject\MockObject&\Ibexa\Bundle\Core\Routing\DefaultRouter */ - protected function generateRouter(array $mockedMethods = []) + protected function generateRouter(array $mockedMethods = [], ?SiteAccessServiceInterface $siteAccessService = null) { /** @var \PHPUnit\Framework\MockObject\MockObject&\Ibexa\Bundle\Core\Routing\DefaultRouter $router */ $router = $this ->getMockBuilder($this->getRouterClass()) - ->setConstructorArgs([$this->container, 'foo', [], $this->requestContext]) + ->setConstructorArgs([$this->container, 'foo', [], $this->requestContext, null, null, null, $siteAccessService]) ->setMethods(array_merge($mockedMethods)) ->getMock(); $router->setConfigResolver($this->configResolver); @@ -170,8 +171,10 @@ public function testGenerateWithSiteAccess($urlGenerated, $relevantUri, $expecte ->with($routeName) ->willReturn($urlGenerated); + $siteAccessService = $this->createMock(SiteAccessServiceInterface::class); + /** @var \Ibexa\Bundle\Core\Routing\DefaultRouter&\PHPUnit\Framework\MockObject\MockObject $router */ - $router = $this->generateRouter(['getGenerator']); + $router = $this->generateRouter(['getGenerator'], $siteAccessService); $router ->expects(self::any()) ->method('getGenerator') @@ -198,7 +201,7 @@ public function testGenerateWithSiteAccess($urlGenerated, $relevantUri, $expecte } $sa = new SiteAccess($saName, 'test', $matcher); - $router->setSiteAccess($sa); + $siteAccessService->method('getCurrent')->willReturn($sa); $requestContext = new RequestContext(); $urlComponents = parse_url($urlGenerated); @@ -272,9 +275,14 @@ public function testGenerateReverseSiteAccessMatch() ->method('setContext') ->with($this->requestContext); - $router = new DefaultRouter($this->container, 'foo', [], $this->requestContext); + $router = new DefaultRouter( + $this->container, + 'foo', + [], + $this->requestContext, + siteAccessService: $this->getSiteAccessService(new SiteAccess('test', 'test', $this->createMock(Matcher::class))) + ); $router->setConfigResolver($this->configResolver); - $router->setSiteAccess(new SiteAccess('test', 'test', $this->createMock(Matcher::class))); $router->setSiteAccessRouter($siteAccessRouter); $refRouter = new ReflectionObject($router); $refGenerator = $refRouter->getProperty('generator'); @@ -342,4 +350,15 @@ private function getExpectedRequestContext($uri) return $requestContext; } + + /** + * @return \Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface|\PHPUnit\Framework\MockObject\MockObject + */ + private function getSiteAccessService(SiteAccess $siteAccess) + { + $siteAccessService = $this->createMock(SiteAccessServiceInterface::class); + $siteAccessService->method('getCurrent')->willReturn($siteAccess); + + return $siteAccessService; + } } diff --git a/tests/bundle/Core/SiteAccess/Config/IOConfigResolverTest.php b/tests/bundle/Core/SiteAccess/Config/IOConfigResolverTest.php index cf6b8a1586..b8d5416121 100644 --- a/tests/bundle/Core/SiteAccess/Config/IOConfigResolverTest.php +++ b/tests/bundle/Core/SiteAccess/Config/IOConfigResolverTest.php @@ -12,7 +12,7 @@ use Ibexa\Bundle\Core\SiteAccess\Config\IOConfigResolver; use Ibexa\Contracts\Core\SiteAccess\ConfigResolverInterface; use Ibexa\Core\MVC\Symfony\SiteAccess; -use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessService; +use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface; use PHPUnit\Framework\TestCase; /** @@ -25,14 +25,14 @@ class IOConfigResolverTest extends TestCase /** @var \Ibexa\Contracts\Core\SiteAccess\ConfigResolverInterface|\PHPUnit\Framework\MockObject\MockObject */ private $configResolver; - /** @var \Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessService|\PHPUnit\Framework\MockObject\MockObject */ + /** @var \Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface|\PHPUnit\Framework\MockObject\MockObject */ private $siteAccessService; protected function setUp(): void { parent::setUp(); $this->configResolver = $this->createMock(ConfigResolverInterface::class); - $this->siteAccessService = $this->createMock(SiteAccessService::class); + $this->siteAccessService = $this->createMock(SiteAccessServiceInterface::class); } public function testGetUrlPrefix(): void diff --git a/tests/bundle/IO/DependencyInjection/IbexaIOExtensionTest.php b/tests/bundle/IO/DependencyInjection/IbexaIOExtensionTest.php index ea3f5356b2..2f695f2af2 100644 --- a/tests/bundle/IO/DependencyInjection/IbexaIOExtensionTest.php +++ b/tests/bundle/IO/DependencyInjection/IbexaIOExtensionTest.php @@ -14,8 +14,14 @@ use Ibexa\Bundle\IO\DependencyInjection\ConfigurationFactory; use Ibexa\Bundle\IO\DependencyInjection\IbexaIOExtension; use Ibexa\Core\IO\UrlDecorator\AbsolutePrefix; +use Ibexa\Core\MVC\Symfony\Event\PostSiteAccessMatchEvent; +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\EventDispatcher\EventDispatcher; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\Yaml\Yaml; class IbexaIOExtensionTest extends AbstractExtensionTestCase @@ -86,6 +92,19 @@ public function testUrlPrefixConfigurationIsUsedToDecorateUrl(): void ); $this->buildMinimalContainerForUrlPrefixTest(); + // Simulate SiteAccess matching having happened for the "site" siteaccess, since + // ComplexConfigProcessor now resolves the current SiteAccess via SiteAccessService + // rather than reading a pre-set property. + // + // Fetched by concrete class (not SiteAccessServiceInterface): onSiteAccessMatch() is a + // subscriber-only method not part of the interface, and SetAllServicesPublicPass only + // exposes real service definitions, not the interface alias. + $siteAccessService = $this->container->get(SiteAccessService::class); + self::assertInstanceOf(SiteAccessService::class, $siteAccessService); + $siteAccessService->onSiteAccessMatch( + new PostSiteAccessMatchEvent(new SiteAccess('site'), new Request(), HttpKernelInterface::MAIN_REQUEST) + ); + $decorator = $this->container->get(AbsolutePrefix::class); self::assertEquals( @@ -101,6 +120,9 @@ 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'); + // needed by Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessService, pulled in transitively + // via the AliasGeneratorDecorator service definition + $this->container->register('event_dispatcher', 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..e293fe2755 100644 --- a/tests/integration/Core/Resources/settings/common.yml +++ b/tests/integration/Core/Resources/settings/common.yml @@ -106,13 +106,21 @@ services: Ibexa\Core\MVC\Symfony\SiteAccess: arguments: ['default', !php/const Ibexa\Core\MVC\Symfony\SiteAccess::MATCHING_TYPE_UNINITIALIZED] + # Dedicated SiteAccess used to seed SiteAccessService below: integration tests never dispatch a + # real request, so this establishes a default current SiteAccess the same way ConsoleCommandListener + # does for CLI (a fresh SiteAccess passed to changeSiteAccess(), no shared/mutated singleton). + ibexa.test.default_site_access: + class: Ibexa\Core\MVC\Symfony\SiteAccess + arguments: ['default'] + Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessService: class: Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessService arguments: - '@ibexa.siteaccess.provider' - '@ibexa.config.resolver' + - '@Symfony\Component\EventDispatcher\EventDispatcher' calls: - - [setSiteAccess, ['@Ibexa\Core\MVC\Symfony\SiteAccess']] + - [changeSiteAccess, ['@ibexa.test.default_site_access']] 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 59afe15726..e09903d446 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; + /** @var \Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface|\PHPUnit\Framework\MockObject\MockObject */ + 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() @@ -47,14 +45,12 @@ public function testChangeConfigScope() ->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,13 +60,11 @@ public function testChangeConfigScope() public function testRestoreConfigScope() { $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() @@ -80,7 +74,9 @@ public function testRestoreConfigScope() public function testPreviewActive() { $originalSiteAccess = new SiteAccess('foo', 'bar'); - $this->previewHelper->setSiteAccess($originalSiteAccess); + $this->siteAccessService + ->method('getCurrent') + ->willReturn($originalSiteAccess); self::assertFalse($this->previewHelper->isPreviewActive()); $this->previewHelper->setPreviewActive(true); @@ -88,7 +84,7 @@ public function testPreviewActive() $this->previewHelper->setPreviewActive(false); self::assertFalse($this->previewHelper->isPreviewActive()); - self::assertNotSame($originalSiteAccess, $this->previewHelper->getOriginalSiteAccess()); + self::assertSame($originalSiteAccess, $this->previewHelper->getOriginalSiteAccess()); } public function testPreviewedContent() diff --git a/tests/lib/MVC/Symfony/Routing/GeneratorTest.php b/tests/lib/MVC/Symfony/Routing/GeneratorTest.php index 51fc9a6c9e..dcf4248323 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\TestCase; @@ -28,12 +29,16 @@ 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->getMockForAbstractClass(Generator::class, [$this->siteAccessService]); $this->generator->setSiteAccessRouter($this->siteAccessRouter); $this->generator->setLogger($this->logger); } @@ -59,7 +64,7 @@ public function generateProvider() public function testSimpleGenerate($urlResource, array $parameters, $referenceType) { $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); @@ -92,7 +97,7 @@ public function testSimpleGenerate($urlResource, array $parameters, $referenceTy public function testGenerateWithSiteAccessNoReverseMatch($urlResource, array $parameters, $referenceType) { $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); diff --git a/tests/lib/MVC/Symfony/Routing/UrlAliasGeneratorTest.php b/tests/lib/MVC/Symfony/Routing/UrlAliasGeneratorTest.php index 845f3bdcca..463e9b7994 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; @@ -51,6 +52,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(); @@ -58,6 +62,7 @@ protected function setUp(): void $this->logger = $this->createMock(LoggerInterface::class); $this->siteAccessRouter = $this->createMock(SiteAccessRouterInterface::class); $this->configResolver = $this->createMock(ConfigResolverInterface::class); + $this->siteAccessService = $this->createMock(SiteAccessServiceInterface::class); $repositoryClass = Repository::class; $this->repository = $repository = $this ->getMockBuilder($repositoryClass) @@ -94,6 +99,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', $this->createMock(SiteAccess\URILexer::class))); + $this->siteAccessService->method('getCurrent')->willReturn(new SiteAccess('test', 'fake', $this->createMock(SiteAccess\URILexer::class))); self::assertSame($expected, $this->urlAliasGenerator->doGenerate($location, $parameters)); } @@ -274,7 +280,7 @@ static function ($location) use ($treeRootUrlAlias) { ) ); - $this->urlAliasGenerator->setSiteAccess(new SiteAccess('test', 'fake', $this->createMock(SiteAccess\URILexer::class))); + $this->siteAccessService->method('getCurrent')->willReturn(new SiteAccess('test', 'fake', $this->createMock(SiteAccess\URILexer::class))); self::assertSame($expected, $this->urlAliasGenerator->doGenerate($location, $parameters)); } @@ -385,7 +391,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 52ebd9be49..aaffaee777 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; @@ -77,6 +78,7 @@ protected function setUp(): void $repository, $this->createMock(RouterInterface::class), $this->createMock(ConfigResolverInterface::class), + $this->createMock(SiteAccessServiceInterface::class), ] ) ->getMock(); diff --git a/tests/lib/MVC/Symfony/Security/HttpUtilsTest.php b/tests/lib/MVC/Symfony/Security/HttpUtilsTest.php index d196b24bd0..efb5d8fa3e 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\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; @@ -21,8 +22,7 @@ class HttpUtilsTest extends TestCase public function testGenerateUriStandard($uri, $isUriRouteName, $expected) { $urlGenerator = $this->createMock(UrlGeneratorInterface::class); - $httpUtils = new HttpUtils($urlGenerator); - $httpUtils->setSiteAccess(new SiteAccess('test')); + $httpUtils = new HttpUtils($urlGenerator, siteAccessService: $this->getSiteAccessService(new SiteAccess('test'))); $request = Request::create('http://ezpublish.dev/'); $request->attributes->set('siteaccess', new SiteAccess('test')); $requestAttributes = ['foo' => 'bar', 'some' => 'thing']; @@ -67,8 +67,7 @@ public function testGenerateUri($uri, $isUriRouteName, $siteAccessUri, $expected } $urlGenerator = $this->createMock(UrlGeneratorInterface::class); - $httpUtils = new HttpUtils($urlGenerator); - $httpUtils->setSiteAccess($siteAccess); + $httpUtils = new HttpUtils($urlGenerator, siteAccessService: $this->getSiteAccessService($siteAccess)); $request = Request::create('http://ezpublish.dev/'); $request->attributes->set('siteaccess', $siteAccess); $requestAttributes = ['foo' => 'bar', 'some' => 'thing']; @@ -99,8 +98,7 @@ public function generateUriProvider() public function testCheckRequestPathStandard() { - $httpUtils = new HttpUtils(); - $httpUtils->setSiteAccess(new SiteAccess('test')); + $httpUtils = new HttpUtils(siteAccessService: $this->getSiteAccessService(new SiteAccess('test'))); $request = Request::create('http://ezpublish.dev/foo/bar'); self::assertTrue($httpUtils->checkRequestPath($request, '/foo/bar')); } @@ -121,8 +119,7 @@ public function testCheckRequestPath($path, $siteAccessUri, $requestUri, $expect $siteAccess->matcher = $matcher; } - $httpUtils = new HttpUtils(); - $httpUtils->setSiteAccess($siteAccess); + $httpUtils = new HttpUtils(siteAccessService: $this->getSiteAccessService($siteAccess)); $request = Request::create($requestUri); self::assertSame($expected, $httpUtils->checkRequestPath($request, $path)); } @@ -138,4 +135,15 @@ public function checkRequestPathProvider() ['/foo/bar', '/blabla', 'http://ezpublish.dev/blabla/foo/bar', true], ]; } + + /** + * @return \Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface|\PHPUnit\Framework\MockObject\MockObject + */ + private function getSiteAccessService(SiteAccess $siteAccess) + { + $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 7ef230d7ee..821a6fae15 100644 --- a/tests/lib/MVC/Symfony/SiteAccess/SiteAccessServiceTest.php +++ b/tests/lib/MVC/Symfony/SiteAccess/SiteAccessServiceTest.php @@ -11,11 +11,18 @@ 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 PHPUnit\Framework\TestCase; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Event\FinishRequestEvent; +use Symfony\Component\HttpKernel\HttpKernelInterface; class SiteAccessServiceTest extends TestCase { @@ -29,6 +36,9 @@ class SiteAccessServiceTest extends TestCase /** @var \Ibexa\Contracts\Core\SiteAccess\ConfigResolverInterface|\PHPUnit\Framework\MockObject\MockObject */ private $configResolver; + /** @var \Symfony\Component\EventDispatcher\EventDispatcherInterface|\PHPUnit\Framework\MockObject\MockObject */ + private $eventDispatcher; + /** @var \Ibexa\Core\MVC\Symfony\SiteAccess */ private $siteAccess; @@ -43,28 +53,44 @@ protected function setUp(): void parent::setUp(); $this->provider = $this->createMock(SiteAccessProviderInterface::class); $this->configResolver = $this->createMock(ConfigResolverInterface::class); + $this->eventDispatcher = $this->createMock(EventDispatcherInterface::class); $this->siteAccess = new SiteAccess('current'); $this->availableSiteAccesses = $this->getAvailableSitAccesses(['current', 'first_sa', 'second_sa', 'default']); $this->configResolverParameters = $this->getConfigResolverParameters(); } - public function testGetCurrentSiteAccess(): void + public function testGetCurrentSiteAccessIsNullWhenStackIsEmpty(): void { - $service = new SiteAccessService( - $this->createMock(SiteAccessProviderInterface::class), - $this->createMock(ConfigResolverInterface::class) - ); + $service = $this->createSiteAccessService(); self::assertNull($service->getCurrent()); + } - $siteAccess = new SiteAccess('default'); - $service->setSiteAccess($siteAccess); - self::assertSame($siteAccess, $service->getCurrent()); + /** + * A SiteAccess with an "uninitialized" matcher (see SiteAccess::MATCHING_TYPE_UNINITIALIZED) + * is not a real current SiteAccess — getCurrent() must surface it as null. + */ + public function testGetCurrentSiteAccessIsNullWhenTopOfStackIsUninitialized(): void + { + $service = $this->createSiteAccessService(); + + $service->changeSiteAccess(new SiteAccess('default', SiteAccess::MATCHING_TYPE_UNINITIALIZED)); - $service->setSiteAccess(null); self::assertNull($service->getCurrent()); } + public function testGetCurrentSiteAccessAfterMainRequestMatch(): void + { + $service = $this->createSiteAccessService(); + + $siteAccess = new SiteAccess('default'); + $service->onSiteAccessMatch( + new PostSiteAccessMatchEvent($siteAccess, new Request(), HttpKernelInterface::MAIN_REQUEST) + ); + + self::assertSame($siteAccess, $service->getCurrent()); + } + public function testGetSiteAccess(): void { $staticSiteAccessProvider = new StaticSiteAccessProvider( @@ -73,7 +99,8 @@ public function testGetSiteAccess(): void ); $service = new SiteAccessService( $staticSiteAccessProvider, - $this->createMock(ConfigResolverInterface::class) + $this->createMock(ConfigResolverInterface::class), + $this->eventDispatcher ); self::assertEquals( @@ -90,7 +117,8 @@ public function testGetSiteAccessThrowsNotFoundException(): void ); $service = new SiteAccessService( $staticSiteAccessProvider, - $this->createMock(ConfigResolverInterface::class) + $this->createMock(ConfigResolverInterface::class), + $this->eventDispatcher ); $this->expectException(NotFoundException::class); @@ -126,10 +154,159 @@ public function testGetFirstSiteAccessesRelation(): void ); } + /** + * changeSiteAccess()/restoreSiteAccess() must behave as a real LIFO stack, and each call + * must dispatch a ScopeChangeEvent carrying the relevant SiteAccess under the right event name. + */ + public function testChangeSiteAccessAndRestoreSiteAccessBehaveAsLifoStack(): void + { + $service = $this->createSiteAccessService(); + + $original = new SiteAccess('original'); + $service->onSiteAccessMatch( + new PostSiteAccessMatchEvent($original, new Request(), HttpKernelInterface::MAIN_REQUEST) + ); + + $first = new SiteAccess('first_change'); + $second = new SiteAccess('second_change'); + + $this->eventDispatcher + ->expects(self::exactly(4)) + ->method('dispatch') + ->withConsecutive( + [self::equalTo(new ScopeChangeEvent($first)), MVCEvents::CONFIG_SCOPE_CHANGE], + [self::equalTo(new ScopeChangeEvent($second)), MVCEvents::CONFIG_SCOPE_CHANGE], + [self::equalTo(new ScopeChangeEvent($first)), MVCEvents::CONFIG_SCOPE_RESTORE], + [self::equalTo(new ScopeChangeEvent($original)), MVCEvents::CONFIG_SCOPE_RESTORE] + ); + + self::assertSame($first, $service->changeSiteAccess($first)); + self::assertSame($first, $service->getCurrent()); + + self::assertSame($second, $service->changeSiteAccess($second)); + self::assertSame($second, $service->getCurrent()); + + self::assertSame($first, $service->restoreSiteAccess()); + self::assertSame($first, $service->getCurrent()); + + self::assertSame($original, $service->restoreSiteAccess()); + self::assertSame($original, $service->getCurrent()); + } + + public function testOnSiteAccessMatchWithMainRequestResetsTheStack(): void + { + $service = $this->createSiteAccessService(); + + $service->onSiteAccessMatch( + new PostSiteAccessMatchEvent(new SiteAccess('first'), new Request(), HttpKernelInterface::MAIN_REQUEST) + ); + $service->onSiteAccessMatch( + new PostSiteAccessMatchEvent(new SiteAccess('sub'), new Request(), HttpKernelInterface::SUB_REQUEST) + ); + + $second = new SiteAccess('second'); + $service->onSiteAccessMatch( + new PostSiteAccessMatchEvent($second, new Request(), HttpKernelInterface::MAIN_REQUEST) + ); + + self::assertSame($second, $service->getCurrent()); + + // The stack was fully reset by the second MAIN_REQUEST match: an unbalanced + // finish-request must not be able to reveal any of the previous entries. + $service->onKernelFinishRequest($this->createFinishRequestEvent()); + self::assertSame($second, $service->getCurrent()); + } + + public function testOnSiteAccessMatchWithSubRequestPushesWithoutResetting(): void + { + $service = $this->createSiteAccessService(); + + $main = new SiteAccess('main'); + $service->onSiteAccessMatch( + new PostSiteAccessMatchEvent($main, new Request(), HttpKernelInterface::MAIN_REQUEST) + ); + + $sub = new SiteAccess('sub'); + $service->onSiteAccessMatch( + new PostSiteAccessMatchEvent($sub, new Request(), HttpKernelInterface::SUB_REQUEST) + ); + + self::assertSame($sub, $service->getCurrent()); + + $service->onKernelFinishRequest($this->createFinishRequestEvent()); + self::assertSame($main, $service->getCurrent()); + } + + public function testOnKernelFinishRequestNeverPopsBelowOneRemainingEntry(): void + { + $service = $this->createSiteAccessService(); + + $original = new SiteAccess('original'); + $service->onSiteAccessMatch( + new PostSiteAccessMatchEvent($original, new Request(), HttpKernelInterface::MAIN_REQUEST) + ); + + // Simulate an extra, unbalanced finish-request event. + $service->onKernelFinishRequest($this->createFinishRequestEvent()); + + self::assertSame($original, $service->getCurrent()); + } + + /** + * Sub-request nesting scenario: a preview scope change followed by a sub-request (e.g. a + * fragment/ESI render) must unwind back to the preview scope once the sub-request finishes, + * and restoreSiteAccess() must then bring back the original SiteAccess. + */ + public function testSubRequestNestingScenario(): void + { + $service = $this->createSiteAccessService(); + + $original = new SiteAccess('original'); + $service->onSiteAccessMatch( + new PostSiteAccessMatchEvent($original, new Request(), HttpKernelInterface::MAIN_REQUEST) + ); + + $preview = new SiteAccess('preview'); + $service->changeSiteAccess($preview); + self::assertSame($preview, $service->getCurrent()); + + $subSiteAccess = new SiteAccess('sub'); + $service->onSiteAccessMatch( + new PostSiteAccessMatchEvent($subSiteAccess, new Request(), HttpKernelInterface::SUB_REQUEST) + ); + self::assertSame($subSiteAccess, $service->getCurrent()); + + $service->onKernelFinishRequest($this->createFinishRequestEvent()); + self::assertSame($preview, $service->getCurrent()); + + self::assertSame($original, $service->restoreSiteAccess()); + self::assertSame($original, $service->getCurrent()); + } + + private function createFinishRequestEvent(): FinishRequestEvent + { + return new FinishRequestEvent( + $this->createMock(HttpKernelInterface::class), + new Request(), + HttpKernelInterface::MAIN_REQUEST + ); + } + + private function createSiteAccessService(): SiteAccessService + { + return new SiteAccessService( + $this->provider, + $this->configResolver, + $this->eventDispatcher + ); + } + private function getSiteAccessService(): SiteAccessService { - $siteAccessService = new SiteAccessService($this->provider, $this->configResolver); - $siteAccessService->setSiteAccess($this->siteAccess); + $siteAccessService = $this->createSiteAccessService(); + $siteAccessService->onSiteAccessMatch( + new PostSiteAccessMatchEvent($this->siteAccess, new Request(), HttpKernelInterface::MAIN_REQUEST) + ); return $siteAccessService; }