From 49aa8bbaada53ec737548f8d3bd89c9b2b51d1c8 Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Tue, 15 Oct 2024 18:23:44 +0200 Subject: [PATCH 001/214] FEATURE: Content Repository Privileges Related: #3732 --- ...trineDbalContentGraphProjectionFactory.php | 2 +- ...actory.php => FakeAuthProviderFactory.php} | 12 +- .../Settings.ContentRepositoryRegistry.yaml | 4 +- .../Classes/CommandHandlingDependencies.php | 8 +- .../Classes/ContentRepository.php | 19 ++- .../Classes/ContentRepositoryReadModel.php | 5 +- .../Factory/ContentRepositoryFactory.php | 9 +- .../Factory/ProjectionFactoryDependencies.php | 2 + .../Auth/AuthProviderInterface.php | 20 +++ .../Classes/SharedModel/Auth/Privilege.php | 38 +++++ .../SharedModel/Auth/StaticAuthProvider.php | 36 +++++ .../SharedModel/{User => Auth}/UserId.php | 2 +- .../Auth/WorkspacePrivilegeType.php | 23 +++ .../SharedModel/User/StaticUserIdProvider.php | 23 --- .../User/UserIdProviderInterface.php | 13 -- .../Bootstrap/CRTestSuiteRuntimeVariables.php | 6 +- .../Bootstrap/Helpers/FakeAuthProvider.php | 37 +++++ .../Bootstrap/Helpers/FakeUserIdProvider.php | 23 --- .../Classes/ContentRepositoryRegistry.php | 18 +-- .../AuthProviderFactoryInterface.php} | 10 +- .../StaticAuthProviderFactory.php | 20 +++ .../StaticUserIdProviderFactory.php | 20 --- .../Configuration/Settings.yaml | 4 +- .../ContentRepositoryAuthProvider.php | 147 ++++++++++++++++++ .../ContentRepositoryAuthProviderFactory.php | 35 +++++ .../Domain/Service/WorkspaceService.php | 9 +- .../Controller/AbstractServiceController.php | 10 -- .../Classes/UserIdProvider/UserIdProvider.php | 29 ---- .../UserIdProvider/UserIdProviderFactory.php | 34 ---- .../Settings.ContentRepositoryRegistry.yaml | 4 +- 30 files changed, 426 insertions(+), 196 deletions(-) rename Neos.ContentRepository.BehavioralTests/Classes/TestSuite/Behavior/{FakeUserIdProviderFactory.php => FakeAuthProviderFactory.php} (52%) create mode 100644 Neos.ContentRepository.Core/Classes/SharedModel/Auth/AuthProviderInterface.php create mode 100644 Neos.ContentRepository.Core/Classes/SharedModel/Auth/Privilege.php create mode 100644 Neos.ContentRepository.Core/Classes/SharedModel/Auth/StaticAuthProvider.php rename Neos.ContentRepository.Core/Classes/SharedModel/{User => Auth}/UserId.php (96%) create mode 100644 Neos.ContentRepository.Core/Classes/SharedModel/Auth/WorkspacePrivilegeType.php delete mode 100644 Neos.ContentRepository.Core/Classes/SharedModel/User/StaticUserIdProvider.php delete mode 100644 Neos.ContentRepository.Core/Classes/SharedModel/User/UserIdProviderInterface.php create mode 100644 Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Helpers/FakeAuthProvider.php delete mode 100644 Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Helpers/FakeUserIdProvider.php rename Neos.ContentRepositoryRegistry/Classes/Factory/{UserIdProvider/UserIdProviderFactoryInterface.php => AuthProvider/AuthProviderFactoryInterface.php} (52%) create mode 100644 Neos.ContentRepositoryRegistry/Classes/Factory/AuthProvider/StaticAuthProviderFactory.php delete mode 100644 Neos.ContentRepositoryRegistry/Classes/Factory/UserIdProvider/StaticUserIdProviderFactory.php create mode 100644 Neos.Neos/Classes/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php create mode 100644 Neos.Neos/Classes/ContentRepositoryAuthProvider/ContentRepositoryAuthProviderFactory.php delete mode 100644 Neos.Neos/Classes/UserIdProvider/UserIdProvider.php delete mode 100644 Neos.Neos/Classes/UserIdProvider/UserIdProviderFactory.php diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjectionFactory.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjectionFactory.php index 1b750c114a4..6a0644842a9 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjectionFactory.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjectionFactory.php @@ -58,7 +58,7 @@ public function build( ), $tableNames, $dimensionSpacePointsRepository, - new ContentRepositoryReadModel($contentRepositoryReadModelAdapter) + new ContentRepositoryReadModel($contentRepositoryReadModelAdapter), ); } } diff --git a/Neos.ContentRepository.BehavioralTests/Classes/TestSuite/Behavior/FakeUserIdProviderFactory.php b/Neos.ContentRepository.BehavioralTests/Classes/TestSuite/Behavior/FakeAuthProviderFactory.php similarity index 52% rename from Neos.ContentRepository.BehavioralTests/Classes/TestSuite/Behavior/FakeUserIdProviderFactory.php rename to Neos.ContentRepository.BehavioralTests/Classes/TestSuite/Behavior/FakeAuthProviderFactory.php index 6a0a5c7a408..df59e7f80c8 100644 --- a/Neos.ContentRepository.BehavioralTests/Classes/TestSuite/Behavior/FakeUserIdProviderFactory.php +++ b/Neos.ContentRepository.BehavioralTests/Classes/TestSuite/Behavior/FakeAuthProviderFactory.php @@ -5,17 +5,17 @@ namespace Neos\ContentRepository\BehavioralTests\TestSuite\Behavior; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; -use Neos\ContentRepository\Core\SharedModel\User\UserIdProviderInterface; -use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Helpers\FakeUserIdProvider; -use Neos\ContentRepositoryRegistry\Factory\UserIdProvider\UserIdProviderFactoryInterface; +use Neos\ContentRepository\Core\SharedModel\Auth\AuthProviderInterface; +use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Helpers\FakeAuthProvider; +use Neos\ContentRepositoryRegistry\Factory\AuthProvider\AuthProviderFactoryInterface; -final class FakeUserIdProviderFactory implements UserIdProviderFactoryInterface +final class FakeAuthProviderFactory implements AuthProviderFactoryInterface { /** * @param array $options */ - public function build(ContentRepositoryId $contentRepositoryId, array $options): UserIdProviderInterface + public function build(ContentRepositoryId $contentRepositoryId, array $options): AuthProviderInterface { - return new FakeUserIdProvider(); + return new FakeAuthProvider(); } } diff --git a/Neos.ContentRepository.BehavioralTests/Configuration/Testing/Behat/Settings.ContentRepositoryRegistry.yaml b/Neos.ContentRepository.BehavioralTests/Configuration/Testing/Behat/Settings.ContentRepositoryRegistry.yaml index 53eafcc4977..74387df65ff 100644 --- a/Neos.ContentRepository.BehavioralTests/Configuration/Testing/Behat/Settings.ContentRepositoryRegistry.yaml +++ b/Neos.ContentRepository.BehavioralTests/Configuration/Testing/Behat/Settings.ContentRepositoryRegistry.yaml @@ -2,8 +2,8 @@ Neos: ContentRepositoryRegistry: presets: default: - userIdProvider: - factoryObjectName: 'Neos\ContentRepository\BehavioralTests\TestSuite\Behavior\FakeUserIdProviderFactory' + authProvider: + factoryObjectName: 'Neos\ContentRepository\BehavioralTests\TestSuite\Behavior\FakeAuthProviderFactory' clock: factoryObjectName: 'Neos\ContentRepository\BehavioralTests\TestSuite\Behavior\FakeClockFactory' nodeTypeManager: diff --git a/Neos.ContentRepository.Core/Classes/CommandHandlingDependencies.php b/Neos.ContentRepository.Core/Classes/CommandHandlingDependencies.php index 1c778159fcd..12d7b67eabe 100644 --- a/Neos.ContentRepository.Core/Classes/CommandHandlingDependencies.php +++ b/Neos.ContentRepository.Core/Classes/CommandHandlingDependencies.php @@ -38,8 +38,10 @@ final class CommandHandlingDependencies */ private array $overriddenContentGraphInstances = []; - public function __construct(private readonly ContentRepository $contentRepository) - { + public function __construct( + private readonly ContentRepository $contentRepository, + private readonly ContentRepositoryReadModel $contentRepositoryReadModel, + ) { } public function handle(CommandInterface $command): CommandResult @@ -84,7 +86,7 @@ public function getContentGraph(WorkspaceName $workspaceName): ContentGraphInter return $this->overriddenContentGraphInstances[$workspaceName->value]; } - return $this->contentRepository->getContentGraph($workspaceName); + return $this->contentRepositoryReadModel->getContentGraphByWorkspaceName($workspaceName); } /** diff --git a/Neos.ContentRepository.Core/Classes/ContentRepository.php b/Neos.ContentRepository.Core/Classes/ContentRepository.php index 54e81fb63a3..f2e50762a18 100644 --- a/Neos.ContentRepository.Core/Classes/ContentRepository.php +++ b/Neos.ContentRepository.Core/Classes/ContentRepository.php @@ -35,10 +35,11 @@ use Neos\ContentRepository\Core\Projection\ProjectionStateInterface; use Neos\ContentRepository\Core\Projection\ProjectionStatuses; use Neos\ContentRepository\Core\Projection\WithMarkStaleInterface; +use Neos\ContentRepository\Core\SharedModel\Auth\WorkspacePrivilegeType; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryStatus; use Neos\ContentRepository\Core\SharedModel\Exception\WorkspaceDoesNotExist; -use Neos\ContentRepository\Core\SharedModel\User\UserIdProviderInterface; +use Neos\ContentRepository\Core\SharedModel\Auth\AuthProviderInterface; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStream; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreams; @@ -85,10 +86,10 @@ public function __construct( private readonly NodeTypeManager $nodeTypeManager, private readonly InterDimensionalVariationGraph $variationGraph, private readonly ContentDimensionSourceInterface $contentDimensionSource, - private readonly UserIdProviderInterface $userIdProvider, + private readonly AuthProviderInterface $authProvider, private readonly ClockInterface $clock, ) { - $this->commandHandlingDependencies = new CommandHandlingDependencies($this); + $this->commandHandlingDependencies = new CommandHandlingDependencies($this, $this->getContentRepositoryReadModel()); } /** @@ -99,12 +100,16 @@ public function __construct( */ public function handle(CommandInterface $command): CommandResult { + $privilege = $this->authProvider->getCommandPrivilege($command); + if (!$privilege->granted) { + throw new \RuntimeException(sprintf('Command "%s" was denied: %s', $command::class, $privilege->message), 1729086686); + } // the commands only calculate which events they want to have published, but do not do the // publishing themselves $eventsToPublish = $this->commandBus->handle($command, $this->commandHandlingDependencies); // TODO meaningful exception message - $initiatingUserId = $this->userIdProvider->getUserId(); + $initiatingUserId = $this->authProvider->getUserId(); $initiatingTimestamp = $this->clock->now()->format(\DateTimeInterface::ATOM); // Add "initiatingUserId" and "initiatingTimestamp" metadata to all events. @@ -269,6 +274,12 @@ public function findContentStreams(): ContentStreams */ public function getContentGraph(WorkspaceName $workspaceName): ContentGraphInterface { + $privilege = $this->authProvider->getWorkspacePrivilege($workspaceName, WorkspacePrivilegeType::READ_NODES); + if (!$privilege->granted) { + throw new \RuntimeException(sprintf('Read access denied for workspace "%s": %s', $workspaceName->value, $privilege->message ?? ''), 1729014760); + // TODO more specific exception + //throw WorkspaceDoesNotExist::butWasSupposedTo($workspaceName); + } return $this->getContentRepositoryReadModel()->getContentGraphByWorkspaceName($workspaceName); } diff --git a/Neos.ContentRepository.Core/Classes/ContentRepositoryReadModel.php b/Neos.ContentRepository.Core/Classes/ContentRepositoryReadModel.php index 95584a07f76..a9fd7d0eeef 100644 --- a/Neos.ContentRepository.Core/Classes/ContentRepositoryReadModel.php +++ b/Neos.ContentRepository.Core/Classes/ContentRepositoryReadModel.php @@ -16,6 +16,9 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphInterface; use Neos\ContentRepository\Core\Projection\ProjectionStateInterface; +use Neos\ContentRepository\Core\Projection\WithMarkStaleInterface; +use Neos\ContentRepository\Core\SharedModel\Auth\AuthProviderInterface; +use Neos\ContentRepository\Core\SharedModel\Auth\WorkspacePrivilegeType; use Neos\ContentRepository\Core\SharedModel\Exception\WorkspaceDoesNotExist; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStream; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; @@ -35,7 +38,7 @@ final class ContentRepositoryReadModel implements ProjectionStateInterface { public function __construct( - private readonly ContentRepositoryReadModelAdapterInterface $adapter + private readonly ContentRepositoryReadModelAdapterInterface $adapter, ) { } diff --git a/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php b/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php index bba7d784579..8dc6e517c88 100644 --- a/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php +++ b/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php @@ -31,7 +31,7 @@ use Neos\ContentRepository\Core\Projection\ProjectionCatchUpTriggerInterface; use Neos\ContentRepository\Core\Projection\ProjectionsAndCatchUpHooks; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; -use Neos\ContentRepository\Core\SharedModel\User\UserIdProviderInterface; +use Neos\ContentRepository\Core\SharedModel\Auth\AuthProviderInterface; use Neos\EventStore\EventStoreInterface; use Psr\Clock\ClockInterface; use Symfony\Component\Serializer\Serializer; @@ -54,7 +54,7 @@ public function __construct( Serializer $propertySerializer, ProjectionsAndCatchUpHooksFactory $projectionsAndCatchUpHooksFactory, private readonly ProjectionCatchUpTriggerInterface $projectionCatchUpTrigger, - private readonly UserIdProviderInterface $userIdProvider, + private readonly AuthProviderInterface $authProvider, private readonly ClockInterface $clock, ) { $contentDimensionZookeeper = new ContentDimensionZookeeper($contentDimensionSource); @@ -70,7 +70,8 @@ public function __construct( $contentDimensionSource, $contentDimensionZookeeper, $interDimensionalVariationGraph, - new PropertyConverter($propertySerializer) + new PropertyConverter($propertySerializer), + $this->authProvider, ); $this->projectionsAndCatchUpHooks = $projectionsAndCatchUpHooksFactory->build($this->projectionFactoryDependencies); } @@ -99,7 +100,7 @@ public function getOrBuild(): ContentRepository $this->projectionFactoryDependencies->nodeTypeManager, $this->projectionFactoryDependencies->interDimensionalVariationGraph, $this->projectionFactoryDependencies->contentDimensionSource, - $this->userIdProvider, + $this->authProvider, $this->clock, ); } diff --git a/Neos.ContentRepository.Core/Classes/Factory/ProjectionFactoryDependencies.php b/Neos.ContentRepository.Core/Classes/Factory/ProjectionFactoryDependencies.php index 9bb2f0cc31f..36b614ba623 100644 --- a/Neos.ContentRepository.Core/Classes/Factory/ProjectionFactoryDependencies.php +++ b/Neos.ContentRepository.Core/Classes/Factory/ProjectionFactoryDependencies.php @@ -20,6 +20,7 @@ use Neos\ContentRepository\Core\EventStore\EventNormalizer; use Neos\ContentRepository\Core\Infrastructure\Property\PropertyConverter; use Neos\ContentRepository\Core\NodeType\NodeTypeManager; +use Neos\ContentRepository\Core\SharedModel\Auth\AuthProviderInterface; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\EventStore\EventStoreInterface; @@ -37,6 +38,7 @@ public function __construct( public ContentDimensionZookeeper $contentDimensionZookeeper, public InterDimensionalVariationGraph $interDimensionalVariationGraph, public PropertyConverter $propertyConverter, + public AuthProviderInterface $authProvider, ) { } } diff --git a/Neos.ContentRepository.Core/Classes/SharedModel/Auth/AuthProviderInterface.php b/Neos.ContentRepository.Core/Classes/SharedModel/Auth/AuthProviderInterface.php new file mode 100644 index 00000000000..b2d03733050 --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/SharedModel/Auth/AuthProviderInterface.php @@ -0,0 +1,20 @@ +userId; + } + + public function getWorkspacePrivilege(WorkspaceName $workspaceName, WorkspacePrivilegeType $privilegeType): Privilege + { + return Privilege::granted(); + } + + public function getCommandPrivilege(CommandInterface $command): Privilege + { + return Privilege::granted(); + } +} diff --git a/Neos.ContentRepository.Core/Classes/SharedModel/User/UserId.php b/Neos.ContentRepository.Core/Classes/SharedModel/Auth/UserId.php similarity index 96% rename from Neos.ContentRepository.Core/Classes/SharedModel/User/UserId.php rename to Neos.ContentRepository.Core/Classes/SharedModel/Auth/UserId.php index 86f78e31a21..80228a031d0 100644 --- a/Neos.ContentRepository.Core/Classes/SharedModel/User/UserId.php +++ b/Neos.ContentRepository.Core/Classes/SharedModel/Auth/UserId.php @@ -12,7 +12,7 @@ declare(strict_types=1); -namespace Neos\ContentRepository\Core\SharedModel\User; +namespace Neos\ContentRepository\Core\SharedModel\Auth; use Neos\ContentRepository\Core\SharedModel\Id\UuidFactory; diff --git a/Neos.ContentRepository.Core/Classes/SharedModel/Auth/WorkspacePrivilegeType.php b/Neos.ContentRepository.Core/Classes/SharedModel/Auth/WorkspacePrivilegeType.php new file mode 100644 index 00000000000..c08d6aa4d46 --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/SharedModel/Auth/WorkspacePrivilegeType.php @@ -0,0 +1,23 @@ +userId; - } -} diff --git a/Neos.ContentRepository.Core/Classes/SharedModel/User/UserIdProviderInterface.php b/Neos.ContentRepository.Core/Classes/SharedModel/User/UserIdProviderInterface.php deleted file mode 100644 index 8530a34e30d..00000000000 --- a/Neos.ContentRepository.Core/Classes/SharedModel/User/UserIdProviderInterface.php +++ /dev/null @@ -1,13 +0,0 @@ -buildPropertySerializer($contentRepositoryId, $contentRepositorySettings), $this->buildProjectionsFactory($contentRepositoryId, $contentRepositorySettings), $this->buildProjectionCatchUpTrigger($contentRepositoryId, $contentRepositorySettings), - $this->buildUserIdProvider($contentRepositoryId, $contentRepositorySettings), + $this->buildAuthProvider($contentRepositoryId, $contentRepositorySettings), $clock ); } catch (\Exception $exception) { @@ -275,14 +275,14 @@ private function buildProjectionCatchUpTrigger(ContentRepositoryId $contentRepos } /** @param array $contentRepositorySettings */ - private function buildUserIdProvider(ContentRepositoryId $contentRepositoryId, array $contentRepositorySettings): UserIdProviderInterface + private function buildAuthProvider(ContentRepositoryId $contentRepositoryId, array $contentRepositorySettings): AuthProviderInterface { - isset($contentRepositorySettings['userIdProvider']['factoryObjectName']) || throw InvalidConfigurationException::fromMessage('Content repository "%s" does not have userIdProvider.factoryObjectName configured.', $contentRepositoryId->value); - $userIdProviderFactory = $this->objectManager->get($contentRepositorySettings['userIdProvider']['factoryObjectName']); - if (!$userIdProviderFactory instanceof UserIdProviderFactoryInterface) { - throw InvalidConfigurationException::fromMessage('userIdProvider.factoryObjectName for content repository "%s" is not an instance of %s but %s.', $contentRepositoryId->value, UserIdProviderFactoryInterface::class, get_debug_type($userIdProviderFactory)); + isset($contentRepositorySettings['authProvider']['factoryObjectName']) || throw InvalidConfigurationException::fromMessage('Content repository "%s" does not have authProvider.factoryObjectName configured.', $contentRepositoryId->value); + $authProviderFactory = $this->objectManager->get($contentRepositorySettings['authProvider']['factoryObjectName']); + if (!$authProviderFactory instanceof AuthProviderFactoryInterface) { + throw InvalidConfigurationException::fromMessage('authProvider.factoryObjectName for content repository "%s" is not an instance of %s but %s.', $contentRepositoryId->value, AuthProviderFactoryInterface::class, get_debug_type($authProviderFactory)); } - return $userIdProviderFactory->build($contentRepositoryId, $contentRepositorySettings['userIdProvider']['options'] ?? []); + return $authProviderFactory->build($contentRepositoryId, $contentRepositorySettings['authProvider']['options'] ?? []); } /** @param array $contentRepositorySettings */ diff --git a/Neos.ContentRepositoryRegistry/Classes/Factory/UserIdProvider/UserIdProviderFactoryInterface.php b/Neos.ContentRepositoryRegistry/Classes/Factory/AuthProvider/AuthProviderFactoryInterface.php similarity index 52% rename from Neos.ContentRepositoryRegistry/Classes/Factory/UserIdProvider/UserIdProviderFactoryInterface.php rename to Neos.ContentRepositoryRegistry/Classes/Factory/AuthProvider/AuthProviderFactoryInterface.php index a6145c7e8dc..9aebd57e688 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Factory/UserIdProvider/UserIdProviderFactoryInterface.php +++ b/Neos.ContentRepositoryRegistry/Classes/Factory/AuthProvider/AuthProviderFactoryInterface.php @@ -1,15 +1,17 @@ $options */ - public function build(ContentRepositoryId $contentRepositoryId, array $options): UserIdProviderInterface; + public function build(ContentRepositoryId $contentRepositoryId, array $options): AuthProviderInterface; } diff --git a/Neos.ContentRepositoryRegistry/Classes/Factory/AuthProvider/StaticAuthProviderFactory.php b/Neos.ContentRepositoryRegistry/Classes/Factory/AuthProvider/StaticAuthProviderFactory.php new file mode 100644 index 00000000000..4cc8b9a7ebd --- /dev/null +++ b/Neos.ContentRepositoryRegistry/Classes/Factory/AuthProvider/StaticAuthProviderFactory.php @@ -0,0 +1,20 @@ + $options */ + public function build(ContentRepositoryId $contentRepositoryId, array $options): AuthProviderInterface + { + return new StaticAuthProvider(UserId::forSystemUser()); + } +} diff --git a/Neos.ContentRepositoryRegistry/Classes/Factory/UserIdProvider/StaticUserIdProviderFactory.php b/Neos.ContentRepositoryRegistry/Classes/Factory/UserIdProvider/StaticUserIdProviderFactory.php deleted file mode 100644 index 563bc6b19a9..00000000000 --- a/Neos.ContentRepositoryRegistry/Classes/Factory/UserIdProvider/StaticUserIdProviderFactory.php +++ /dev/null @@ -1,20 +0,0 @@ - $options */ - public function build(ContentRepositoryId $contentRepositoryId, array $options): UserIdProviderInterface - { - return new StaticUserIdProvider(UserId::forSystemUser()); - } -} diff --git a/Neos.ContentRepositoryRegistry/Configuration/Settings.yaml b/Neos.ContentRepositoryRegistry/Configuration/Settings.yaml index 44e28699641..460706caaf8 100644 --- a/Neos.ContentRepositoryRegistry/Configuration/Settings.yaml +++ b/Neos.ContentRepositoryRegistry/Configuration/Settings.yaml @@ -34,8 +34,8 @@ Neos: projectionCatchUpTrigger: factoryObjectName: Neos\ContentRepositoryRegistry\Factory\ProjectionCatchUpTrigger\SubprocessProjectionCatchUpTriggerFactory - userIdProvider: - factoryObjectName: Neos\ContentRepositoryRegistry\Factory\UserIdProvider\StaticUserIdProviderFactory + authProvider: + factoryObjectName: Neos\ContentRepositoryRegistry\Factory\AuthProvider\StaticAuthProviderFactory clock: factoryObjectName: Neos\ContentRepositoryRegistry\Factory\Clock\SystemClockFactory diff --git a/Neos.Neos/Classes/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php b/Neos.Neos/Classes/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php new file mode 100644 index 00000000000..b154f482211 --- /dev/null +++ b/Neos.Neos/Classes/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php @@ -0,0 +1,147 @@ +userService->getCurrentUser(); + if ($user === null) { + return UserId::forSystemUser(); + } + return UserId::fromString($user->getId()->value); + } + + public function getWorkspacePrivilege(WorkspaceName $workspaceName, WorkspacePrivilegeType $privilegeType): Privilege + { + if ($this->securityContext->areAuthorizationChecksDisabled()) { + return Privilege::granted(); + } + $workspacePermissions = $this->getWorkspacePermissionsForAuthenticatedUser($workspaceName); + if ($workspacePermissions === null) { + return Privilege::denied('No user is authenticated'); + } + return match ($privilegeType) { + WorkspacePrivilegeType::READ_NODES => $workspacePermissions->read ? Privilege::granted() : Privilege::denied(sprintf('User has no read permission for workspace "%s"', $workspaceName->value)), + }; + } + + public function getCommandPrivilege(CommandInterface $command): Privilege + { + if ($this->securityContext->areAuthorizationChecksDisabled()) { + return Privilege::granted(); + } + if ($command instanceof CreateWorkspace) { + $baseWorkspacePermissions = $this->getWorkspacePermissionsForAuthenticatedUser($command->baseWorkspaceName); + if ($baseWorkspacePermissions === null || !$baseWorkspacePermissions->write) { + return Privilege::denied(sprintf('no write permissions on base workspace "%s"', $command->baseWorkspaceName->value)); + } + return Privilege::granted(); + } + list($privilege, $workspaceName) = match ($command::class) { + AddDimensionShineThrough::class, + ChangeNodeAggregateName::class, + ChangeNodeAggregateType::class, + CopyNodesRecursively::class, + CreateNodeAggregateWithNode::class, + CreateNodeAggregateWithNodeAndSerializedProperties::class, + CreateNodeVariant::class, + CreateRootNodeAggregateWithNode::class, + DisableNodeAggregate::class, + DiscardIndividualNodesFromWorkspace::class, + DiscardWorkspace::class, + EnableNodeAggregate::class, + MoveDimensionSpacePoint::class, + MoveNodeAggregate::class, + PublishIndividualNodesFromWorkspace::class, + PublishWorkspace::class, + RebaseWorkspace::class, + RemoveNodeAggregate::class, + SetNodeProperties::class, + SetNodeReferences::class, + SetSerializedNodeProperties::class, + SetSerializedNodeReferences::class, + TagSubtree::class, + UntagSubtree::class, + UpdateRootNodeAggregateDimensions::class => ['write', $command->workspaceName], + ChangeBaseWorkspace::class, + CreateRootWorkspace::class, + CreateWorkspace::class, + DeleteWorkspace::class => ['manage', $command->workspaceName], + default => [null, null], + }; + if ($privilege === null) { + return Privilege::granted(); + } + $workspacePermissions = $this->getWorkspacePermissionsForAuthenticatedUser($workspaceName); + if ($workspacePermissions === null) { + return Privilege::denied(sprintf('No user is authenticated to %s workspace "%s" because no user is authenticated', $privilege, $workspaceName->value)); + } + $privilegeGranted = $privilege === 'write' ? $workspacePermissions->write : $workspacePermissions->manage; + return $privilegeGranted ? Privilege::granted() : Privilege::denied(sprintf('User has no %s permission for workspace "%s"', $privilege, $workspaceName->value)); + } + + private function getWorkspacePermissionsForAuthenticatedUser(WorkspaceName $workspaceName): ?WorkspacePermissions + { + $user = $this->userService->getCurrentUser(); + if ($user === null) { + return null; + } + return $this->workspaceService->getWorkspacePermissionsForUser($this->contentRepositoryId, $workspaceName, $user); + } +} diff --git a/Neos.Neos/Classes/ContentRepositoryAuthProvider/ContentRepositoryAuthProviderFactory.php b/Neos.Neos/Classes/ContentRepositoryAuthProvider/ContentRepositoryAuthProviderFactory.php new file mode 100644 index 00000000000..63f84ae439e --- /dev/null +++ b/Neos.Neos/Classes/ContentRepositoryAuthProvider/ContentRepositoryAuthProviderFactory.php @@ -0,0 +1,35 @@ + $options + */ + public function build(ContentRepositoryId $contentRepositoryId, array $options): ContentRepositoryAuthProvider + { + return new ContentRepositoryAuthProvider($contentRepositoryId, $this->userService, $this->workspaceService); + } +} diff --git a/Neos.Neos/Classes/Domain/Service/WorkspaceService.php b/Neos.Neos/Classes/Domain/Service/WorkspaceService.php index 0ada7877998..e64c5acba84 100644 --- a/Neos.Neos/Classes/Domain/Service/WorkspaceService.php +++ b/Neos.Neos/Classes/Domain/Service/WorkspaceService.php @@ -27,6 +27,7 @@ use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Annotations as Flow; +use Neos\Flow\Security\Context as SecurityContext; use Neos\Flow\Security\Exception\NoSuchRoleException; use Neos\Neos\Domain\Model\User; use Neos\Neos\Domain\Model\UserId; @@ -44,6 +45,8 @@ /** * Central authority to interact with Content Repository Workspaces within Neos * + * TODO evaluate permissions for workspace changes + * * @api */ #[Flow\Scope('singleton')] @@ -56,6 +59,7 @@ public function __construct( private readonly ContentRepositoryRegistry $contentRepositoryRegistry, private readonly UserService $userService, private readonly Connection $dbal, + private readonly SecurityContext $securityContext, ) { } @@ -83,6 +87,7 @@ public function getWorkspaceMetadata(ContentRepositoryId $contentRepositoryId, W */ public function setWorkspaceTitle(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, WorkspaceTitle $newWorkspaceTitle): void { + // TODO check workspace permissions -> $this->getWorkspacePermissionsForUser($contentRepositoryId, $workspaceName, $this->userService->getCurrentUser()); $this->updateWorkspaceMetadata($contentRepositoryId, $workspaceName, [ 'title' => $newWorkspaceTitle->value, ]); @@ -173,14 +178,14 @@ public function createPersonalWorkspaceForUserIfMissing(ContentRepositoryId $con return; } $workspaceName = $this->getUniqueWorkspaceName($contentRepositoryId, $user->getLabel()); - $this->createPersonalWorkspace( + $this->securityContext->withoutAuthorizationChecks(fn () => $this->createPersonalWorkspace( $contentRepositoryId, $workspaceName, WorkspaceTitle::fromString($user->getLabel()), WorkspaceDescription::empty(), WorkspaceName::forLive(), $user->getId(), - ); + )); } /** diff --git a/Neos.Neos/Classes/Service/Controller/AbstractServiceController.php b/Neos.Neos/Classes/Service/Controller/AbstractServiceController.php index a212f1ac725..aca0d7a9327 100644 --- a/Neos.Neos/Classes/Service/Controller/AbstractServiceController.php +++ b/Neos.Neos/Classes/Service/Controller/AbstractServiceController.php @@ -15,7 +15,6 @@ namespace Neos\Neos\Service\Controller; use GuzzleHttp\Psr7\Response; -use Neos\ContentRepository\Core\SharedModel\User\UserId; use Neos\Flow\Annotations as Flow; use Neos\Flow\Exception as FlowException; use Neos\Flow\Log\ThrowableStorageInterface; @@ -157,13 +156,4 @@ protected function convertException(\Throwable $exception): array } return $exceptionData; } - - protected function getCurrentUserIdentifier(): ?UserId - { - $user = $this->domainUserService->getCurrentUser(); - - return $user - ? UserId::fromString($this->persistenceManager->getIdentifierByObject($user)) - : null; - } } diff --git a/Neos.Neos/Classes/UserIdProvider/UserIdProvider.php b/Neos.Neos/Classes/UserIdProvider/UserIdProvider.php deleted file mode 100644 index b81c97e8c5a..00000000000 --- a/Neos.Neos/Classes/UserIdProvider/UserIdProvider.php +++ /dev/null @@ -1,29 +0,0 @@ -userService->getCurrentUser(); - if ($user === null) { - return UserId::forSystemUser(); - } - return UserId::fromString($user->getId()->value); - } -} diff --git a/Neos.Neos/Classes/UserIdProvider/UserIdProviderFactory.php b/Neos.Neos/Classes/UserIdProvider/UserIdProviderFactory.php deleted file mode 100644 index 388dc6f19f3..00000000000 --- a/Neos.Neos/Classes/UserIdProvider/UserIdProviderFactory.php +++ /dev/null @@ -1,34 +0,0 @@ - $options - */ - public function build(ContentRepositoryId $contentRepositoryId, array $options): UserIdProviderInterface - { - return new UserIdProvider($this->userService); - } -} diff --git a/Neos.Neos/Configuration/Settings.ContentRepositoryRegistry.yaml b/Neos.Neos/Configuration/Settings.ContentRepositoryRegistry.yaml index a3ae6ab53c0..c088a067cbe 100644 --- a/Neos.Neos/Configuration/Settings.ContentRepositoryRegistry.yaml +++ b/Neos.Neos/Configuration/Settings.ContentRepositoryRegistry.yaml @@ -3,8 +3,8 @@ Neos: presets: 'default': - userIdProvider: - factoryObjectName: Neos\Neos\UserIdProvider\UserIdProviderFactory + authProvider: + factoryObjectName: Neos\Neos\ContentRepositoryAuthProvider\ContentRepositoryAuthProviderFactory projections: 'Neos.Neos:DocumentUriPathProjection': From 4a9361072f76bd414b6ec31fc1a39b6424d50c44 Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Thu, 17 Oct 2024 17:36:31 +0200 Subject: [PATCH 002/214] WIP: FEATURE: Content Repository Privileges --- .../Classes/ContentRepository.php | 9 ++ .../RebasableToOtherWorkspaceInterface.php | 2 + .../Command/AddDimensionShineThrough.php | 5 + .../Command/MoveDimensionSpacePoint.php | 5 + ...gregateWithNodeAndSerializedProperties.php | 5 + .../Command/DisableNodeAggregate.php | 5 + .../Command/EnableNodeAggregate.php | 5 + .../Command/CopyNodesRecursively.php | 5 + .../Command/SetSerializedNodeProperties.php | 5 + .../NodeMove/Command/MoveNodeAggregate.php | 5 + .../Command/SetSerializedNodeReferences.php | 5 + .../Command/RemoveNodeAggregate.php | 5 + .../Command/ChangeNodeAggregateName.php | 5 + .../Command/ChangeNodeAggregateType.php | 5 + .../Command/CreateNodeVariant.php | 5 + .../CreateRootNodeAggregateWithNode.php | 5 + .../UpdateRootNodeAggregateDimensions.php | 5 + .../SubtreeTagging/Command/TagSubtree.php | 5 + .../SubtreeTagging/Command/UntagSubtree.php | 5 + .../ContentGraph/VisibilityConstraints.php | 4 +- .../Auth/AuthProviderInterface.php | 3 + .../SharedModel/Auth/StaticAuthProvider.php | 6 + .../Bootstrap/CRTestSuiteRuntimeVariables.php | 2 +- .../Features/Bootstrap/CRTestSuiteTrait.php | 2 +- .../Bootstrap/Helpers/FakeAuthProvider.php | 6 + .../ContentRepositoryAuthProvider.php | 147 ------------------ .../Controller/Frontend/NodeController.php | 17 +- .../Cache/NeosFusionContextSerializer.php | 7 +- .../Privilege/SubtreeTagPrivilege.php | 40 +++++ .../Privilege/SubtreeTagPrivilegeSubject.php | 29 ++++ .../ContentRepositoryAuthProvider.php | 126 +++++++++++++++ .../ContentRepositoryAuthProviderFactory.php | 3 +- .../NodeAddressToNodeConverter.php | 9 +- .../Classes/View/FusionExceptionView.php | 2 +- .../Settings.ContentRepositoryRegistry.yaml | 2 +- 35 files changed, 317 insertions(+), 184 deletions(-) delete mode 100644 Neos.Neos/Classes/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php create mode 100644 Neos.Neos/Classes/Security/Authorization/Privilege/SubtreeTagPrivilege.php create mode 100644 Neos.Neos/Classes/Security/Authorization/Privilege/SubtreeTagPrivilegeSubject.php create mode 100644 Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php rename Neos.Neos/Classes/{ => Security}/ContentRepositoryAuthProvider/ContentRepositoryAuthProviderFactory.php (89%) diff --git a/Neos.ContentRepository.Core/Classes/ContentRepository.php b/Neos.ContentRepository.Core/Classes/ContentRepository.php index f2e50762a18..7ba16616c99 100644 --- a/Neos.ContentRepository.Core/Classes/ContentRepository.php +++ b/Neos.ContentRepository.Core/Classes/ContentRepository.php @@ -18,6 +18,7 @@ use Neos\ContentRepository\Core\CommandHandler\CommandInterface; use Neos\ContentRepository\Core\CommandHandler\CommandResult; use Neos\ContentRepository\Core\Dimension\ContentDimensionSourceInterface; +use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\DimensionSpace\InterDimensionalVariationGraph; use Neos\ContentRepository\Core\EventStore\DecoratedEvent; use Neos\ContentRepository\Core\EventStore\EventInterface; @@ -30,6 +31,7 @@ use Neos\ContentRepository\Core\Projection\CatchUp; use Neos\ContentRepository\Core\Projection\CatchUpOptions; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphInterface; +use Neos\ContentRepository\Core\Projection\ContentGraph\ContentSubgraphInterface; use Neos\ContentRepository\Core\Projection\ProjectionInterface; use Neos\ContentRepository\Core\Projection\ProjectionsAndCatchUpHooks; use Neos\ContentRepository\Core\Projection\ProjectionStateInterface; @@ -283,6 +285,13 @@ public function getContentGraph(WorkspaceName $workspaceName): ContentGraphInter return $this->getContentRepositoryReadModel()->getContentGraphByWorkspaceName($workspaceName); } + public function getContentSubgraph(WorkspaceName $workspaceName, DimensionSpacePoint $dimensionSpacePoint): ContentSubgraphInterface + { + $contentGraph = $this->getContentGraph($workspaceName); + $visibilityConstraints = $this->authProvider->getVisibilityConstraints($workspaceName); + return $contentGraph->getSubgraph($dimensionSpacePoint, $visibilityConstraints); + } + public function getNodeTypeManager(): NodeTypeManager { return $this->nodeTypeManager; diff --git a/Neos.ContentRepository.Core/Classes/Feature/Common/RebasableToOtherWorkspaceInterface.php b/Neos.ContentRepository.Core/Classes/Feature/Common/RebasableToOtherWorkspaceInterface.php index 4d2d5094818..844f12ae402 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/Common/RebasableToOtherWorkspaceInterface.php +++ b/Neos.ContentRepository.Core/Classes/Feature/Common/RebasableToOtherWorkspaceInterface.php @@ -31,6 +31,8 @@ public function createCopyForWorkspace( WorkspaceName $targetWorkspaceName, ): CommandInterface; + public function getWorkspaceName(): WorkspaceName; + /** * called during deserialization from metadata * @param array $array diff --git a/Neos.ContentRepository.Core/Classes/Feature/DimensionSpaceAdjustment/Command/AddDimensionShineThrough.php b/Neos.ContentRepository.Core/Classes/Feature/DimensionSpaceAdjustment/Command/AddDimensionShineThrough.php index 936505d278d..e3143635b15 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/DimensionSpaceAdjustment/Command/AddDimensionShineThrough.php +++ b/Neos.ContentRepository.Core/Classes/Feature/DimensionSpaceAdjustment/Command/AddDimensionShineThrough.php @@ -84,6 +84,11 @@ public function createCopyForWorkspace(WorkspaceName $targetWorkspaceName): self ); } + public function getWorkspaceName(): WorkspaceName + { + return $this->workspaceName; + } + /** * @return array */ diff --git a/Neos.ContentRepository.Core/Classes/Feature/DimensionSpaceAdjustment/Command/MoveDimensionSpacePoint.php b/Neos.ContentRepository.Core/Classes/Feature/DimensionSpaceAdjustment/Command/MoveDimensionSpacePoint.php index 498ba261e49..e6ae20eb2b5 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/DimensionSpaceAdjustment/Command/MoveDimensionSpacePoint.php +++ b/Neos.ContentRepository.Core/Classes/Feature/DimensionSpaceAdjustment/Command/MoveDimensionSpacePoint.php @@ -80,6 +80,11 @@ public function createCopyForWorkspace( ); } + public function getWorkspaceName(): WorkspaceName + { + return $this->workspaceName; + } + /** * @return array */ diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/Command/CreateNodeAggregateWithNodeAndSerializedProperties.php b/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/Command/CreateNodeAggregateWithNodeAndSerializedProperties.php index f295cd6dabe..6f1f44c2531 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/Command/CreateNodeAggregateWithNodeAndSerializedProperties.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/Command/CreateNodeAggregateWithNodeAndSerializedProperties.php @@ -176,4 +176,9 @@ public function createCopyForWorkspace( $this->tetheredDescendantNodeAggregateIds ); } + + public function getWorkspaceName(): WorkspaceName + { + return $this->workspaceName; + } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/Command/DisableNodeAggregate.php b/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/Command/DisableNodeAggregate.php index 454b7c2ccb2..527e19ae21c 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/Command/DisableNodeAggregate.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/Command/DisableNodeAggregate.php @@ -99,4 +99,9 @@ public function createCopyForWorkspace( $this->nodeVariantSelectionStrategy ); } + + public function getWorkspaceName(): WorkspaceName + { + return $this->workspaceName; + } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/Command/EnableNodeAggregate.php b/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/Command/EnableNodeAggregate.php index de0ad11d57d..41d70615b29 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/Command/EnableNodeAggregate.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/Command/EnableNodeAggregate.php @@ -99,4 +99,9 @@ public function createCopyForWorkspace( $this->nodeVariantSelectionStrategy ); } + + public function getWorkspaceName(): WorkspaceName + { + return $this->workspaceName; + } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/Command/CopyNodesRecursively.php b/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/Command/CopyNodesRecursively.php index 52355fd5e4a..20ca7998a06 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/Command/CopyNodesRecursively.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/Command/CopyNodesRecursively.php @@ -183,4 +183,9 @@ public function createCopyForWorkspace( $this->nodeAggregateIdMapping ); } + + public function getWorkspaceName(): WorkspaceName + { + return $this->workspaceName; + } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Command/SetSerializedNodeProperties.php b/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Command/SetSerializedNodeProperties.php index ca589337e85..edae34fbc7f 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Command/SetSerializedNodeProperties.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Command/SetSerializedNodeProperties.php @@ -119,4 +119,9 @@ public function createCopyForWorkspace( $this->propertiesToUnset, ); } + + public function getWorkspaceName(): WorkspaceName + { + return $this->workspaceName; + } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeMove/Command/MoveNodeAggregate.php b/Neos.ContentRepository.Core/Classes/Feature/NodeMove/Command/MoveNodeAggregate.php index 3a756163488..ca2fe213734 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeMove/Command/MoveNodeAggregate.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeMove/Command/MoveNodeAggregate.php @@ -131,4 +131,9 @@ public function createCopyForWorkspace( $this->newSucceedingSiblingNodeAggregateId ); } + + public function getWorkspaceName(): WorkspaceName + { + return $this->workspaceName; + } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Command/SetSerializedNodeReferences.php b/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Command/SetSerializedNodeReferences.php index 3f635af08e0..6ddcdbb6c93 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Command/SetSerializedNodeReferences.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Command/SetSerializedNodeReferences.php @@ -107,4 +107,9 @@ public function createCopyForWorkspace( $this->references, ); } + + public function getWorkspaceName(): WorkspaceName + { + return $this->workspaceName; + } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeRemoval/Command/RemoveNodeAggregate.php b/Neos.ContentRepository.Core/Classes/Feature/NodeRemoval/Command/RemoveNodeAggregate.php index 085af255b8c..654edc63dd5 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeRemoval/Command/RemoveNodeAggregate.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeRemoval/Command/RemoveNodeAggregate.php @@ -119,4 +119,9 @@ public function createCopyForWorkspace( $this->removalAttachmentPoint, ); } + + public function getWorkspaceName(): WorkspaceName + { + return $this->workspaceName; + } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeRenaming/Command/ChangeNodeAggregateName.php b/Neos.ContentRepository.Core/Classes/Feature/NodeRenaming/Command/ChangeNodeAggregateName.php index 38d1f195ed6..5df67f01181 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeRenaming/Command/ChangeNodeAggregateName.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeRenaming/Command/ChangeNodeAggregateName.php @@ -93,4 +93,9 @@ public function createCopyForWorkspace( $this->newNodeName, ); } + + public function getWorkspaceName(): WorkspaceName + { + return $this->workspaceName; + } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeTypeChange/Command/ChangeNodeAggregateType.php b/Neos.ContentRepository.Core/Classes/Feature/NodeTypeChange/Command/ChangeNodeAggregateType.php index e979e4d25c1..ede35595343 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeTypeChange/Command/ChangeNodeAggregateType.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeTypeChange/Command/ChangeNodeAggregateType.php @@ -118,4 +118,9 @@ public function createCopyForWorkspace( $this->tetheredDescendantNodeAggregateIds ); } + + public function getWorkspaceName(): WorkspaceName + { + return $this->workspaceName; + } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeVariation/Command/CreateNodeVariant.php b/Neos.ContentRepository.Core/Classes/Feature/NodeVariation/Command/CreateNodeVariant.php index 001f9bd66e9..a8224f79dbc 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeVariation/Command/CreateNodeVariant.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeVariation/Command/CreateNodeVariant.php @@ -97,4 +97,9 @@ public function createCopyForWorkspace( $this->targetOrigin, ); } + + public function getWorkspaceName(): WorkspaceName + { + return $this->workspaceName; + } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/RootNodeCreation/Command/CreateRootNodeAggregateWithNode.php b/Neos.ContentRepository.Core/Classes/Feature/RootNodeCreation/Command/CreateRootNodeAggregateWithNode.php index 9bbb1f320cc..b2fe51dc5c6 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/RootNodeCreation/Command/CreateRootNodeAggregateWithNode.php +++ b/Neos.ContentRepository.Core/Classes/Feature/RootNodeCreation/Command/CreateRootNodeAggregateWithNode.php @@ -135,4 +135,9 @@ public function createCopyForWorkspace( $this->tetheredDescendantNodeAggregateIds ); } + + public function getWorkspaceName(): WorkspaceName + { + return $this->workspaceName; + } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/RootNodeCreation/Command/UpdateRootNodeAggregateDimensions.php b/Neos.ContentRepository.Core/Classes/Feature/RootNodeCreation/Command/UpdateRootNodeAggregateDimensions.php index 302c05ed895..489c0d96ca5 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/RootNodeCreation/Command/UpdateRootNodeAggregateDimensions.php +++ b/Neos.ContentRepository.Core/Classes/Feature/RootNodeCreation/Command/UpdateRootNodeAggregateDimensions.php @@ -78,4 +78,9 @@ public function createCopyForWorkspace( $this->nodeAggregateId, ); } + + public function getWorkspaceName(): WorkspaceName + { + return $this->workspaceName; + } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Command/TagSubtree.php b/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Command/TagSubtree.php index 71fb012b0cc..21e9ad2cb21 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Command/TagSubtree.php +++ b/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Command/TagSubtree.php @@ -89,6 +89,11 @@ public function createCopyForWorkspace(WorkspaceName $targetWorkspaceName): self ); } + public function getWorkspaceName(): WorkspaceName + { + return $this->workspaceName; + } + public function matchesNodeId(NodeIdToPublishOrDiscard $nodeIdToPublish): bool { return $this->nodeAggregateId->equals($nodeIdToPublish->nodeAggregateId) diff --git a/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Command/UntagSubtree.php b/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Command/UntagSubtree.php index cca49333c95..7aa78d270aa 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Command/UntagSubtree.php +++ b/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Command/UntagSubtree.php @@ -90,6 +90,11 @@ public function createCopyForWorkspace(WorkspaceName $targetWorkspaceName): self ); } + public function getWorkspaceName(): WorkspaceName + { + return $this->workspaceName; + } + public function matchesNodeId(NodeIdToPublishOrDiscard $nodeIdToPublish): bool { return $this->nodeAggregateId->equals($nodeIdToPublish->nodeAggregateId) diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/VisibilityConstraints.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/VisibilityConstraints.php index 7462e28a4e1..7271e3634a3 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/VisibilityConstraints.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/VisibilityConstraints.php @@ -29,7 +29,7 @@ /** * @param SubtreeTags $tagConstraints A set of {@see SubtreeTag} instances that will be _excluded_ from the results of any content graph query */ - private function __construct( + public function __construct( public SubtreeTags $tagConstraints, ) { } @@ -48,7 +48,7 @@ public static function withoutRestrictions(): self return new self(SubtreeTags::createEmpty()); } - public static function frontend(): VisibilityConstraints + public static function default(): VisibilityConstraints { return new self(SubtreeTags::fromStrings('disabled')); } diff --git a/Neos.ContentRepository.Core/Classes/SharedModel/Auth/AuthProviderInterface.php b/Neos.ContentRepository.Core/Classes/SharedModel/Auth/AuthProviderInterface.php index b2d03733050..b01147cf727 100644 --- a/Neos.ContentRepository.Core/Classes/SharedModel/Auth/AuthProviderInterface.php +++ b/Neos.ContentRepository.Core/Classes/SharedModel/Auth/AuthProviderInterface.php @@ -5,6 +5,7 @@ namespace Neos\ContentRepository\Core\SharedModel\Auth; use Neos\ContentRepository\Core\CommandHandler\CommandInterface; +use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; /** @@ -16,5 +17,7 @@ public function getUserId(): UserId; public function getWorkspacePrivilege(WorkspaceName $workspaceName, WorkspacePrivilegeType $privilegeType): Privilege; + public function getVisibilityConstraints(WorkspaceName $workspaceName): VisibilityConstraints; + public function getCommandPrivilege(CommandInterface $command): Privilege; } diff --git a/Neos.ContentRepository.Core/Classes/SharedModel/Auth/StaticAuthProvider.php b/Neos.ContentRepository.Core/Classes/SharedModel/Auth/StaticAuthProvider.php index 66ad3102805..4b49b9bcea3 100644 --- a/Neos.ContentRepository.Core/Classes/SharedModel/Auth/StaticAuthProvider.php +++ b/Neos.ContentRepository.Core/Classes/SharedModel/Auth/StaticAuthProvider.php @@ -5,6 +5,7 @@ namespace Neos\ContentRepository\Core\SharedModel\Auth; use Neos\ContentRepository\Core\CommandHandler\CommandInterface; +use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; /** @@ -24,6 +25,11 @@ public function getUserId(): UserId return $this->userId; } + public function getVisibilityConstraints(WorkspaceName $workspaceName): VisibilityConstraints + { + return VisibilityConstraints::default(); + } + public function getWorkspacePrivilege(WorkspaceName $workspaceName, WorkspacePrivilegeType $privilegeType): Privilege { return Privilege::granted(); diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteRuntimeVariables.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteRuntimeVariables.php index 1bdc2a5be01..66b1e76d28d 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteRuntimeVariables.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteRuntimeVariables.php @@ -118,7 +118,7 @@ public function visibilityConstraintsAreSetTo(string $restrictionType): void { $this->currentVisibilityConstraints = match ($restrictionType) { 'withoutRestrictions' => VisibilityConstraints::withoutRestrictions(), - 'frontend' => VisibilityConstraints::frontend(), + 'frontend' => VisibilityConstraints::default(), default => throw new \InvalidArgumentException('Visibility constraint "' . $restrictionType . '" not supported.'), }; } diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteTrait.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteTrait.php index 6dbe16daf40..b067f059ae2 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteTrait.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteTrait.php @@ -93,7 +93,7 @@ public function beforeEventSourcedScenarioDispatcher(BeforeScenarioScope $scope) $this->contentRepositories = []; } $this->currentContentRepository = null; - $this->currentVisibilityConstraints = VisibilityConstraints::frontend(); + $this->currentVisibilityConstraints = VisibilityConstraints::default(); $this->currentDimensionSpacePoint = null; $this->currentRootNodeAggregateId = null; $this->currentWorkspaceName = null; diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Helpers/FakeAuthProvider.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Helpers/FakeAuthProvider.php index 977e82ac1ec..605c17e0957 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Helpers/FakeAuthProvider.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Helpers/FakeAuthProvider.php @@ -5,6 +5,7 @@ namespace Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Helpers; use Neos\ContentRepository\Core\CommandHandler\CommandInterface; +use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\Core\SharedModel\Auth\Privilege; use Neos\ContentRepository\Core\SharedModel\Auth\UserId; use Neos\ContentRepository\Core\SharedModel\Auth\AuthProviderInterface; @@ -25,6 +26,11 @@ public function getUserId(): UserId return self::$userId ?? UserId::forSystemUser(); } + public function getVisibilityConstraints(WorkspaceName $workspaceName): VisibilityConstraints + { + return VisibilityConstraints::withoutRestrictions(); + } + public function getWorkspacePrivilege(WorkspaceName $workspaceName, WorkspacePrivilegeType $privilegeType): Privilege { return Privilege::granted(); diff --git a/Neos.Neos/Classes/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php b/Neos.Neos/Classes/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php deleted file mode 100644 index b154f482211..00000000000 --- a/Neos.Neos/Classes/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php +++ /dev/null @@ -1,147 +0,0 @@ -userService->getCurrentUser(); - if ($user === null) { - return UserId::forSystemUser(); - } - return UserId::fromString($user->getId()->value); - } - - public function getWorkspacePrivilege(WorkspaceName $workspaceName, WorkspacePrivilegeType $privilegeType): Privilege - { - if ($this->securityContext->areAuthorizationChecksDisabled()) { - return Privilege::granted(); - } - $workspacePermissions = $this->getWorkspacePermissionsForAuthenticatedUser($workspaceName); - if ($workspacePermissions === null) { - return Privilege::denied('No user is authenticated'); - } - return match ($privilegeType) { - WorkspacePrivilegeType::READ_NODES => $workspacePermissions->read ? Privilege::granted() : Privilege::denied(sprintf('User has no read permission for workspace "%s"', $workspaceName->value)), - }; - } - - public function getCommandPrivilege(CommandInterface $command): Privilege - { - if ($this->securityContext->areAuthorizationChecksDisabled()) { - return Privilege::granted(); - } - if ($command instanceof CreateWorkspace) { - $baseWorkspacePermissions = $this->getWorkspacePermissionsForAuthenticatedUser($command->baseWorkspaceName); - if ($baseWorkspacePermissions === null || !$baseWorkspacePermissions->write) { - return Privilege::denied(sprintf('no write permissions on base workspace "%s"', $command->baseWorkspaceName->value)); - } - return Privilege::granted(); - } - list($privilege, $workspaceName) = match ($command::class) { - AddDimensionShineThrough::class, - ChangeNodeAggregateName::class, - ChangeNodeAggregateType::class, - CopyNodesRecursively::class, - CreateNodeAggregateWithNode::class, - CreateNodeAggregateWithNodeAndSerializedProperties::class, - CreateNodeVariant::class, - CreateRootNodeAggregateWithNode::class, - DisableNodeAggregate::class, - DiscardIndividualNodesFromWorkspace::class, - DiscardWorkspace::class, - EnableNodeAggregate::class, - MoveDimensionSpacePoint::class, - MoveNodeAggregate::class, - PublishIndividualNodesFromWorkspace::class, - PublishWorkspace::class, - RebaseWorkspace::class, - RemoveNodeAggregate::class, - SetNodeProperties::class, - SetNodeReferences::class, - SetSerializedNodeProperties::class, - SetSerializedNodeReferences::class, - TagSubtree::class, - UntagSubtree::class, - UpdateRootNodeAggregateDimensions::class => ['write', $command->workspaceName], - ChangeBaseWorkspace::class, - CreateRootWorkspace::class, - CreateWorkspace::class, - DeleteWorkspace::class => ['manage', $command->workspaceName], - default => [null, null], - }; - if ($privilege === null) { - return Privilege::granted(); - } - $workspacePermissions = $this->getWorkspacePermissionsForAuthenticatedUser($workspaceName); - if ($workspacePermissions === null) { - return Privilege::denied(sprintf('No user is authenticated to %s workspace "%s" because no user is authenticated', $privilege, $workspaceName->value)); - } - $privilegeGranted = $privilege === 'write' ? $workspacePermissions->write : $workspacePermissions->manage; - return $privilegeGranted ? Privilege::granted() : Privilege::denied(sprintf('User has no %s permission for workspace "%s"', $privilege, $workspaceName->value)); - } - - private function getWorkspacePermissionsForAuthenticatedUser(WorkspaceName $workspaceName): ?WorkspacePermissions - { - $user = $this->userService->getCurrentUser(); - if ($user === null) { - return null; - } - return $this->workspaceService->getWorkspacePermissionsForUser($this->contentRepositoryId, $workspaceName, $user); - } -} diff --git a/Neos.Neos/Classes/Controller/Frontend/NodeController.php b/Neos.Neos/Classes/Controller/Frontend/NodeController.php index a426c24328f..36fba3aacfe 100644 --- a/Neos.Neos/Classes/Controller/Frontend/NodeController.php +++ b/Neos.Neos/Classes/Controller/Frontend/NodeController.php @@ -20,7 +20,6 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\Projection\ContentGraph\Nodes; use Neos\ContentRepository\Core\Projection\ContentGraph\Subtree; -use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; @@ -125,21 +124,12 @@ public function previewAction(string $node): void { // @todo add $renderingModeName as parameter and append it for successive links again as get parameter to node uris $renderingMode = $this->renderingModeService->findByCurrentUser(); - - $visibilityConstraints = VisibilityConstraints::frontend(); - if ($this->privilegeManager->isPrivilegeTargetGranted('Neos.Neos:Backend.GeneralAccess')) { - $visibilityConstraints = VisibilityConstraints::withoutRestrictions(); - } - $siteDetectionResult = SiteDetectionResult::fromRequest($this->request->getHttpRequest()); $contentRepository = $this->contentRepositoryRegistry->get($siteDetectionResult->contentRepositoryId); $nodeAddress = NodeAddress::fromJsonString($node); - $subgraph = $contentRepository->getContentGraph($nodeAddress->workspaceName)->getSubgraph( - $nodeAddress->dimensionSpacePoint, - $visibilityConstraints - ); + $subgraph = $contentRepository->getContentSubgraph($nodeAddress->workspaceName, $nodeAddress->dimensionSpacePoint); $nodeInstance = $subgraph->findNodeById($nodeAddress->aggregateId); @@ -204,10 +194,7 @@ public function showAction(string $node): void } $contentRepository = $this->contentRepositoryRegistry->get($nodeAddress->contentRepositoryId); - $uncachedSubgraph = $contentRepository->getContentGraph($nodeAddress->workspaceName)->getSubgraph( - $nodeAddress->dimensionSpacePoint, - VisibilityConstraints::frontend() - ); + $uncachedSubgraph = $contentRepository->getContentSubgraph($nodeAddress->workspaceName, $nodeAddress->dimensionSpacePoint); $subgraph = new ContentSubgraphWithRuntimeCaches($uncachedSubgraph, $this->subgraphCachePool); $nodeInstance = $subgraph->findNodeById($nodeAddress->aggregateId); diff --git a/Neos.Neos/Classes/Fusion/Cache/NeosFusionContextSerializer.php b/Neos.Neos/Classes/Fusion/Cache/NeosFusionContextSerializer.php index ac47b1e3172..cf9165eb0fa 100644 --- a/Neos.Neos/Classes/Fusion/Cache/NeosFusionContextSerializer.php +++ b/Neos.Neos/Classes/Fusion/Cache/NeosFusionContextSerializer.php @@ -73,12 +73,7 @@ private function tryDeserializeNode(array $serializedNode): ?Node $contentRepository = $this->contentRepositoryRegistry->get($nodeAddress->contentRepositoryId); try { - $subgraph = $contentRepository->getContentGraph($nodeAddress->workspaceName)->getSubgraph( - $nodeAddress->dimensionSpacePoint, - $nodeAddress->workspaceName->isLive() - ? VisibilityConstraints::frontend() - : VisibilityConstraints::withoutRestrictions() - ); + $subgraph = $contentRepository->getContentSubgraph($nodeAddress->workspaceName, $nodeAddress->dimensionSpacePoint); } catch (WorkspaceDoesNotExist $exception) { // in case the workspace was deleted the rendering should probably not come to this very point // still if it does we fail silently diff --git a/Neos.Neos/Classes/Security/Authorization/Privilege/SubtreeTagPrivilege.php b/Neos.Neos/Classes/Security/Authorization/Privilege/SubtreeTagPrivilege.php new file mode 100644 index 00000000000..2b3d1f157d0 --- /dev/null +++ b/Neos.Neos/Classes/Security/Authorization/Privilege/SubtreeTagPrivilege.php @@ -0,0 +1,40 @@ +userService->getCurrentUser(); + if ($user === null) { + return UserId::forSystemUser(); + } + return UserId::fromString($user->getId()->value); + } + + public function getVisibilityConstraints(WorkspaceName $workspaceName): VisibilityConstraints + { + if ($this->securityContext->areAuthorizationChecksDisabled()) { + return VisibilityConstraints::default(); + } + $restrictedSubtreeTags = [SubtreeTag::disabled()]; + try { + /** @var array $subtreeTagPrivileges */ + $subtreeTagPrivileges = $this->policyService->getAllPrivilegesByType(SubtreeTagPrivilege::class); + } catch (\Exception $e) { + throw new \RuntimeException(sprintf('Failed to determine SubtreeTag privileges: %s', $e->getMessage()), 1729180655, $e); + } + foreach ($subtreeTagPrivileges as $privilege) { + if (!$privilege->isGranted()) { + $restrictedSubtreeTags[] = SubtreeTag::fromString($privilege->getParsedMatcher()); + } + } + return new VisibilityConstraints( + SubtreeTags::fromArray($restrictedSubtreeTags) + ); + } + + public function getWorkspacePrivilege(WorkspaceName $workspaceName, WorkspacePrivilegeType $privilegeType): Privilege + { + if ($this->securityContext->areAuthorizationChecksDisabled()) { + return Privilege::granted(); + } + $user = $this->userService->getCurrentUser(); + if ($user === null) { + return Privilege::denied('No user is authenticated'); + } + $workspacePermissions = $this->workspaceService->getWorkspacePermissionsForUser($this->contentRepositoryId, $workspaceName, $user); + return match ($privilegeType) { + WorkspacePrivilegeType::READ_NODES => $workspacePermissions->read ? Privilege::granted() : Privilege::denied(sprintf('User "%s" (id: %s) has no read permission for workspace "%s"', $user->getLabel(), $user->getId()->value, $workspaceName->value)), + }; + } + + public function getCommandPrivilege(CommandInterface $command): Privilege + { + if ($this->securityContext->areAuthorizationChecksDisabled()) { + return Privilege::granted(); + } + // TODO handle: + // ChangeBaseWorkspace + // CreateRootWorkspace + // DeleteWorkspace + // DiscardIndividualNodesFromWorkspace + // DiscardWorkspace + // PublishWorkspace + // PublishIndividualNodesFromWorkspace + // RebaseWorkspace + if ($command instanceof CreateWorkspace) { + $baseWorkspacePermissions = $this->getWorkspacePermissionsForAuthenticatedUser($command->baseWorkspaceName); + if ($baseWorkspacePermissions === null || !$baseWorkspacePermissions->write) { + return Privilege::denied(sprintf('no write permissions on base workspace "%s"', $command->baseWorkspaceName->value)); + } + return Privilege::granted(); + } + if (!$command instanceof RebasableToOtherWorkspaceInterface) { + return Privilege::granted(); + } + $user = $this->userService->getCurrentUser(); + if ($user === null) { + return Privilege::denied('No user is authenticated'); + } + $workspacePermissions = $this->workspaceService->getWorkspacePermissionsForUser($this->contentRepositoryId, $command->getWorkspaceName(), $user); + return $workspacePermissions->write ? Privilege::granted() : Privilege::denied(sprintf('User "%s" (id: %s) has no write permission for workspace "%s"', $user->getLabel(), $user->getId()->value, $command->getWorkspaceName()->value)); + } + + private function getWorkspacePermissionsForAuthenticatedUser(WorkspaceName $workspaceName): ?WorkspacePermissions + { + $user = $this->userService->getCurrentUser(); + if ($user === null) { + return null; + } + return $this->workspaceService->getWorkspacePermissionsForUser($this->contentRepositoryId, $workspaceName, $user); + } +} diff --git a/Neos.Neos/Classes/ContentRepositoryAuthProvider/ContentRepositoryAuthProviderFactory.php b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProviderFactory.php similarity index 89% rename from Neos.Neos/Classes/ContentRepositoryAuthProvider/ContentRepositoryAuthProviderFactory.php rename to Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProviderFactory.php index 63f84ae439e..d1535c50275 100644 --- a/Neos.Neos/Classes/ContentRepositoryAuthProvider/ContentRepositoryAuthProviderFactory.php +++ b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProviderFactory.php @@ -2,9 +2,8 @@ declare(strict_types=1); -namespace Neos\Neos\ContentRepositoryAuthProvider; +namespace Neos\Neos\Security\ContentRepositoryAuthProvider; -use Neos\ContentRepository\Core\SharedModel\Auth\AuthProviderInterface; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepositoryRegistry\Factory\AuthProvider\AuthProviderFactoryInterface; use Neos\Flow\Annotations as Flow; diff --git a/Neos.Neos/Classes/TypeConverter/NodeAddressToNodeConverter.php b/Neos.Neos/Classes/TypeConverter/NodeAddressToNodeConverter.php index c8744bb5eda..4157629043d 100644 --- a/Neos.Neos/Classes/TypeConverter/NodeAddressToNodeConverter.php +++ b/Neos.Neos/Classes/TypeConverter/NodeAddressToNodeConverter.php @@ -15,7 +15,6 @@ */ use Neos\ContentRepository\Core\Projection\ContentGraph\Node; -use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Annotations as Flow; @@ -59,13 +58,7 @@ public function convertFrom( ) { $nodeAddress = NodeAddress::fromJsonString($source); $contentRepository = $this->contentRepositoryRegistry->get($nodeAddress->contentRepositoryId); - $subgraph = $contentRepository->getContentGraph($nodeAddress->workspaceName) - ->getSubgraph( - $nodeAddress->dimensionSpacePoint, - $nodeAddress->workspaceName->isLive() - ? VisibilityConstraints::frontend() - : VisibilityConstraints::withoutRestrictions() - ); + $subgraph = $contentRepository->getContentSubgraph($nodeAddress->workspaceName, $nodeAddress->dimensionSpacePoint); return $subgraph->findNodeById($nodeAddress->aggregateId); } diff --git a/Neos.Neos/Classes/View/FusionExceptionView.php b/Neos.Neos/Classes/View/FusionExceptionView.php index fd6814acf20..564304dbda0 100644 --- a/Neos.Neos/Classes/View/FusionExceptionView.php +++ b/Neos.Neos/Classes/View/FusionExceptionView.php @@ -123,7 +123,7 @@ public function render(): ResponseInterface|StreamInterface $site, WorkspaceName::forLive(), $dimensionSpacePoint, - VisibilityConstraints::frontend() + VisibilityConstraints::default() ); } catch (WorkspaceDoesNotExist | \RuntimeException) { return $this->renderErrorWelcomeScreen(); diff --git a/Neos.Neos/Configuration/Settings.ContentRepositoryRegistry.yaml b/Neos.Neos/Configuration/Settings.ContentRepositoryRegistry.yaml index c088a067cbe..ea085721623 100644 --- a/Neos.Neos/Configuration/Settings.ContentRepositoryRegistry.yaml +++ b/Neos.Neos/Configuration/Settings.ContentRepositoryRegistry.yaml @@ -4,7 +4,7 @@ Neos: 'default': authProvider: - factoryObjectName: Neos\Neos\ContentRepositoryAuthProvider\ContentRepositoryAuthProviderFactory + factoryObjectName: Neos\Neos\Security\ContentRepositoryAuthProvider\ContentRepositoryAuthProviderFactory projections: 'Neos.Neos:DocumentUriPathProjection': From cf1273c006daeed0f754675e05f64be1e2e26259 Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Thu, 17 Oct 2024 18:15:47 +0200 Subject: [PATCH 003/214] Fix ContentRepositoryAuthProviderFactory --- .../ContentRepositoryAuthProviderFactory.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProviderFactory.php b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProviderFactory.php index d1535c50275..d708cb18ac8 100644 --- a/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProviderFactory.php +++ b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProviderFactory.php @@ -7,6 +7,8 @@ use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepositoryRegistry\Factory\AuthProvider\AuthProviderFactoryInterface; use Neos\Flow\Annotations as Flow; +use Neos\Flow\Security\Context as SecurityContext; +use Neos\Flow\Security\Policy\PolicyService; use Neos\Neos\Domain\Service\UserService; use Neos\Neos\Domain\Service\WorkspaceService; @@ -16,11 +18,13 @@ * @api */ #[Flow\Scope('singleton')] -final class ContentRepositoryAuthProviderFactory implements AuthProviderFactoryInterface +final readonly class ContentRepositoryAuthProviderFactory implements AuthProviderFactoryInterface { public function __construct( - private readonly UserService $userService, - private readonly WorkspaceService $workspaceService, + private UserService $userService, + private WorkspaceService $workspaceService, + private SecurityContext $securityContext, + private PolicyService $policyService, ) { } @@ -29,6 +33,6 @@ public function __construct( */ public function build(ContentRepositoryId $contentRepositoryId, array $options): ContentRepositoryAuthProvider { - return new ContentRepositoryAuthProvider($contentRepositoryId, $this->userService, $this->workspaceService); + return new ContentRepositoryAuthProvider($contentRepositoryId, $this->userService, $this->workspaceService, $this->securityContext, $this->policyService); } } From b1fe6c68cca50bfb0d0cf06464712723c497dcff Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Fri, 18 Oct 2024 13:11:10 +0200 Subject: [PATCH 004/214] Simplify `AuthProviderInterface::getWorkspacePrivilege()` --- .../Classes/ContentRepository.php | 2 +- .../Auth/AuthProviderInterface.php | 2 +- .../SharedModel/Auth/StaticAuthProvider.php | 2 +- .../Auth/WorkspacePrivilegeType.php | 23 ------------------- .../Bootstrap/Helpers/FakeAuthProvider.php | 2 +- .../ContentRepositoryAuthProvider.php | 9 ++++---- 6 files changed, 9 insertions(+), 31 deletions(-) delete mode 100644 Neos.ContentRepository.Core/Classes/SharedModel/Auth/WorkspacePrivilegeType.php diff --git a/Neos.ContentRepository.Core/Classes/ContentRepository.php b/Neos.ContentRepository.Core/Classes/ContentRepository.php index 77dce980c42..b6d5448e188 100644 --- a/Neos.ContentRepository.Core/Classes/ContentRepository.php +++ b/Neos.ContentRepository.Core/Classes/ContentRepository.php @@ -249,7 +249,7 @@ public function resetProjectionState(string $projectionClassName): void */ public function getContentGraph(WorkspaceName $workspaceName): ContentGraphInterface { - $privilege = $this->authProvider->getWorkspacePrivilege($workspaceName, WorkspacePrivilegeType::READ_NODES); + $privilege = $this->authProvider->getReadNodesFromWorkspacePrivilege($workspaceName); if (!$privilege->granted) { // TODO more specific exception throw new \RuntimeException(sprintf('Read access denied for workspace "%s": %s', $workspaceName->value, $privilege->message ?? ''), 1729014760); diff --git a/Neos.ContentRepository.Core/Classes/SharedModel/Auth/AuthProviderInterface.php b/Neos.ContentRepository.Core/Classes/SharedModel/Auth/AuthProviderInterface.php index b01147cf727..98b488bd420 100644 --- a/Neos.ContentRepository.Core/Classes/SharedModel/Auth/AuthProviderInterface.php +++ b/Neos.ContentRepository.Core/Classes/SharedModel/Auth/AuthProviderInterface.php @@ -15,7 +15,7 @@ interface AuthProviderInterface { public function getUserId(): UserId; - public function getWorkspacePrivilege(WorkspaceName $workspaceName, WorkspacePrivilegeType $privilegeType): Privilege; + public function getReadNodesFromWorkspacePrivilege(WorkspaceName $workspaceName): Privilege; public function getVisibilityConstraints(WorkspaceName $workspaceName): VisibilityConstraints; diff --git a/Neos.ContentRepository.Core/Classes/SharedModel/Auth/StaticAuthProvider.php b/Neos.ContentRepository.Core/Classes/SharedModel/Auth/StaticAuthProvider.php index 4b49b9bcea3..b7b8f5a3b86 100644 --- a/Neos.ContentRepository.Core/Classes/SharedModel/Auth/StaticAuthProvider.php +++ b/Neos.ContentRepository.Core/Classes/SharedModel/Auth/StaticAuthProvider.php @@ -30,7 +30,7 @@ public function getVisibilityConstraints(WorkspaceName $workspaceName): Visibili return VisibilityConstraints::default(); } - public function getWorkspacePrivilege(WorkspaceName $workspaceName, WorkspacePrivilegeType $privilegeType): Privilege + public function getReadNodesFromWorkspacePrivilege(WorkspaceName $workspaceName): Privilege { return Privilege::granted(); } diff --git a/Neos.ContentRepository.Core/Classes/SharedModel/Auth/WorkspacePrivilegeType.php b/Neos.ContentRepository.Core/Classes/SharedModel/Auth/WorkspacePrivilegeType.php deleted file mode 100644 index c08d6aa4d46..00000000000 --- a/Neos.ContentRepository.Core/Classes/SharedModel/Auth/WorkspacePrivilegeType.php +++ /dev/null @@ -1,23 +0,0 @@ -securityContext->areAuthorizationChecksDisabled()) { return Privilege::granted(); @@ -78,9 +78,7 @@ public function getWorkspacePrivilege(WorkspaceName $workspaceName, WorkspacePri return Privilege::denied('No user is authenticated'); } $workspacePermissions = $this->workspaceService->getWorkspacePermissionsForUser($this->contentRepositoryId, $workspaceName, $user); - return match ($privilegeType) { - WorkspacePrivilegeType::READ_NODES => $workspacePermissions->read ? Privilege::granted() : Privilege::denied(sprintf('User "%s" (id: %s) has no read permission for workspace "%s"', $user->getLabel(), $user->getId()->value, $workspaceName->value)), - }; + return $workspacePermissions->read ? Privilege::granted() : Privilege::denied(sprintf('User "%s" (id: %s) has no read permission for workspace "%s"', $user->getLabel(), $user->getId()->value, $workspaceName->value)); } public function getCommandPrivilege(CommandInterface $command): Privilege @@ -104,6 +102,9 @@ public function getCommandPrivilege(CommandInterface $command): Privilege } return Privilege::granted(); } + // Note: We check against the {@see RebasableToOtherWorkspaceInterface} because that is implemented by all + // commands that interact with nodes on a content stream. With that it's likely that we don't have to adjust the + // code if we were to add new commands in the future if (!$command instanceof RebasableToOtherWorkspaceInterface) { return Privilege::granted(); } From 3dbf15cfb880b825b6a2a0d25433363d28573d11 Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Fri, 18 Oct 2024 13:11:38 +0200 Subject: [PATCH 005/214] Always grant read access to `live` workspace --- .../ContentRepositoryAuthProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php index 2666e9f768b..cc9e59012d0 100644 --- a/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php +++ b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php @@ -75,7 +75,7 @@ public function getReadNodesFromWorkspacePrivilege(WorkspaceName $workspaceName) } $user = $this->userService->getCurrentUser(); if ($user === null) { - return Privilege::denied('No user is authenticated'); + return $workspaceName->isLive() ? Privilege::granted() : Privilege::denied('No user is authenticated'); } $workspacePermissions = $this->workspaceService->getWorkspacePermissionsForUser($this->contentRepositoryId, $workspaceName, $user); return $workspacePermissions->read ? Privilege::granted() : Privilege::denied(sprintf('User "%s" (id: %s) has no read permission for workspace "%s"', $user->getLabel(), $user->getId()->value, $workspaceName->value)); From ea2a9d5706f9e86f4f5595f1e70c964d6be80a39 Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Fri, 18 Oct 2024 13:13:04 +0200 Subject: [PATCH 006/214] Support CR specific subtree tag privileges ```yaml privilegeTargets: 'Neos\Neos\Security\Authorization\Privilege\SubtreeTagPrivilege': 'Some.Package:FooInAllContentRepositories': matcher: 'foo' 'Some.Package:BarInDefaultContentRepository': matcher: 'default:bar' ``` --- .../ContentRepositoryAuthProvider.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php index cc9e59012d0..1123cf07e64 100644 --- a/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php +++ b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php @@ -59,9 +59,17 @@ public function getVisibilityConstraints(WorkspaceName $workspaceName): Visibili throw new \RuntimeException(sprintf('Failed to determine SubtreeTag privileges: %s', $e->getMessage()), 1729180655, $e); } foreach ($subtreeTagPrivileges as $privilege) { - if (!$privilege->isGranted()) { - $restrictedSubtreeTags[] = SubtreeTag::fromString($privilege->getParsedMatcher()); + if ($privilege->isGranted()) { + continue; } + $subtreeTag = $privilege->getParsedMatcher(); + if (str_contains($subtreeTag, ':')) { + [$contentRepositoryId, $subtreeTag] = explode(':', $subtreeTag); + if ($this->contentRepositoryId->value !== $contentRepositoryId) { + continue; + } + } + $restrictedSubtreeTags[] = SubtreeTag::fromString($subtreeTag); } return new VisibilityConstraints( SubtreeTags::fromArray($restrictedSubtreeTags) From 6bf2e73222ecb1263298afbc695d37026d0005ee Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Fri, 18 Oct 2024 15:47:36 +0200 Subject: [PATCH 007/214] Rename `AuthProviderInterface::getUserId()` to `getAuthenticatedUserId()` and make return type nullable --- Neos.ContentRepository.Core/Classes/ContentRepository.php | 4 ++-- .../Classes/SharedModel/Auth/AuthProviderInterface.php | 2 +- .../Classes/SharedModel/Auth/StaticAuthProvider.php | 2 +- .../Behavior/Features/Bootstrap/Helpers/FakeAuthProvider.php | 4 ++-- .../ContentRepositoryAuthProvider.php | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/ContentRepository.php b/Neos.ContentRepository.Core/Classes/ContentRepository.php index b6d5448e188..7f2b6ab77c0 100644 --- a/Neos.ContentRepository.Core/Classes/ContentRepository.php +++ b/Neos.ContentRepository.Core/Classes/ContentRepository.php @@ -38,7 +38,7 @@ use Neos\ContentRepository\Core\Projection\ProjectionStatuses; use Neos\ContentRepository\Core\Projection\WithMarkStaleInterface; use Neos\ContentRepository\Core\SharedModel\Auth\AuthProviderInterface; -use Neos\ContentRepository\Core\SharedModel\Auth\WorkspacePrivilegeType; +use Neos\ContentRepository\Core\SharedModel\Auth\UserId; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryStatus; use Neos\ContentRepository\Core\SharedModel\Exception\WorkspaceDoesNotExist; @@ -111,7 +111,7 @@ public function handle(CommandInterface $command): CommandResult $eventsToPublish = $this->commandBus->handle($command, $this->commandHandlingDependencies); // TODO meaningful exception message - $initiatingUserId = $this->authProvider->getUserId(); + $initiatingUserId = $this->authProvider->getAuthenticatedUserId() ?? UserId::forSystemUser(); $initiatingTimestamp = $this->clock->now()->format(\DateTimeInterface::ATOM); // Add "initiatingUserId" and "initiatingTimestamp" metadata to all events. diff --git a/Neos.ContentRepository.Core/Classes/SharedModel/Auth/AuthProviderInterface.php b/Neos.ContentRepository.Core/Classes/SharedModel/Auth/AuthProviderInterface.php index 98b488bd420..84890012d9d 100644 --- a/Neos.ContentRepository.Core/Classes/SharedModel/Auth/AuthProviderInterface.php +++ b/Neos.ContentRepository.Core/Classes/SharedModel/Auth/AuthProviderInterface.php @@ -13,7 +13,7 @@ */ interface AuthProviderInterface { - public function getUserId(): UserId; + public function getAuthenticatedUserId(): ?UserId; public function getReadNodesFromWorkspacePrivilege(WorkspaceName $workspaceName): Privilege; diff --git a/Neos.ContentRepository.Core/Classes/SharedModel/Auth/StaticAuthProvider.php b/Neos.ContentRepository.Core/Classes/SharedModel/Auth/StaticAuthProvider.php index b7b8f5a3b86..893c6967573 100644 --- a/Neos.ContentRepository.Core/Classes/SharedModel/Auth/StaticAuthProvider.php +++ b/Neos.ContentRepository.Core/Classes/SharedModel/Auth/StaticAuthProvider.php @@ -20,7 +20,7 @@ public function __construct( ) { } - public function getUserId(): UserId + public function getAuthenticatedUserId(): UserId { return $this->userId; } diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Helpers/FakeAuthProvider.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Helpers/FakeAuthProvider.php index e056ae14d54..163da1395ad 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Helpers/FakeAuthProvider.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Helpers/FakeAuthProvider.php @@ -21,9 +21,9 @@ public static function setUserId(UserId $userId): void self::$userId = $userId; } - public function getUserId(): UserId + public function getAuthenticatedUserId(): ?UserId { - return self::$userId ?? UserId::forSystemUser(); + return self::$userId ?? null; } public function getVisibilityConstraints(WorkspaceName $workspaceName): VisibilityConstraints diff --git a/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php index 1123cf07e64..7b073144476 100644 --- a/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php +++ b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php @@ -37,11 +37,11 @@ public function __construct( ) { } - public function getUserId(): UserId + public function getAuthenticatedUserId(): ?UserId { $user = $this->userService->getCurrentUser(); if ($user === null) { - return UserId::forSystemUser(); + return null; } return UserId::fromString($user->getId()->value); } From bf57af6d351550860be19f28b8318889fc9d2836 Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Sat, 19 Oct 2024 13:16:45 +0200 Subject: [PATCH 008/214] Extract permission evaluation from `ContentRepositoryAuthProvider` to new `ContentRepositoryAuthorizationService` singleton --- .../src/Domain/Repository/ContentSubgraph.php | 15 ++ .../Repository/ContentSubhypergraph.php | 15 ++ .../Classes/ContentRepository.php | 4 +- .../Classes/ContentRepositoryReadModel.php | 3 - .../ContentGraph/ContentSubgraphInterface.php | 2 + .../ContentGraph/VisibilityConstraints.php | 5 + .../Classes/SharedModel/Auth/Privilege.php | 10 +- .../SharedModel/Auth/StaticAuthProvider.php | 4 +- .../Bootstrap/Helpers/FakeAuthProvider.php | 5 +- .../Classes/Service/EventMigrationService.php | 15 +- .../ContentSubgraphWithRuntimeCaches.php | 5 + .../Classes/Controller/UsageController.php | 9 +- .../Classes/Command/CrCommandController.php | 1 + .../Command/WorkspaceCommandController.php | 40 +++--- .../Controller/Frontend/NodeController.php | 4 + .../Domain/Model/WorkspacePermissions.php | 48 +++++-- .../Classes/Domain/Model/WorkspaceRole.php | 10 +- .../Domain/Model/WorkspaceRoleAssignment.php | 10 +- .../Domain/Model/WorkspaceRoleAssignments.php | 3 +- .../Domain/Model/WorkspaceRoleSubject.php | 35 +++-- .../Domain/Model/WorkspaceRoleSubjects.php | 50 +++++++ .../Domain/Service/WorkspaceService.php | 135 +++++++++--------- .../ContentRepositoryAuthorizationService.php | 127 ++++++++++++++++ .../Privilege/SubtreeTagPrivilege.php | 37 ++++- .../Privilege/SubtreeTagPrivilegeSubject.php | 6 +- .../ContentRepositoryAuthProvider.php | 124 ++++++++-------- .../ContentRepositoryAuthProviderFactory.php | 8 +- Neos.Neos/Configuration/Policy.yaml | 11 ++ .../Bootstrap/WorkspaceServiceTrait.php | 23 ++- .../Controller/WorkspaceController.php | 10 +- 30 files changed, 549 insertions(+), 225 deletions(-) create mode 100644 Neos.Neos/Classes/Domain/Model/WorkspaceRoleSubjects.php create mode 100644 Neos.Neos/Classes/Security/Authorization/ContentRepositoryAuthorizationService.php diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php index 810a095069f..72b74631867 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php @@ -122,6 +122,21 @@ public function getVisibilityConstraints(): VisibilityConstraints return $this->visibilityConstraints; } + public function withVisibilityConstraints(VisibilityConstraints $newVisibilityConstraints): self + { + return new self( + $this->contentRepositoryId, + $this->workspaceName, + $this->contentStreamId, + $this->dimensionSpacePoint, + $newVisibilityConstraints, + $this->dbal, + $this->nodeFactory, + $this->nodeTypeManager, + $this->nodeQueryBuilder->tableNames, + ); + } + public function findChildNodes(NodeAggregateId $parentNodeAggregateId, FindChildNodesFilter $filter): Nodes { $queryBuilder = $this->buildChildNodesQuery($parentNodeAggregateId, $filter); diff --git a/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/ContentSubhypergraph.php b/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/ContentSubhypergraph.php index 8b312166d2a..994a880dae7 100644 --- a/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/ContentSubhypergraph.php +++ b/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/ContentSubhypergraph.php @@ -106,6 +106,21 @@ public function getVisibilityConstraints(): VisibilityConstraints return $this->visibilityConstraints; } + public function withVisibilityConstraints(VisibilityConstraints $newVisibilityConstraints): self + { + return new self( + $this->contentRepositoryId, + $this->contentStreamId, + $this->workspaceName, + $this->dimensionSpacePoint, + $newVisibilityConstraints, + $this->dbal, + $this->nodeFactory, + $this->nodeTypeManager, + $this->tableNamePrefix, + ); + } + public function findNodeById(NodeAggregateId $nodeAggregateId): ?Node { $query = HypergraphQuery::create($this->contentStreamId, $this->tableNamePrefix); diff --git a/Neos.ContentRepository.Core/Classes/ContentRepository.php b/Neos.ContentRepository.Core/Classes/ContentRepository.php index 7f2b6ab77c0..ad566c64655 100644 --- a/Neos.ContentRepository.Core/Classes/ContentRepository.php +++ b/Neos.ContentRepository.Core/Classes/ContentRepository.php @@ -104,7 +104,7 @@ public function handle(CommandInterface $command): CommandResult { $privilege = $this->authProvider->getCommandPrivilege($command); if (!$privilege->granted) { - throw new \RuntimeException(sprintf('Command "%s" was denied: %s', $command::class, $privilege->message), 1729086686); + throw new \RuntimeException(sprintf('Command "%s" was denied: %s', $command::class, $privilege->reason), 1729086686); } // the commands only calculate which events they want to have published, but do not do the // publishing themselves @@ -252,7 +252,7 @@ public function getContentGraph(WorkspaceName $workspaceName): ContentGraphInter $privilege = $this->authProvider->getReadNodesFromWorkspacePrivilege($workspaceName); if (!$privilege->granted) { // TODO more specific exception - throw new \RuntimeException(sprintf('Read access denied for workspace "%s": %s', $workspaceName->value, $privilege->message ?? ''), 1729014760); + throw new \RuntimeException(sprintf('Read access denied for workspace "%s": %s', $workspaceName->value, $privilege->reason ?? ''), 1729014760); } return $this->getContentRepositoryReadModel()->getContentGraphByWorkspaceName($workspaceName); } diff --git a/Neos.ContentRepository.Core/Classes/ContentRepositoryReadModel.php b/Neos.ContentRepository.Core/Classes/ContentRepositoryReadModel.php index 631d827e624..26be912f0cd 100644 --- a/Neos.ContentRepository.Core/Classes/ContentRepositoryReadModel.php +++ b/Neos.ContentRepository.Core/Classes/ContentRepositoryReadModel.php @@ -16,9 +16,6 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphInterface; use Neos\ContentRepository\Core\Projection\ProjectionStateInterface; -use Neos\ContentRepository\Core\Projection\WithMarkStaleInterface; -use Neos\ContentRepository\Core\SharedModel\Auth\AuthProviderInterface; -use Neos\ContentRepository\Core\SharedModel\Auth\WorkspacePrivilegeType; use Neos\ContentRepository\Core\SharedModel\Exception\WorkspaceDoesNotExist; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStream; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentSubgraphInterface.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentSubgraphInterface.php index c47c0771932..d2df7ce2dd7 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentSubgraphInterface.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentSubgraphInterface.php @@ -57,6 +57,8 @@ public function getDimensionSpacePoint(): DimensionSpacePoint; public function getVisibilityConstraints(): VisibilityConstraints; + public function withVisibilityConstraints(VisibilityConstraints $newVisibilityConstraints): self; + /** * Find a single node by its aggregate id * diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/VisibilityConstraints.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/VisibilityConstraints.php index 7271e3634a3..598b9945a15 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/VisibilityConstraints.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/VisibilityConstraints.php @@ -53,6 +53,11 @@ public static function default(): VisibilityConstraints return new self(SubtreeTags::fromStrings('disabled')); } + public function withAddedSubtreeTag(SubtreeTag $subtreeTag): self + { + return new self($this->tagConstraints->merge(SubtreeTags::fromArray([$subtreeTag]))); + } + /** * @return array */ diff --git a/Neos.ContentRepository.Core/Classes/SharedModel/Auth/Privilege.php b/Neos.ContentRepository.Core/Classes/SharedModel/Auth/Privilege.php index e7746a7dfe4..cfe8798273b 100644 --- a/Neos.ContentRepository.Core/Classes/SharedModel/Auth/Privilege.php +++ b/Neos.ContentRepository.Core/Classes/SharedModel/Auth/Privilege.php @@ -22,17 +22,17 @@ { private function __construct( public bool $granted, - public ?string $message, + public string $reason, ) { } - public static function granted(): self + public static function granted(string $reason): self { - return new self(true, null); + return new self(true, $reason); } - public static function denied(string $message): self + public static function denied(string $reason): self { - return new self(false, $message); + return new self(false, $reason); } } diff --git a/Neos.ContentRepository.Core/Classes/SharedModel/Auth/StaticAuthProvider.php b/Neos.ContentRepository.Core/Classes/SharedModel/Auth/StaticAuthProvider.php index 893c6967573..a44a4e61db1 100644 --- a/Neos.ContentRepository.Core/Classes/SharedModel/Auth/StaticAuthProvider.php +++ b/Neos.ContentRepository.Core/Classes/SharedModel/Auth/StaticAuthProvider.php @@ -32,11 +32,11 @@ public function getVisibilityConstraints(WorkspaceName $workspaceName): Visibili public function getReadNodesFromWorkspacePrivilege(WorkspaceName $workspaceName): Privilege { - return Privilege::granted(); + return Privilege::granted(self::class . ' always grants privileges'); } public function getCommandPrivilege(CommandInterface $command): Privilege { - return Privilege::granted(); + return Privilege::granted(self::class . ' always grants privileges'); } } diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Helpers/FakeAuthProvider.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Helpers/FakeAuthProvider.php index 163da1395ad..c2cb6b8a84d 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Helpers/FakeAuthProvider.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Helpers/FakeAuthProvider.php @@ -9,7 +9,6 @@ use Neos\ContentRepository\Core\SharedModel\Auth\Privilege; use Neos\ContentRepository\Core\SharedModel\Auth\UserId; use Neos\ContentRepository\Core\SharedModel\Auth\AuthProviderInterface; -use Neos\ContentRepository\Core\SharedModel\Auth\WorkspacePrivilegeType; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; final class FakeAuthProvider implements AuthProviderInterface @@ -33,11 +32,11 @@ public function getVisibilityConstraints(WorkspaceName $workspaceName): Visibili public function getReadNodesFromWorkspacePrivilege(WorkspaceName $workspaceName): Privilege { - return Privilege::granted(); + return Privilege::granted(self::class . ' always grants privileges'); } public function getCommandPrivilege(CommandInterface $command): Privilege { - return Privilege::granted(); + return Privilege::granted(self::class . ' always grants privileges'); } } diff --git a/Neos.ContentRepositoryRegistry/Classes/Service/EventMigrationService.php b/Neos.ContentRepositoryRegistry/Classes/Service/EventMigrationService.php index 9cd7d315f80..a747c1611ba 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Service/EventMigrationService.php +++ b/Neos.ContentRepositoryRegistry/Classes/Service/EventMigrationService.php @@ -589,27 +589,32 @@ public function migrateWorkspaceMetadataToWorkspaceService(\Closure $outputFn): } catch (UniqueConstraintViolationException) { $outputFn(' Metadata already exists'); } - $roleAssignment = []; + $roleAssignments = []; if ($workspaceName->isLive()) { - $roleAssignment = [ + $roleAssignments[] = [ 'subject_type' => WorkspaceRoleSubjectType::GROUP->value, 'subject' => 'Neos.Neos:LivePublisher', 'role' => WorkspaceRole::COLLABORATOR->value, ]; + $roleAssignments[] = [ + 'subject_type' => WorkspaceRoleSubjectType::GROUP->value, + 'subject' => 'Neos.Neos:Everybody', + 'role' => WorkspaceRole::VIEWER->value, + ]; } elseif ($isInternalWorkspace) { - $roleAssignment = [ + $roleAssignments[] = [ 'subject_type' => WorkspaceRoleSubjectType::GROUP->value, 'subject' => 'Neos.Neos:AbstractEditor', 'role' => WorkspaceRole::COLLABORATOR->value, ]; } elseif ($isPrivateWorkspace) { - $roleAssignment = [ + $roleAssignments[] = [ 'subject_type' => WorkspaceRoleSubjectType::USER->value, 'subject' => $workspaceOwner, 'role' => WorkspaceRole::COLLABORATOR->value, ]; } - if ($roleAssignment !== []) { + foreach ($roleAssignments as $roleAssignment) { try { $this->connection->insert('neos_neos_workspace_role', [ 'content_repository_id' => $this->contentRepositoryId->value, diff --git a/Neos.ContentRepositoryRegistry/Classes/SubgraphCachingInMemory/ContentSubgraphWithRuntimeCaches.php b/Neos.ContentRepositoryRegistry/Classes/SubgraphCachingInMemory/ContentSubgraphWithRuntimeCaches.php index c26bcef38d1..f9f88c2e164 100644 --- a/Neos.ContentRepositoryRegistry/Classes/SubgraphCachingInMemory/ContentSubgraphWithRuntimeCaches.php +++ b/Neos.ContentRepositoryRegistry/Classes/SubgraphCachingInMemory/ContentSubgraphWithRuntimeCaches.php @@ -77,6 +77,11 @@ public function getVisibilityConstraints(): VisibilityConstraints return $this->wrappedContentSubgraph->getVisibilityConstraints(); } + public function withVisibilityConstraints(VisibilityConstraints $newVisibilityConstraints): self + { + return new self($this->wrappedContentSubgraph->withVisibilityConstraints($newVisibilityConstraints), $this->subgraphCachePool); + } + public function findChildNodes(NodeAggregateId $parentNodeAggregateId, FindChildNodesFilter $filter): Nodes { if (!self::isFilterEmpty($filter)) { diff --git a/Neos.Media.Browser/Classes/Controller/UsageController.php b/Neos.Media.Browser/Classes/Controller/UsageController.php index 4e75aa55aa1..5d11138f12c 100644 --- a/Neos.Media.Browser/Classes/Controller/UsageController.php +++ b/Neos.Media.Browser/Classes/Controller/UsageController.php @@ -25,6 +25,7 @@ use Neos\Neos\Domain\Service\NodeTypeNameFactory; use Neos\Neos\Domain\Service\WorkspaceService; use Neos\Neos\FrontendRouting\SiteDetection\SiteDetectionResult; +use Neos\Neos\Security\Authorization\ContentRepositoryAuthorizationService; use Neos\Neos\Service\UserService; use Neos\Neos\AssetUsage\Dto\AssetUsageReference; @@ -65,6 +66,12 @@ class UsageController extends ActionController */ protected $workspaceService; + /** + * @Flow\Inject + * @var ContentRepositoryAuthorizationService + */ + protected $contentRepositoryAuthorizationService; + /** * Get Related Nodes for an asset * @@ -103,7 +110,7 @@ public function relatedNodesAction(AssetInterface $asset) ); $nodeType = $nodeAggregate ? $contentRepository->getNodeTypeManager()->getNodeType($nodeAggregate->nodeTypeName) : null; - $workspacePermissions = $this->workspaceService->getWorkspacePermissionsForUser( + $workspacePermissions = $this->contentRepositoryAuthorizationService->getWorkspacePermissionsForUser( $currentContentRepositoryId, $usage->getWorkspaceName(), $currentUser diff --git a/Neos.Neos/Classes/Command/CrCommandController.php b/Neos.Neos/Classes/Command/CrCommandController.php index a6c2f0606ad..12fc64ac987 100644 --- a/Neos.Neos/Classes/Command/CrCommandController.php +++ b/Neos.Neos/Classes/Command/CrCommandController.php @@ -122,6 +122,7 @@ public function importCommand(string $path, string $contentRepository = 'default // set the live-workspace title to (implicitly) create the metadata record for this workspace $this->workspaceService->setWorkspaceTitle($contentRepositoryId, WorkspaceName::forLive(), WorkspaceTitle::fromString('Live workspace')); $this->workspaceService->assignWorkspaceRole($contentRepositoryId, WorkspaceName::forLive(), WorkspaceRoleAssignment::createForGroup('Neos.Neos:LivePublisher', WorkspaceRole::COLLABORATOR)); + $this->workspaceService->assignWorkspaceRole($contentRepositoryId, WorkspaceName::forLive(), WorkspaceRoleAssignment::createForGroup('Neos.Neos:Everybody', WorkspaceRole::VIEWER)); $this->outputLine('Done'); } diff --git a/Neos.Neos/Classes/Command/WorkspaceCommandController.php b/Neos.Neos/Classes/Command/WorkspaceCommandController.php index 2fdd87931c2..d4d9f2688ef 100644 --- a/Neos.Neos/Classes/Command/WorkspaceCommandController.php +++ b/Neos.Neos/Classes/Command/WorkspaceCommandController.php @@ -255,6 +255,7 @@ public function setDescriptionCommand(string $workspace, string $newDescription, * * Without explicit workspace roles, only administrators can change the corresponding workspace. * With this command, a user or group (represented by a Flow role identifier) can be granted one of the two roles: + * - viewer: Can read from the workspace * - collaborator: Can read from and write to the workspace * - manager: Can read from and write to the workspace and manage it (i.e. change metadata & role assignments) * @@ -284,30 +285,21 @@ public function assignRoleCommand(string $workspace, string $subject, string $ro default => throw new \InvalidArgumentException(sprintf('type must be "group" or "user", given "%s"', $type), 1728398802), }; $workspaceRole = match ($role) { + 'viewer' => WorkspaceRole::VIEWER, 'collaborator' => WorkspaceRole::COLLABORATOR, 'manager' => WorkspaceRole::MANAGER, - default => throw new \InvalidArgumentException(sprintf('role must be "collaborator" or "manager", given "%s"', $role), 1728398880), + default => throw new \InvalidArgumentException(sprintf('role must be "viewer", "collaborator" or "manager", given "%s"', $role), 1728398880), }; - if ($subjectType === WorkspaceRoleSubjectType::USER) { - $neosUser = $this->userService->getUser($subject); - if ($neosUser === null) { - $this->outputLine('The user "%s" specified as subject does not exist', [$subject]); - $this->quit(1); - } - $roleSubject = WorkspaceRoleSubject::fromString($neosUser->getId()->value); - } else { - $roleSubject = WorkspaceRoleSubject::fromString($subject); - } + $roleSubject = $this->buildWorkspaceRoleSubject($subjectType, $subject); $this->workspaceService->assignWorkspaceRole( $contentRepositoryId, $workspaceName, WorkspaceRoleAssignment::create( - $subjectType, $roleSubject, $workspaceRole ) ); - $this->outputLine('Assigned role "%s" to subject "%s" for workspace "%s"', [$workspaceRole->value, $roleSubject->value, $workspaceName->value]); + $this->outputLine('Assigned role "%s" to subject "%s" for workspace "%s"', [$workspaceRole->value, $roleSubject, $workspaceName->value]); } /** @@ -331,11 +323,10 @@ public function unassignRoleCommand(string $workspace, string $subject, string $ 'user' => WorkspaceRoleSubjectType::USER, default => throw new \InvalidArgumentException(sprintf('type must be "group" or "user", given "%s"', $type), 1728398802), }; - $roleSubject = WorkspaceRoleSubject::fromString($subject); + $roleSubject = $this->buildWorkspaceRoleSubject($subjectType, $subject); $this->workspaceService->unassignWorkspaceRole( $contentRepositoryId, $workspaceName, - $subjectType, $roleSubject, ); $this->outputLine('Removed role assignment from subject "%s" for workspace "%s"', [$roleSubject->value, $workspaceName->value]); @@ -524,7 +515,7 @@ public function showCommand(string $workspace, string $contentRepository = 'defa return; } $this->output->outputTable(array_map(static fn (WorkspaceRoleAssignment $assignment) => [ - $assignment->subjectType->value, + $assignment->subject->type->value, $assignment->subject->value, $assignment->role->value, ], iterator_to_array($workspaceRoleAssignments)), [ @@ -533,4 +524,21 @@ public function showCommand(string $workspace, string $contentRepository = 'defa 'Role', ]); } + + // ----------------------- + + private function buildWorkspaceRoleSubject(WorkspaceRoleSubjectType $subjectType, string $usernameOrRoleIdentifier): WorkspaceRoleSubject + { + if ($subjectType === WorkspaceRoleSubjectType::USER) { + $neosUser = $this->userService->getUser($usernameOrRoleIdentifier); + if ($neosUser === null) { + $this->outputLine('The user "%s" specified as subject does not exist', [$usernameOrRoleIdentifier]); + $this->quit(1); + } + $roleSubject = WorkspaceRoleSubject::createForUser($neosUser->getId()); + } else { + $roleSubject = WorkspaceRoleSubject::createForGroup($usernameOrRoleIdentifier); + } + return $roleSubject; + } } diff --git a/Neos.Neos/Classes/Controller/Frontend/NodeController.php b/Neos.Neos/Classes/Controller/Frontend/NodeController.php index 36fba3aacfe..9d5756d5fba 100644 --- a/Neos.Neos/Classes/Controller/Frontend/NodeController.php +++ b/Neos.Neos/Classes/Controller/Frontend/NodeController.php @@ -14,12 +14,14 @@ namespace Neos\Neos\Controller\Frontend; +use Neos\ContentRepository\Core\Feature\SubtreeTagging\Dto\SubtreeTag; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentSubgraphInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindClosestNodeFilter; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindSubtreeFilter; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\Projection\ContentGraph\Nodes; use Neos\ContentRepository\Core\Projection\ContentGraph\Subtree; +use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; @@ -195,6 +197,8 @@ public function showAction(string $node): void $contentRepository = $this->contentRepositoryRegistry->get($nodeAddress->contentRepositoryId); $uncachedSubgraph = $contentRepository->getContentSubgraph($nodeAddress->workspaceName, $nodeAddress->dimensionSpacePoint); + // todo document + $uncachedSubgraph = $uncachedSubgraph->withVisibilityConstraints($uncachedSubgraph->getVisibilityConstraints()->withAddedSubtreeTag(SubtreeTag::disabled())); $subgraph = new ContentSubgraphWithRuntimeCaches($uncachedSubgraph, $this->subgraphCachePool); $nodeInstance = $subgraph->findNodeById($nodeAddress->aggregateId); diff --git a/Neos.Neos/Classes/Domain/Model/WorkspacePermissions.php b/Neos.Neos/Classes/Domain/Model/WorkspacePermissions.php index faf543259cf..cdb2461f6bb 100644 --- a/Neos.Neos/Classes/Domain/Model/WorkspacePermissions.php +++ b/Neos.Neos/Classes/Domain/Model/WorkspacePermissions.php @@ -5,9 +5,10 @@ namespace Neos\Neos\Domain\Model; use Neos\Flow\Annotations as Flow; +use Neos\Neos\Security\Authorization\ContentRepositoryAuthorizationService; /** - * Calculated permissions a specific user has on a workspace + * Evaluated permissions a specific user has on a workspace, usually evaluated by the {@see ContentRepositoryAuthorizationService} * * - read: Permission to read data from the corresponding workspace (e.g. get hold of and traverse the content graph) * - write: Permission to write to the corresponding workspace, including publishing a derived workspace to it @@ -22,29 +23,48 @@ * @param bool $read Permission to read data from the corresponding workspace (e.g. get hold of and traverse the content graph) * @param bool $write Permission to write to the corresponding workspace, including publishing a derived workspace to it * @param bool $manage Permission to change the metadata and roles of the corresponding workspace (e.g. change description/title or add/remove workspace roles) + * @param string $reason Human-readable explanation for why this permission was evaluated {@see getReason()} */ - public static function create( - bool $read, - bool $write, - bool $manage, - ): self { - return new self($read, $write, $manage); + private function __construct( + public bool $read, + public bool $write, + public bool $manage, + private string $reason, + ) { } /** * @param bool $read Permission to read data from the corresponding workspace (e.g. get hold of and traverse the content graph) * @param bool $write Permission to write to the corresponding workspace, including publishing a derived workspace to it * @param bool $manage Permission to change the metadata and roles of the corresponding workspace (e.g. change description/title or add/remove workspace roles) + * @param string $reason Human-readable explanation for why this permission was evaluated {@see getReason()} */ - private function __construct( - public bool $read, - public bool $write, - public bool $manage, - ) { + public static function create( + bool $read, + bool $write, + bool $manage, + string $reason, + ): self { + return new self($read, $write, $manage, $reason); + } + + public static function all(string $reason): self + { + return new self(true, true, true, $reason); + } + + public static function manage(string $reason): self + { + return new self(false, false, true, $reason); + } + + public static function none(string $reason): self + { + return new self(false, false, false, $reason); } - public static function all(): self + public function getReason(): string { - return new self(true, true, true); + return $this->reason; } } diff --git a/Neos.Neos/Classes/Domain/Model/WorkspaceRole.php b/Neos.Neos/Classes/Domain/Model/WorkspaceRole.php index 898c961f5a0..a36269e22b8 100644 --- a/Neos.Neos/Classes/Domain/Model/WorkspaceRole.php +++ b/Neos.Neos/Classes/Domain/Model/WorkspaceRole.php @@ -12,6 +12,11 @@ */ enum WorkspaceRole : string { + /** + * Can read from the workspace + */ + case VIEWER = 'VIEWER'; + /** * Can read from and write to the workspace */ @@ -30,8 +35,9 @@ public function isAtLeast(self $role): bool private function specificity(): int { return match ($this) { - self::COLLABORATOR => 1, - self::MANAGER => 2, + self::VIEWER => 1, + self::COLLABORATOR => 2, + self::MANAGER => 3, }; } } diff --git a/Neos.Neos/Classes/Domain/Model/WorkspaceRoleAssignment.php b/Neos.Neos/Classes/Domain/Model/WorkspaceRoleAssignment.php index fd7d5a7896f..f8206c8d2ce 100644 --- a/Neos.Neos/Classes/Domain/Model/WorkspaceRoleAssignment.php +++ b/Neos.Neos/Classes/Domain/Model/WorkspaceRoleAssignment.php @@ -15,25 +15,22 @@ final readonly class WorkspaceRoleAssignment { private function __construct( - public WorkspaceRoleSubjectType $subjectType, public WorkspaceRoleSubject $subject, public WorkspaceRole $role, ) { } public static function create( - WorkspaceRoleSubjectType $subjectType, WorkspaceRoleSubject $subject, WorkspaceRole $role, ): self { - return new self($subjectType, $subject, $role); + return new self($subject, $role); } public static function createForUser(UserId $userId, WorkspaceRole $role): self { return new self( - WorkspaceRoleSubjectType::USER, - WorkspaceRoleSubject::fromString($userId->value), + WorkspaceRoleSubject::createForUser($userId), $role ); } @@ -41,8 +38,7 @@ public static function createForUser(UserId $userId, WorkspaceRole $role): self public static function createForGroup(string $flowRoleIdentifier, WorkspaceRole $role): self { return new self( - WorkspaceRoleSubjectType::GROUP, - WorkspaceRoleSubject::fromString($flowRoleIdentifier), + WorkspaceRoleSubject::createForGroup($flowRoleIdentifier), $role ); } diff --git a/Neos.Neos/Classes/Domain/Model/WorkspaceRoleAssignments.php b/Neos.Neos/Classes/Domain/Model/WorkspaceRoleAssignments.php index 82dc1eb4a3f..a63eb23b899 100644 --- a/Neos.Neos/Classes/Domain/Model/WorkspaceRoleAssignments.php +++ b/Neos.Neos/Classes/Domain/Model/WorkspaceRoleAssignments.php @@ -5,7 +5,6 @@ namespace Neos\Neos\Domain\Model; use Neos\Flow\Annotations as Flow; -use Traversable; /** * A set of {@see WorkspaceRoleAssignment} instances @@ -39,7 +38,7 @@ public function isEmpty(): bool return $this->assignments === []; } - public function getIterator(): Traversable + public function getIterator(): \Traversable { yield from $this->assignments; } diff --git a/Neos.Neos/Classes/Domain/Model/WorkspaceRoleSubject.php b/Neos.Neos/Classes/Domain/Model/WorkspaceRoleSubject.php index fb80329b09d..c9255bcfe5f 100644 --- a/Neos.Neos/Classes/Domain/Model/WorkspaceRoleSubject.php +++ b/Neos.Neos/Classes/Domain/Model/WorkspaceRoleSubject.php @@ -12,28 +12,47 @@ * @api */ #[Flow\Proxy(false)] -final readonly class WorkspaceRoleSubject implements \JsonSerializable +final readonly class WorkspaceRoleSubject { - public function __construct( - public string $value + private function __construct( + public WorkspaceRoleSubjectType $type, + public string $value, ) { if (preg_match('/^[\p{L}\p{P}\d .]{1,200}$/u', $this->value) !== 1) { throw new \InvalidArgumentException(sprintf('"%s" is not a valid workspace role subject.', $value), 1728384932); } } - public static function fromString(string $value): self + public static function createForUser(UserId $userId): self { - return new self($value); + return new self( + WorkspaceRoleSubjectType::USER, + $userId->value, + ); } - public function jsonSerialize(): string + public static function createForGroup(string $flowRoleIdentifier): self { - return $this->value; + return new self( + WorkspaceRoleSubjectType::GROUP, + $flowRoleIdentifier, + ); + } + + public static function create( + WorkspaceRoleSubjectType $type, + string $value, + ): self { + return new self($type, $value); } public function equals(self $other): bool { - return $this->value === $other->value; + return $this->type === $other->type && $this->value === $other->value; + } + + public function __toString(): string + { + return "{$this->type->value}: {$this->value}"; } } diff --git a/Neos.Neos/Classes/Domain/Model/WorkspaceRoleSubjects.php b/Neos.Neos/Classes/Domain/Model/WorkspaceRoleSubjects.php new file mode 100644 index 00000000000..5af2a3b2b36 --- /dev/null +++ b/Neos.Neos/Classes/Domain/Model/WorkspaceRoleSubjects.php @@ -0,0 +1,50 @@ + + * @api + */ +#[Flow\Proxy(false)] +final readonly class WorkspaceRoleSubjects implements \IteratorAggregate, \Countable +{ + /** + * @var array + */ + private array $subjects; + + private function __construct(WorkspaceRoleSubject ...$subjects) + { + $this->subjects = $subjects; + } + + /** + * @param array $subjects + */ + public static function fromArray(array $subjects): self + { + return new self(...$subjects); + } + + public function isEmpty(): bool + { + return $this->subjects === []; + } + + public function getIterator(): \Traversable + { + yield from $this->subjects; + } + + public function count(): int + { + return count($this->subjects); + } +} diff --git a/Neos.Neos/Classes/Domain/Service/WorkspaceService.php b/Neos.Neos/Classes/Domain/Service/WorkspaceService.php index 1431181f52e..4e9b2e892e8 100644 --- a/Neos.Neos/Classes/Domain/Service/WorkspaceService.php +++ b/Neos.Neos/Classes/Domain/Service/WorkspaceService.php @@ -28,19 +28,19 @@ use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Annotations as Flow; use Neos\Flow\Security\Context as SecurityContext; -use Neos\Flow\Security\Exception\NoSuchRoleException; use Neos\Neos\Domain\Model\User; use Neos\Neos\Domain\Model\UserId; use Neos\Neos\Domain\Model\WorkspaceClassification; use Neos\Neos\Domain\Model\WorkspaceDescription; use Neos\Neos\Domain\Model\WorkspaceMetadata; -use Neos\Neos\Domain\Model\WorkspacePermissions; use Neos\Neos\Domain\Model\WorkspaceRole; use Neos\Neos\Domain\Model\WorkspaceRoleAssignment; use Neos\Neos\Domain\Model\WorkspaceRoleAssignments; use Neos\Neos\Domain\Model\WorkspaceRoleSubject; +use Neos\Neos\Domain\Model\WorkspaceRoleSubjects; use Neos\Neos\Domain\Model\WorkspaceRoleSubjectType; use Neos\Neos\Domain\Model\WorkspaceTitle; +use Neos\Neos\Security\Authorization\ContentRepositoryAuthorizationService; /** * Central authority to interact with Content Repository Workspaces within Neos @@ -57,7 +57,6 @@ final class WorkspaceService public function __construct( private readonly ContentRepositoryRegistry $contentRepositoryRegistry, - private readonly UserService $userService, private readonly Connection $dbal, private readonly SecurityContext $securityContext, ) { @@ -87,7 +86,7 @@ public function getWorkspaceMetadata(ContentRepositoryId $contentRepositoryId, W */ public function setWorkspaceTitle(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, WorkspaceTitle $newWorkspaceTitle): void { - // TODO check workspace permissions -> $this->getWorkspacePermissionsForUser($contentRepositoryId, $workspaceName, $this->userService->getCurrentUser()); + // TODO check workspace permissions $this->updateWorkspaceMetadata($contentRepositoryId, $workspaceName, [ 'title' => $newWorkspaceTitle->value, ]); @@ -98,6 +97,7 @@ public function setWorkspaceTitle(ContentRepositoryId $contentRepositoryId, Work */ public function setWorkspaceDescription(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, WorkspaceDescription $newWorkspaceDescription): void { + // TODO check workspace permissions $this->updateWorkspaceMetadata($contentRepositoryId, $workspaceName, [ 'description' => $newWorkspaceDescription->value, ]); @@ -201,7 +201,7 @@ public function assignWorkspaceRole(ContentRepositoryId $contentRepositoryId, Wo $this->dbal->insert(self::TABLE_NAME_WORKSPACE_ROLE, [ 'content_repository_id' => $contentRepositoryId->value, 'workspace_name' => $workspaceName->value, - 'subject_type' => $assignment->subjectType->value, + 'subject_type' => $assignment->subject->type->value, 'subject' => $assignment->subject->value, 'role' => $assignment->role->value, ]); @@ -217,14 +217,14 @@ public function assignWorkspaceRole(ContentRepositoryId $contentRepositoryId, Wo * * @see self::assignWorkspaceRole() */ - public function unassignWorkspaceRole(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, WorkspaceRoleSubjectType $subjectType, WorkspaceRoleSubject $subject): void + public function unassignWorkspaceRole(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, WorkspaceRoleSubject $subject): void { $this->requireWorkspace($contentRepositoryId, $workspaceName); try { $affectedRows = $this->dbal->delete(self::TABLE_NAME_WORKSPACE_ROLE, [ 'content_repository_id' => $contentRepositoryId->value, 'workspace_name' => $workspaceName->value, - 'subject_type' => $subjectType->value, + 'subject_type' => $subject->type->value, 'subject' => $subject->value, ]); } catch (DbalException $e) { @@ -238,7 +238,7 @@ public function unassignWorkspaceRole(ContentRepositoryId $contentRepositoryId, /** * Get all role assignments for the specified workspace * - * NOTE: This should never be used to evaluate permissions, instead {@see self::getWorkspacePermissionsForUser()} should be used! + * NOTE: This should never be used to evaluate permissions, instead {@see ContentRepositoryAuthorizationService::getWorkspacePermissionsForUser()} should be used! */ public function getWorkspaceRoleAssignments(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName): WorkspaceRoleAssignments { @@ -262,37 +262,73 @@ public function getWorkspaceRoleAssignments(ContentRepositoryId $contentReposito } return WorkspaceRoleAssignments::fromArray( array_map(static fn (array $row) => WorkspaceRoleAssignment::create( - WorkspaceRoleSubjectType::from($row['subject_type']), - WorkspaceRoleSubject::fromString($row['subject']), + WorkspaceRoleSubject::create( + WorkspaceRoleSubjectType::from($row['subject_type']), + $row['subject'], + ), WorkspaceRole::from($row['role']), ), $rows) ); } /** - * Determines the permission the given user has for the specified workspace {@see WorkspacePermissions} + * Get the role with the most privileges for the specified {@see WorkspaceRoleSubjects} on workspace $workspaceName + * + * NOTE: This should never be used to evaluate permissions, instead {@see ContentRepositoryAuthorizationService::getWorkspacePermissionsForUser()} should be used! */ - public function getWorkspacePermissionsForUser(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, User $user): WorkspacePermissions + public function getMostPrivilegedWorkspaceRoleForSubjects(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, WorkspaceRoleSubjects $subjects): ?WorkspaceRole { - try { - $userRoles = array_keys($this->userService->getAllRoles($user)); - } catch (NoSuchRoleException $e) { - throw new \RuntimeException(sprintf('Failed to determine roles for user "%s", check your package dependencies: %s', $user->getId()->value, $e->getMessage()), 1727084881, $e); + $tableRole = self::TABLE_NAME_WORKSPACE_ROLE; + $query = <<type === WorkspaceRoleSubjectType::GROUP) { + $groupSubjectValues[] = $subject->value; + } else { + $userSubjectValues[] = $subject->value; + } } - $workspaceMetadata = $this->loadWorkspaceMetadata($contentRepositoryId, $workspaceName); - if ($workspaceMetadata !== null && $workspaceMetadata->ownerUserId !== null && $workspaceMetadata->ownerUserId->equals($user->getId())) { - return WorkspacePermissions::all(); + try { + $role = $this->dbal->fetchOne($query, [ + 'contentRepositoryId' => $contentRepositoryId->value, + 'workspaceName' => $workspaceName->value, + 'userSubjectType' => WorkspaceRoleSubjectType::USER->value, + 'userSubjectValues' => $userSubjectValues, + 'groupSubjectType' => WorkspaceRoleSubjectType::GROUP->value, + 'groupSubjectValues' => $groupSubjectValues, + ], [ + 'userSubjectValues' => ArrayParameterType::STRING, + 'groupSubjectValues' => ArrayParameterType::STRING, + ]); + } catch (DbalException $e) { + throw new \RuntimeException(sprintf('Failed to load role for workspace "%s" (content repository "%s"): %e', $workspaceName->value, $contentRepositoryId->value, $e->getMessage()), 1729325871, $e); } - $userWorkspaceRole = $this->loadWorkspaceRoleOfUser($contentRepositoryId, $workspaceName, $user->getId(), $userRoles); - $userIsAdministrator = in_array('Neos.Neos:Administrator', $userRoles, true); - if ($userWorkspaceRole === null) { - return WorkspacePermissions::create(false, false, $userIsAdministrator); + if ($role === false) { + return null; } - return WorkspacePermissions::create( - read: $userWorkspaceRole->isAtLeast(WorkspaceRole::COLLABORATOR), - write: $userWorkspaceRole->isAtLeast(WorkspaceRole::COLLABORATOR), - manage: $userIsAdministrator || $userWorkspaceRole->isAtLeast(WorkspaceRole::MANAGER), - ); + return WorkspaceRole::from($role); } /** @@ -437,49 +473,6 @@ private function findPrimaryWorkspaceNameForUser(ContentRepositoryId $contentRep return $workspaceName === false ? null : WorkspaceName::fromString($workspaceName); } - /** - * @param array $userRoles - */ - private function loadWorkspaceRoleOfUser(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, UserId $userId, array $userRoles): ?WorkspaceRole - { - $tableRole = self::TABLE_NAME_WORKSPACE_ROLE; - $query = <<dbal->fetchOne($query, [ - 'contentRepositoryId' => $contentRepositoryId->value, - 'workspaceName' => $workspaceName->value, - 'userSubjectType' => WorkspaceRoleSubjectType::USER->value, - 'userId' => $userId->value, - 'groupSubjectType' => WorkspaceRoleSubjectType::GROUP->value, - 'groupSubjects' => $userRoles, - ], [ - 'groupSubjects' => ArrayParameterType::STRING, - ]); - if ($role === false) { - return null; - } - return WorkspaceRole::from($role); - } - private function requireWorkspace(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName): Workspace { $workspace = $this->contentRepositoryRegistry diff --git a/Neos.Neos/Classes/Security/Authorization/ContentRepositoryAuthorizationService.php b/Neos.Neos/Classes/Security/Authorization/ContentRepositoryAuthorizationService.php new file mode 100644 index 00000000000..3d056cae5a3 --- /dev/null +++ b/Neos.Neos/Classes/Security/Authorization/ContentRepositoryAuthorizationService.php @@ -0,0 +1,127 @@ +workspaceService->getMostPrivilegedWorkspaceRoleForSubjects($contentRepositoryId, $workspaceName, WorkspaceRoleSubjects::fromArray($subjects)); + if ($userWorkspaceRole === null) { + return WorkspacePermissions::none("Anonymous user has no explicit role for workspace '{$workspaceName->value}'"); + } + return WorkspacePermissions::create( + read: $userWorkspaceRole->isAtLeast(WorkspaceRole::VIEWER), + write: $userWorkspaceRole->isAtLeast(WorkspaceRole::COLLABORATOR), + manage: $userWorkspaceRole->isAtLeast(WorkspaceRole::MANAGER), + reason: "Anonymous user has role '{$userWorkspaceRole->value}' for workspace '{$workspaceName->value}'", + ); + } + + /** + * Determines the permission the given user has for the specified workspace {@see WorkspacePermissions} + */ + public function getWorkspacePermissionsForUser(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, User $user): WorkspacePermissions + { + $workspaceMetadata = $this->workspaceService->getWorkspaceMetadata($contentRepositoryId, $workspaceName); + if ($workspaceMetadata->ownerUserId !== null && $workspaceMetadata->ownerUserId->equals($user->getId())) { + return WorkspacePermissions::all("User '{$user->getLabel()}' (id: {$user->getId()->value} is the owner of workspace '{$workspaceName->value}'"); + } + $userRoles = $this->rolesForUser($user); + $userIsAdministrator = array_key_exists(self::FLOW_ROLE_ADMINISTRATOR, $userRoles); + $subjects = array_map(WorkspaceRoleSubject::createForGroup(...), array_keys($userRoles)); + $subjects[] = WorkspaceRoleSubject::createForUser($user->getId()); + $userWorkspaceRole = $this->workspaceService->getMostPrivilegedWorkspaceRoleForSubjects($contentRepositoryId, $workspaceName, WorkspaceRoleSubjects::fromArray($subjects)); + if ($userWorkspaceRole === null) { + if ($userIsAdministrator) { + return WorkspacePermissions::manage("User '{$user->getLabel()}' (id: '{$user->getId()->value}') has no explicit role for workspace '{$workspaceName->value}' but is an Administrator"); + } + return WorkspacePermissions::none("User '{$user->getLabel()}' (id: '{$user->getId()->value}') has no explicit role for workspace '{$workspaceName->value}' and is no Administrator"); + } + return WorkspacePermissions::create( + read: $userWorkspaceRole->isAtLeast(WorkspaceRole::VIEWER), + write: $userWorkspaceRole->isAtLeast(WorkspaceRole::COLLABORATOR), + manage: $userIsAdministrator || $userWorkspaceRole->isAtLeast(WorkspaceRole::MANAGER), + reason: "User '{$user->getLabel()}' (id: '{$user->getId()->value}') has role '{$userWorkspaceRole->value}' for workspace '{$workspaceName->value}'" . ($userIsAdministrator ? ' and is an Administrator' : ' and is no Administrator'), + ); + } + + public function getVisibilityConstraintsForAnonymousUser(ContentRepositoryId $contentRepositoryId): VisibilityConstraints + { + $roles = array_map($this->policyService->getRole(...), [self::FLOW_ROLE_EVERYBODY, self::FLOW_ROLE_ANONYMOUS]); + return $this->visibilityConstraintsForRoles($contentRepositoryId, $roles); + } + + public function getVisibilityConstraintsForUser(ContentRepositoryId $contentRepositoryId, User $user): VisibilityConstraints + { + $userRoles = $this->rolesForUser($user); + return $this->visibilityConstraintsForRoles($contentRepositoryId, $userRoles); + } + + /** + * @param array $roles + */ + private function visibilityConstraintsForRoles(ContentRepositoryId $contentRepositoryId, array $roles): VisibilityConstraints + { + $restrictedSubtreeTags = []; + /** @var SubtreeTagPrivilege $privilege */ + foreach ($this->policyService->getAllPrivilegesByType(SubtreeTagPrivilege::class) as $privilege) { + if (!$this->privilegeManager->isGrantedForRoles($roles, SubtreeTagPrivilege::class, new SubtreeTagPrivilegeSubject($privilege->getSubtreeTag(), $contentRepositoryId))) { + $restrictedSubtreeTags[] = $privilege->getSubtreeTag(); + } + } + return new VisibilityConstraints(SubtreeTags::fromArray($restrictedSubtreeTags)); + } + + /** + * @return array + */ + private function rolesForUser(User $user): array + { + try { + $userRoles = $this->userService->getAllRoles($user); + } catch (NoSuchRoleException $e) { + throw new \RuntimeException("Failed to determine roles for user '{$user->getLabel()}' (id: '{$user->getId()->value}'), check your package dependencies: {$e->getMessage()}", 1727084881, $e); + } + return $userRoles; + } +} diff --git a/Neos.Neos/Classes/Security/Authorization/Privilege/SubtreeTagPrivilege.php b/Neos.Neos/Classes/Security/Authorization/Privilege/SubtreeTagPrivilege.php index 2b3d1f157d0..da4eb47357c 100644 --- a/Neos.Neos/Classes/Security/Authorization/Privilege/SubtreeTagPrivilege.php +++ b/Neos.Neos/Classes/Security/Authorization/Privilege/SubtreeTagPrivilege.php @@ -14,6 +14,8 @@ namespace Neos\Neos\Security\Authorization\Privilege; +use Neos\ContentRepository\Core\Feature\SubtreeTagging\Dto\SubtreeTag; +use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\Flow\Security\Authorization\Privilege\AbstractPrivilege; use Neos\Flow\Security\Authorization\Privilege\PrivilegeSubjectInterface; use Neos\Flow\Security\Exception\InvalidPrivilegeTypeException; @@ -23,6 +25,22 @@ */ class SubtreeTagPrivilege extends AbstractPrivilege { + private SubtreeTag|null $subtreeTagRuntimeCache = null; + private ContentRepositoryId|null $contentRepositoryIdRuntimeCache = null; + + private function initialize(): void + { + if ($this->subtreeTagRuntimeCache !== null) { + return; + } + $subtreeTag = $this->getParsedMatcher(); + if (str_contains($subtreeTag, ':')) { + [$contentRepositoryId, $subtreeTag] = explode(':', $subtreeTag); + $this->contentRepositoryIdRuntimeCache = ContentRepositoryId::fromString($contentRepositoryId); + } + $this->subtreeTagRuntimeCache = SubtreeTag::fromString($subtreeTag); + } + /** * Returns true, if this privilege covers the given subject * @@ -35,6 +53,23 @@ public function matchesSubject(PrivilegeSubjectInterface $subject): bool if (!$subject instanceof SubtreeTagPrivilegeSubject) { throw new InvalidPrivilegeTypeException(sprintf('Privileges of type "%s" only support subjects of type "%s" but we got a subject of type: "%s".', self::class, SubtreeTagPrivilegeSubject::class, get_class($subject)), 1729173985); } - return false; + $contentRepositoryId = $this->getContentRepositoryId(); + if ($contentRepositoryId !== null && $subject->contentRepositoryId !== null && !$contentRepositoryId->equals($subject->contentRepositoryId)) { + return false; + } + return $subject->subTreeTag->equals($this->getSubtreeTag()); + } + + public function getSubtreeTag(): SubtreeTag + { + $this->initialize(); + assert($this->subtreeTagRuntimeCache !== null); + return $this->subtreeTagRuntimeCache; + } + + public function getContentRepositoryId(): ?ContentRepositoryId + { + $this->initialize(); + return $this->contentRepositoryIdRuntimeCache; } } diff --git a/Neos.Neos/Classes/Security/Authorization/Privilege/SubtreeTagPrivilegeSubject.php b/Neos.Neos/Classes/Security/Authorization/Privilege/SubtreeTagPrivilegeSubject.php index df2cd33c5b6..a08a0a090aa 100644 --- a/Neos.Neos/Classes/Security/Authorization/Privilege/SubtreeTagPrivilegeSubject.php +++ b/Neos.Neos/Classes/Security/Authorization/Privilege/SubtreeTagPrivilegeSubject.php @@ -14,6 +14,8 @@ namespace Neos\Neos\Security\Authorization\Privilege; +use Neos\ContentRepository\Core\Feature\SubtreeTagging\Dto\SubtreeTag; +use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\Flow\Security\Authorization\Privilege\PrivilegeSubjectInterface; /** @@ -22,8 +24,8 @@ final readonly class SubtreeTagPrivilegeSubject implements PrivilegeSubjectInterface { public function __construct( - public string $subTreeTag, - public string|null $contentRepository = null, + public SubtreeTag $subTreeTag, + public ContentRepositoryId|null $contentRepositoryId = null, ) { } } diff --git a/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php index 7b073144476..6179994e815 100644 --- a/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php +++ b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php @@ -6,34 +6,39 @@ use Neos\ContentRepository\Core\CommandHandler\CommandInterface; use Neos\ContentRepository\Core\Feature\Common\RebasableToOtherWorkspaceInterface; -use Neos\ContentRepository\Core\Feature\SubtreeTagging\Dto\SubtreeTag; -use Neos\ContentRepository\Core\Feature\SubtreeTagging\Dto\SubtreeTags; +use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Command\CreateRootWorkspace; use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Command\CreateWorkspace; +use Neos\ContentRepository\Core\Feature\WorkspaceModification\Command\ChangeBaseWorkspace; +use Neos\ContentRepository\Core\Feature\WorkspaceModification\Command\DeleteWorkspace; +use Neos\ContentRepository\Core\Feature\WorkspacePublication\Command\DiscardIndividualNodesFromWorkspace; +use Neos\ContentRepository\Core\Feature\WorkspacePublication\Command\DiscardWorkspace; +use Neos\ContentRepository\Core\Feature\WorkspacePublication\Command\PublishIndividualNodesFromWorkspace; +use Neos\ContentRepository\Core\Feature\WorkspacePublication\Command\PublishWorkspace; +use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Command\RebaseWorkspace; use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\Core\SharedModel\Auth\AuthProviderInterface; use Neos\ContentRepository\Core\SharedModel\Auth\Privilege; use Neos\ContentRepository\Core\SharedModel\Auth\UserId; -use Neos\ContentRepository\Core\SharedModel\Auth\WorkspacePrivilegeType; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\Flow\Security\Context as SecurityContext; -use Neos\Flow\Security\Policy\PolicyService; use Neos\Neos\Domain\Model\WorkspacePermissions; use Neos\Neos\Domain\Service\UserService; -use Neos\Neos\Domain\Service\WorkspaceService; -use Neos\Neos\Security\Authorization\Privilege\SubtreeTagPrivilege; +use Neos\Neos\Security\Authorization\ContentRepositoryAuthorizationService; /** * @api */ final class ContentRepositoryAuthProvider implements AuthProviderInterface { + private const WORKSPACE_PERMISSION_WRITE = 'write'; + private const WORKSPACE_PERMISSION_MANAGE = 'manage'; + public function __construct( private readonly ContentRepositoryId $contentRepositoryId, private readonly UserService $userService, - private readonly WorkspaceService $workspaceService, + private readonly ContentRepositoryAuthorizationService $authorizationService, private readonly SecurityContext $securityContext, - private readonly PolicyService $policyService, ) { } @@ -51,85 +56,82 @@ public function getVisibilityConstraints(WorkspaceName $workspaceName): Visibili if ($this->securityContext->areAuthorizationChecksDisabled()) { return VisibilityConstraints::default(); } - $restrictedSubtreeTags = [SubtreeTag::disabled()]; - try { - /** @var array $subtreeTagPrivileges */ - $subtreeTagPrivileges = $this->policyService->getAllPrivilegesByType(SubtreeTagPrivilege::class); - } catch (\Exception $e) { - throw new \RuntimeException(sprintf('Failed to determine SubtreeTag privileges: %s', $e->getMessage()), 1729180655, $e); - } - foreach ($subtreeTagPrivileges as $privilege) { - if ($privilege->isGranted()) { - continue; - } - $subtreeTag = $privilege->getParsedMatcher(); - if (str_contains($subtreeTag, ':')) { - [$contentRepositoryId, $subtreeTag] = explode(':', $subtreeTag); - if ($this->contentRepositoryId->value !== $contentRepositoryId) { - continue; - } - } - $restrictedSubtreeTags[] = SubtreeTag::fromString($subtreeTag); + $user = $this->userService->getCurrentUser(); + if ($user === null) { + return $this->authorizationService->getVisibilityConstraintsForAnonymousUser($this->contentRepositoryId); } - return new VisibilityConstraints( - SubtreeTags::fromArray($restrictedSubtreeTags) - ); + return $this->authorizationService->getVisibilityConstraintsForUser($this->contentRepositoryId, $user); } public function getReadNodesFromWorkspacePrivilege(WorkspaceName $workspaceName): Privilege { if ($this->securityContext->areAuthorizationChecksDisabled()) { - return Privilege::granted(); + return Privilege::granted('Authorization checks are disabled'); } $user = $this->userService->getCurrentUser(); if ($user === null) { - return $workspaceName->isLive() ? Privilege::granted() : Privilege::denied('No user is authenticated'); + $workspacePermissions = $this->authorizationService->getWorkspacePermissionsForAnonymousUser($this->contentRepositoryId, $workspaceName); + } else { + $workspacePermissions = $this->authorizationService->getWorkspacePermissionsForUser($this->contentRepositoryId, $workspaceName, $user); } - $workspacePermissions = $this->workspaceService->getWorkspacePermissionsForUser($this->contentRepositoryId, $workspaceName, $user); - return $workspacePermissions->read ? Privilege::granted() : Privilege::denied(sprintf('User "%s" (id: %s) has no read permission for workspace "%s"', $user->getLabel(), $user->getId()->value, $workspaceName->value)); + return $workspacePermissions->read ? Privilege::granted($workspacePermissions->getReason()) : Privilege::denied($workspacePermissions->getReason()); } public function getCommandPrivilege(CommandInterface $command): Privilege { if ($this->securityContext->areAuthorizationChecksDisabled()) { - return Privilege::granted(); - } - // TODO handle: - // ChangeBaseWorkspace - // CreateRootWorkspace - // DeleteWorkspace - // DiscardIndividualNodesFromWorkspace - // DiscardWorkspace - // PublishWorkspace - // PublishIndividualNodesFromWorkspace - // RebaseWorkspace - if ($command instanceof CreateWorkspace) { - $baseWorkspacePermissions = $this->getWorkspacePermissionsForAuthenticatedUser($command->baseWorkspaceName); - if ($baseWorkspacePermissions === null || !$baseWorkspacePermissions->write) { - return Privilege::denied(sprintf('no write permissions on base workspace "%s"', $command->baseWorkspaceName->value)); - } - return Privilege::granted(); + return Privilege::granted('Authorization checks are disabled'); } + // Note: We check against the {@see RebasableToOtherWorkspaceInterface} because that is implemented by all // commands that interact with nodes on a content stream. With that it's likely that we don't have to adjust the // code if we were to add new commands in the future - if (!$command instanceof RebasableToOtherWorkspaceInterface) { - return Privilege::granted(); + if ($command instanceof RebasableToOtherWorkspaceInterface) { + return $this->requireWorkspacePermission($command->getWorkspaceName(), self::WORKSPACE_PERMISSION_WRITE); } - $user = $this->userService->getCurrentUser(); - if ($user === null) { - return Privilege::denied('No user is authenticated'); + + if ($command instanceof CreateRootWorkspace) { + return Privilege::denied('Creation of root workspaces is currently only allowed with disabled authorization checks'); + } + + if ($command instanceof ChangeBaseWorkspace) { + $workspacePermissions = $this->getWorkspacePermissionsForCurrentUser($command->workspaceName); + if (!$workspacePermissions->manage) { + return Privilege::denied("Missing 'manage' permissions for workspace '{$command->workspaceName->value}': {$workspacePermissions->getReason()}"); + } + $baseWorkspacePermissions = $this->getWorkspacePermissionsForCurrentUser($command->baseWorkspaceName); + if (!$baseWorkspacePermissions->write) { + return Privilege::denied("Missing 'write' permissions for base workspace '{$command->baseWorkspaceName->value}': {$baseWorkspacePermissions->getReason()}"); + } + return Privilege::granted("User has 'manage' permissions for workspace '{$command->workspaceName->value}' and 'write' permissions for base workspace '{$command->baseWorkspaceName->value}'"); } - $workspacePermissions = $this->workspaceService->getWorkspacePermissionsForUser($this->contentRepositoryId, $command->getWorkspaceName(), $user); - return $workspacePermissions->write ? Privilege::granted() : Privilege::denied(sprintf('User "%s" (id: %s) has no write permission for workspace "%s"', $user->getLabel(), $user->getId()->value, $command->getWorkspaceName()->value)); + return match ($command::class) { + CreateWorkspace::class => $this->requireWorkspacePermission($command->baseWorkspaceName, self::WORKSPACE_PERMISSION_WRITE), + DeleteWorkspace::class => $this->requireWorkspacePermission($command->workspaceName, self::WORKSPACE_PERMISSION_MANAGE), + DiscardWorkspace::class, + DiscardIndividualNodesFromWorkspace::class, + PublishWorkspace::class, + PublishIndividualNodesFromWorkspace::class, + RebaseWorkspace::class => $this->requireWorkspacePermission($command->workspaceName, self::WORKSPACE_PERMISSION_WRITE), + default => Privilege::granted('Command not restricted'), + }; } - private function getWorkspacePermissionsForAuthenticatedUser(WorkspaceName $workspaceName): ?WorkspacePermissions + private function requireWorkspacePermission(WorkspaceName $workspaceName, string $permission): Privilege + { + $workspacePermissions = $this->getWorkspacePermissionsForCurrentUser($workspaceName); + if (!$workspacePermissions->{$permission}) { + return Privilege::denied("Missing '{$permission}' permissions for workspace '{$workspaceName->value}': {$workspacePermissions->getReason()}"); + } + return Privilege::granted("User has '{$permission}' permissions for workspace '{$workspaceName->value}'"); + } + + private function getWorkspacePermissionsForCurrentUser(WorkspaceName $workspaceName): WorkspacePermissions { $user = $this->userService->getCurrentUser(); if ($user === null) { - return null; + return $this->authorizationService->getWorkspacePermissionsForAnonymousUser($this->contentRepositoryId, $workspaceName); } - return $this->workspaceService->getWorkspacePermissionsForUser($this->contentRepositoryId, $workspaceName, $user); + return $this->authorizationService->getWorkspacePermissionsForUser($this->contentRepositoryId, $workspaceName, $user); } } diff --git a/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProviderFactory.php b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProviderFactory.php index d708cb18ac8..54bfc97294c 100644 --- a/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProviderFactory.php +++ b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProviderFactory.php @@ -8,9 +8,8 @@ use Neos\ContentRepositoryRegistry\Factory\AuthProvider\AuthProviderFactoryInterface; use Neos\Flow\Annotations as Flow; use Neos\Flow\Security\Context as SecurityContext; -use Neos\Flow\Security\Policy\PolicyService; use Neos\Neos\Domain\Service\UserService; -use Neos\Neos\Domain\Service\WorkspaceService; +use Neos\Neos\Security\Authorization\ContentRepositoryAuthorizationService; /** * Implementation of the {@see AuthProviderFactoryInterface} in order to provide authentication and authorization for Content Repositories @@ -22,9 +21,8 @@ { public function __construct( private UserService $userService, - private WorkspaceService $workspaceService, + private ContentRepositoryAuthorizationService $contentRepositoryAuthorizationService, private SecurityContext $securityContext, - private PolicyService $policyService, ) { } @@ -33,6 +31,6 @@ public function __construct( */ public function build(ContentRepositoryId $contentRepositoryId, array $options): ContentRepositoryAuthProvider { - return new ContentRepositoryAuthProvider($contentRepositoryId, $this->userService, $this->workspaceService, $this->securityContext, $this->policyService); + return new ContentRepositoryAuthProvider($contentRepositoryId, $this->userService, $this->contentRepositoryAuthorizationService, $this->securityContext); } } diff --git a/Neos.Neos/Configuration/Policy.yaml b/Neos.Neos/Configuration/Policy.yaml index 8ccf44e7ce0..1cd85ab83c2 100644 --- a/Neos.Neos/Configuration/Policy.yaml +++ b/Neos.Neos/Configuration/Policy.yaml @@ -142,6 +142,12 @@ privilegeTargets: label: General access to the dimensions module matcher: 'administration/dimensions' + + 'Neos\Neos\Security\Authorization\Privilege\SubtreeTagPrivilege': + + 'Neos.Neos:ContentRepository.ReadDisabledNodes': + matcher: 'disabled' + roles: 'Neos.Flow:Everybody': @@ -229,6 +235,11 @@ roles: privilegeTarget: 'Neos.Neos:Backend.Module.Management' permission: GRANT + - + privilegeTarget: 'Neos.Neos:ContentRepository.ReadDisabledNodes' + permission: GRANT + + 'Neos.Neos:RestrictedEditor': label: Restricted Editor diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/WorkspaceServiceTrait.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/WorkspaceServiceTrait.php index 06916378f00..8106b291e13 100644 --- a/Neos.Neos/Tests/Behavior/Features/Bootstrap/WorkspaceServiceTrait.php +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/WorkspaceServiceTrait.php @@ -27,6 +27,7 @@ use Neos\Neos\Domain\Model\WorkspaceTitle; use Neos\Neos\Domain\Service\UserService; use Neos\Neos\Domain\Service\WorkspaceService; +use Neos\Neos\Security\Authorization\ContentRepositoryAuthorizationService; use PHPUnit\Framework\Assert; /** @@ -165,16 +166,15 @@ public function theWorkspaceShouldHaveTheFollowingMetadata($workspaceName, Table /** * @When the role :role is assigned to workspace :workspaceName for group :groupName - * @When the role :role is assigned to workspace :workspaceName for user :username + * @When the role :role is assigned to workspace :workspaceName for user :userId */ - public function theRoleIsAssignedToWorkspaceForGroupOrUser(string $role, string $workspaceName, string $groupName = null, string $username = null): void + public function theRoleIsAssignedToWorkspaceForGroupOrUser(string $role, string $workspaceName, string $groupName = null, string $userId = null): void { $this->tryCatchingExceptions(fn () => $this->getObject(WorkspaceService::class)->assignWorkspaceRole( $this->currentContentRepository->id, WorkspaceName::fromString($workspaceName), WorkspaceRoleAssignment::create( - $groupName !== null ? WorkspaceRoleSubjectType::GROUP : WorkspaceRoleSubjectType::USER, - WorkspaceRoleSubject::fromString($groupName ?? $username), + $groupName !== null ? WorkspaceRoleSubject::createForGroup($groupName) : WorkspaceRoleSubject::createForUser(UserId::fromString($userId)), WorkspaceRole::from($role) ) )); @@ -182,15 +182,14 @@ public function theRoleIsAssignedToWorkspaceForGroupOrUser(string $role, string /** * @When the role for group :groupName is unassigned from workspace :workspaceName - * @When the role for user :username is unassigned from workspace :workspaceName + * @When the role for user :userId is unassigned from workspace :workspaceName */ - public function theRoleIsUnassignedFromWorkspace(string $workspaceName, string $groupName = null, string $username = null): void + public function theRoleIsUnassignedFromWorkspace(string $workspaceName, string $groupName = null, string $userId = null): void { $this->tryCatchingExceptions(fn () => $this->getObject(WorkspaceService::class)->unassignWorkspaceRole( $this->currentContentRepository->id, WorkspaceName::fromString($workspaceName), - $groupName !== null ? WorkspaceRoleSubjectType::GROUP : WorkspaceRoleSubjectType::USER, - WorkspaceRoleSubject::fromString($groupName ?? $username), + $groupName !== null ? WorkspaceRoleSubject::createForGroup($groupName) : WorkspaceRoleSubject::createForUser(UserId::fromString($userId)), )); } @@ -201,7 +200,7 @@ public function theWorkspaceShouldHaveTheFollowingRoleAssignments($workspaceName { $workspaceAssignments = $this->getObject(WorkspaceService::class)->getWorkspaceRoleAssignments($this->currentContentRepository->id, WorkspaceName::fromString($workspaceName)); $actualAssignments = array_map(static fn (WorkspaceRoleAssignment $assignment) => [ - 'Subject type' => $assignment->subjectType->value, + 'Subject type' => $assignment->subject->type->value, 'Subject' => $assignment->subject->value, 'Role' => $assignment->role->value, ], iterator_to_array($workspaceAssignments)); @@ -214,12 +213,12 @@ public function theWorkspaceShouldHaveTheFollowingRoleAssignments($workspaceName public function theNeosUserShouldHaveThePermissionsForWorkspace(string $username, string $expectedPermissions, string $workspaceName): void { $user = $this->getObject(UserService::class)->getUser($username); - $permissions = $this->getObject(WorkspaceService::class)->getWorkspacePermissionsForUser( + $permissions = $this->getObject(ContentRepositoryAuthorizationService::class)->getWorkspacePermissionsForUser( $this->currentContentRepository->id, WorkspaceName::fromString($workspaceName), $user, ); - Assert::assertSame($expectedPermissions, implode(',', array_keys(array_filter((array)$permissions)))); + Assert::assertSame($expectedPermissions, implode(',', array_keys(array_filter(get_object_vars($permissions))))); } /** @@ -228,7 +227,7 @@ public function theNeosUserShouldHaveThePermissionsForWorkspace(string $username public function theNeosUserShouldHaveNoPermissionsForWorkspace(string $username, string $workspaceName): void { $user = $this->getObject(UserService::class)->getUser($username); - $permissions = $this->getObject(WorkspaceService::class)->getWorkspacePermissionsForUser( + $permissions = $this->getObject(ContentRepositoryAuthorizationService::class)->getWorkspacePermissionsForUser( $this->currentContentRepository->id, WorkspaceName::fromString($workspaceName), $user, diff --git a/Neos.Workspace.Ui/Classes/Controller/WorkspaceController.php b/Neos.Workspace.Ui/Classes/Controller/WorkspaceController.php index dac01978f21..72cd21ac82b 100644 --- a/Neos.Workspace.Ui/Classes/Controller/WorkspaceController.php +++ b/Neos.Workspace.Ui/Classes/Controller/WorkspaceController.php @@ -62,6 +62,7 @@ use Neos\Neos\FrontendRouting\NodeUriBuilderFactory; use Neos\Neos\FrontendRouting\SiteDetection\SiteDetectionResult; use Neos\Neos\PendingChangesProjection\ChangeFinder; +use Neos\Neos\Security\Authorization\ContentRepositoryAuthorizationService; use Neos\Neos\Utility\NodeTypeWithFallbackProvider; use Neos\Workspace\Ui\ViewModel\PendingChanges; use Neos\Workspace\Ui\ViewModel\WorkspaceListItem; @@ -103,6 +104,9 @@ class WorkspaceController extends AbstractModuleController #[Flow\Inject] protected WorkspaceService $workspaceService; + #[Flow\Inject] + protected ContentRepositoryAuthorizationService $contentRepositoryAuthorizationService; + /** * Display a list of unpublished content */ @@ -135,7 +139,7 @@ public function indexAction(): void $allWorkspaces = $contentRepository->findWorkspaces(); foreach ($allWorkspaces as $workspace) { $workspaceMetadata = $this->workspaceService->getWorkspaceMetadata($contentRepositoryId, $workspace->workspaceName); - $permissions = $this->workspaceService->getWorkspacePermissionsForUser($contentRepositoryId, $workspace->workspaceName, $currentUser); + $permissions = $this->contentRepositoryAuthorizationService->getWorkspacePermissionsForUser($contentRepositoryId, $workspace->workspaceName, $currentUser); if (!$permissions->read) { continue; } @@ -174,7 +178,7 @@ public function showAction(WorkspaceName $workspace): void $baseWorkspace = $contentRepository->findWorkspaceByName($workspaceObj->baseWorkspaceName); assert($baseWorkspace !== null); $baseWorkspaceMetadata = $this->workspaceService->getWorkspaceMetadata($contentRepositoryId, $baseWorkspace->workspaceName); - $baseWorkspacePermissions = $this->workspaceService->getWorkspacePermissionsForUser($contentRepositoryId, $baseWorkspace->workspaceName, $currentUser); + $baseWorkspacePermissions = $this->contentRepositoryAuthorizationService->getWorkspacePermissionsForUser($contentRepositoryId, $baseWorkspace->workspaceName, $currentUser); } $this->view->assignMultiple([ 'selectedWorkspace' => $workspaceObj, @@ -1030,7 +1034,7 @@ protected function prepareBaseWorkspaceOptions( if ($user === null) { continue; } - $permissions = $this->workspaceService->getWorkspacePermissionsForUser($contentRepository->id, $workspace->workspaceName, $user); + $permissions = $this->contentRepositoryAuthorizationService->getWorkspacePermissionsForUser($contentRepository->id, $workspace->workspaceName, $user); if (!$permissions->manage) { continue; } From 08193796b7ecf810b39b32dd34c17553b2f9e29f Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 20 Oct 2024 14:11:36 +0200 Subject: [PATCH 009/214] TASK: Introduce full behat test for FrontendNodeController --- .../Bootstrap/Features/NodeCreation.php | 2 +- .../Classes/Domain/Service/FusionService.php | 12 ++ .../Features/Bootstrap/DispatcherTrait.php | 103 ++++++++++++++++ .../Features/Bootstrap/FeatureContext.php | 1 + .../DefaultFusionRendering.feature | 113 ++++++++++++++++++ 5 files changed, 230 insertions(+), 1 deletion(-) create mode 100644 Neos.Neos/Tests/Behavior/Features/Bootstrap/DispatcherTrait.php create mode 100644 Neos.Neos/Tests/Behavior/Features/FrontendNodeController/DefaultFusionRendering.feature diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeCreation.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeCreation.php index d20ebdab535..5f4a1b8c699 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeCreation.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeCreation.php @@ -205,7 +205,7 @@ public function theFollowingCreateNodeAggregateWithNodeCommandsAreExecuted(Table if (isset($row['tetheredDescendantNodeAggregateIds'])) { $command = $command->withTetheredDescendantNodeAggregateIds(NodeAggregateIdsByNodePaths::fromJsonString($row['tetheredDescendantNodeAggregateIds'])); } - if (isset($row['nodeName'])) { + if (!empty($row['nodeName'])) { $command = $command->withNodeName(NodeName::fromString($row['nodeName'])); } $this->currentContentRepository->handle($command); diff --git a/Neos.Neos/Classes/Domain/Service/FusionService.php b/Neos.Neos/Classes/Domain/Service/FusionService.php index 528b8315a31..9573ef9befe 100644 --- a/Neos.Neos/Classes/Domain/Service/FusionService.php +++ b/Neos.Neos/Classes/Domain/Service/FusionService.php @@ -44,6 +44,16 @@ class FusionService */ protected $fusionConfigurationCache; + private ?FusionSourceCodeCollection $additionalFusionSourceCode = null; + + /** + * @deprecated fixme!!! + */ + public function unsafeSetAdditionalFusionSourceCodeToThisSingleton(string $additionalFusionSourceCode) + { + $this->additionalFusionSourceCode = FusionSourceCodeCollection::fromString($additionalFusionSourceCode); + } + public function createFusionConfigurationFromSite(Site $site): FusionConfiguration { return $this->fusionConfigurationCache->cacheFusionConfigurationBySite($site, function () use ($site) { @@ -56,6 +66,8 @@ public function createFusionConfigurationFromSite(Site $site): FusionConfigurati ) ->union( FusionSourceCodeCollection::tryFromPackageRootFusion($siteResourcesPackageKey) + )->union( + $this->additionalFusionSourceCode ?? FusionSourceCodeCollection::empty() ) ); }); diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/DispatcherTrait.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/DispatcherTrait.php new file mode 100644 index 00000000000..56a39fa4e9e --- /dev/null +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/DispatcherTrait.php @@ -0,0 +1,103 @@ + $className + * + * @return T + */ + abstract private function getObject(string $className): object; + + /** + * @BeforeScenario + */ + public function setupDispatcherTest(): void + { + $this->getObject(ContentCache::class)->flush(); + $this->response = null; + } + + /** + * @When the sites Fusion code is: + */ + public function iHaveTheFollowingFusionCodeForTheSite(PyStringNode $fusionCode) + { + $this->getObject( + FusionService::class + )->unsafeSetAdditionalFusionSourceCodeToThisSingleton( + $fusionCode->getRaw() + ); + // $fakeFusionService = new class ($original) extends \Neos\Neos\Domain\Service\FusionService + // { + // public function __construct( + // private \Neos\Neos\Domain\Service\FusionService $original, + // private \Neos\Fusion\Core\FusionSourceCode $additionalFusion + // ) { + // } + // public function createFusionConfigurationFromSite(\Neos\Neos\Domain\Model\Site $site): \Neos\Fusion\Core\FusionConfiguration + // { + // $this->original->createFusionConfigurationFromSite($site)-> ... doest work + // } + // }; + } + + /** + * @When I dispatch the following request :requestUri + */ + public function iDispatchTheFollowingRequest(string $requestUri) + { + $httpRequest = $this->getObject(ServerRequestFactoryInterface::class)->createServerRequest('GET', $requestUri); + + $this->response = $this->getObject(\Neos\Flow\Http\Middleware\MiddlewaresChain::class)->handle( + $httpRequest + ); + } + + /** + * @Then I expect the following response header: + */ + public function iExpectTheFollowingResponseHeader(PyStringNode $expectedResult): void + { + Assert::assertNotNull($this->response); + Assert::assertSame($expectedResult->getRaw(), $this->response->getBody()->getContents()); + } + + /** + * @Then I expect the following response: + */ + public function iExpectTheFollowingResponse(PyStringNode $expectedResult): void + { + Assert::assertNotNull($this->response); + Assert::assertEquals($expectedResult->getRaw(), str_replace("\r\n", "\n", Message::toString($this->response))); + } +} diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/FeatureContext.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/FeatureContext.php index 9d06ce491eb..b02048f437e 100644 --- a/Neos.Neos/Tests/Behavior/Features/Bootstrap/FeatureContext.php +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/FeatureContext.php @@ -39,6 +39,7 @@ class FeatureContext implements BehatContext use CRBehavioralTestsSubjectProvider; use RoutingTrait; use MigrationsTrait; + use DispatcherTrait; use FusionTrait; use ContentCacheTrait; diff --git a/Neos.Neos/Tests/Behavior/Features/FrontendNodeController/DefaultFusionRendering.feature b/Neos.Neos/Tests/Behavior/Features/FrontendNodeController/DefaultFusionRendering.feature new file mode 100644 index 00000000000..cb7689bf7cb --- /dev/null +++ b/Neos.Neos/Tests/Behavior/Features/FrontendNodeController/DefaultFusionRendering.feature @@ -0,0 +1,113 @@ +@flowEntities +Feature: Test the default Fusion rendering for a request + Background: + Given using no content dimensions + And using the following node types: + """yaml + 'Neos.ContentRepository:Root': {} + 'Neos.Neos:ContentCollection': {} + 'Neos.Neos:Content': {} + 'Neos.Neos:Sites': + superTypes: + 'Neos.ContentRepository:Root': true + 'Neos.Neos:Document': + properties: + title: + type: string + uriPathSegment: + type: string + 'Neos.Neos:Site': + superTypes: + 'Neos.Neos:Document': true + childNodes: + main: + type: 'Neos.Neos:ContentCollection' + 'Neos.Neos:Test.DocumentType': + superTypes: + 'Neos.Neos:Document': true + childNodes: + main: + type: 'Neos.Neos:ContentCollection' + 'Neos.Neos:Test.ContentType': + superTypes: + 'Neos.Neos:Content': true + properties: + text: + type: string + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + When the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | newContentStreamId | "cs-identifier" | + And I am in workspace "live" and dimension space point {} + And the command CreateRootNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "root" | + | nodeTypeName | "Neos.Neos:Sites" | + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | parentNodeAggregateId | nodeTypeName | initialPropertyValues | tetheredDescendantNodeAggregateIds | nodeName | + | a | root | Neos.Neos:Site | {"title": "Node a"} | {} | a | + | a1 | a | Neos.Neos:Test.DocumentType | {"uriPathSegment": "a1", "title": "Node a1"} | {"main": "a-tetherton" } | | + | a1a1 | a-tetherton | Neos.Neos:Test.ContentType | {"text": "my first text"} | {} | | + | a1a2 | a-tetherton | Neos.Neos:Test.ContentType | {"text": "my second text"} | {} | | + And A site exists for node name "a" and domain "http://localhost" + And the sites configuration is: + """yaml + Neos: + Neos: + sites: + 'a': + preset: default + uriPathSuffix: '' + contentDimensions: + resolver: + factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\NoopResolverFactory + """ + + Scenario: Default output + And the sites Fusion code is: + """fusion + prototype(Neos.Neos:Test.DocumentType) < prototype(Neos.Neos:Page) { + body { + content = Neos.Fusion:Component { + renderer = afx` + {String.chr(10)}title: {node.properties.title} + {String.chr(10)}children: + {String.chr(10)} + ` + } + } + } + prototype(Neos.Neos:Test.ContentType) < prototype(Neos.Neos:ContentComponent) { + text = Neos.Neos:Editable { + property = 'text' + } + + renderer = afx` + [{props.text}] + ` + } + """ + + When I dispatch the following request "/a1" + Then I expect the following response: + """ + HTTP/1.1 200 OK + Content-Type: text/html + X-Flow-Powered: Flow/dev Neos/dev + Content-Length: 486 + + + + Node a1 + title: Node a1 + children:
[my first text][my second text]
+ + """ From 58fceb8aa61a3029f2cd21807fcaaf6ced665b4f Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 21 Oct 2024 11:24:14 +0200 Subject: [PATCH 010/214] WIP --- .../Tests/Behavior/Features/Bootstrap/DispatcherTrait.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/DispatcherTrait.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/DispatcherTrait.php index 56a39fa4e9e..fa9a6d68f0e 100644 --- a/Neos.Neos/Tests/Behavior/Features/Bootstrap/DispatcherTrait.php +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/DispatcherTrait.php @@ -69,6 +69,11 @@ public function iHaveTheFollowingFusionCodeForTheSite(PyStringNode $fusionCode) // $this->original->createFusionConfigurationFromSite($site)-> ... doest work // } // }; + + // doesnt work as the packages base path cannot change ... we would need to create an actual package in /Packages as rescanPackages() will be invoked + // vfsStream::setup('packages'); + // $this->getObject(\Neos\Flow\Package\PackageManager::class)->createPackage('Vendor.Site', [], 'vfs://packages/'); + // file_put_contents('resource://Vendor.Site/Private/Fusion/Root.fusion', $fusionCode->getRaw()); } /** From 8a954495097c1b992a1786919599119dab607d4c Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Mon, 21 Oct 2024 13:53:46 +0200 Subject: [PATCH 011/214] WIP: FEATURE: Site import Resolves: #4448 --- .../Bootstrap/CrImportExportTrait.php | 209 ------------------ .../Features/Bootstrap/FeatureContext.php | 78 ------- .../Behavior/Features/Export/Export.feature | 43 ---- .../Behavior/Features/Import/Import.feature | 81 ------- .../Tests/Behavior/behat.yml.dist | 11 - .../src/Asset/AssetExporter.php | 5 +- .../src/ExportService.php | 63 ------ .../src/ExportServiceFactory.php | 39 ---- .../src/ImportService.php | 86 ------- .../src/ImportServiceFactory.php | 45 ---- .../src/ProcessingContext.php | 24 ++ .../src/ProcessorInterface.php | 13 +- .../src/ProcessorResult.php | 23 -- .../src/Processors.php | 37 ++++ .../src/Processors/AssetExportProcessor.php | 68 ++---- .../AssetRepositoryImportProcessor.php | 65 ++---- .../ContentRepositorySetupProcessor.php | 28 +++ .../src/Processors/EventExportProcessor.php | 52 ++--- .../Processors/EventStoreImportProcessor.php | 38 +--- .../Classes/LegacyMigrationService.php | 33 ++- .../Classes/NodeDataToAssetsProcessor.php | 44 +--- .../Classes/NodeDataToEventsProcessor.php | 82 +++---- .../Behavior/Bootstrap/FeatureContext.php | 38 ++-- .../Classes/Command/CrCommandController.php | 128 ----------- .../Classes/Command/SiteCommandController.php | 41 ++++ .../Import/DoctrineMigrateProcessor.php | 32 +++ .../Domain/Import/SiteCreationProcessor.php | 104 +++++++++ .../Domain/Service/SiteImportService.php | 53 +++++ .../Service/SiteImportServiceFactory.php | 67 ++++++ .../Classes/Domain/Service/SiteService.php | 2 +- 30 files changed, 535 insertions(+), 1097 deletions(-) delete mode 100644 Neos.ContentRepository.Export/Tests/Behavior/Features/Bootstrap/CrImportExportTrait.php delete mode 100644 Neos.ContentRepository.Export/Tests/Behavior/Features/Bootstrap/FeatureContext.php delete mode 100644 Neos.ContentRepository.Export/Tests/Behavior/Features/Export/Export.feature delete mode 100644 Neos.ContentRepository.Export/Tests/Behavior/Features/Import/Import.feature delete mode 100644 Neos.ContentRepository.Export/Tests/Behavior/behat.yml.dist delete mode 100644 Neos.ContentRepository.Export/src/ExportService.php delete mode 100644 Neos.ContentRepository.Export/src/ExportServiceFactory.php delete mode 100644 Neos.ContentRepository.Export/src/ImportService.php delete mode 100644 Neos.ContentRepository.Export/src/ImportServiceFactory.php create mode 100644 Neos.ContentRepository.Export/src/ProcessingContext.php delete mode 100644 Neos.ContentRepository.Export/src/ProcessorResult.php create mode 100644 Neos.ContentRepository.Export/src/Processors.php create mode 100644 Neos.ContentRepository.Export/src/Processors/ContentRepositorySetupProcessor.php delete mode 100644 Neos.Neos/Classes/Command/CrCommandController.php create mode 100644 Neos.Neos/Classes/Domain/Import/DoctrineMigrateProcessor.php create mode 100644 Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php create mode 100644 Neos.Neos/Classes/Domain/Service/SiteImportService.php create mode 100644 Neos.Neos/Classes/Domain/Service/SiteImportServiceFactory.php diff --git a/Neos.ContentRepository.Export/Tests/Behavior/Features/Bootstrap/CrImportExportTrait.php b/Neos.ContentRepository.Export/Tests/Behavior/Features/Bootstrap/CrImportExportTrait.php deleted file mode 100644 index 0aa571b20ea..00000000000 --- a/Neos.ContentRepository.Export/Tests/Behavior/Features/Bootstrap/CrImportExportTrait.php +++ /dev/null @@ -1,209 +0,0 @@ - */ - private array $crImportExportTrait_loggedErrors = []; - - /** @var array */ - private array $crImportExportTrait_loggedWarnings = []; - - public function setupCrImportExportTrait() - { - $this->crImportExportTrait_filesystem = new Filesystem(new InMemoryFilesystemAdapter()); - } - - /** - * @When /^the events are exported$/ - */ - public function theEventsAreExportedIExpectTheFollowingJsonl() - { - $eventExporter = $this->getContentRepositoryService( - new class ($this->crImportExportTrait_filesystem) implements ContentRepositoryServiceFactoryInterface { - public function __construct(private readonly Filesystem $filesystem) - { - } - public function build(ContentRepositoryServiceFactoryDependencies $serviceFactoryDependencies): EventExportProcessor { - return new EventExportProcessor( - $this->filesystem, - $serviceFactoryDependencies->contentRepository->findWorkspaceByName(WorkspaceName::forLive()), - $serviceFactoryDependencies->eventStore - ); - } - } - ); - assert($eventExporter instanceof EventExportProcessor); - - $eventExporter->onMessage(function (Severity $severity, string $message) { - if ($severity === Severity::ERROR) { - $this->crImportExportTrait_loggedErrors[] = $message; - } elseif ($severity === Severity::WARNING) { - $this->crImportExportTrait_loggedWarnings[] = $message; - } - }); - $this->crImportExportTrait_lastMigrationResult = $eventExporter->run(); - } - - /** - * @When /^I import the events\.jsonl(?: into "([^"]*)")?$/ - */ - public function iImportTheFollowingJson(?string $contentStreamId = null) - { - $eventImporter = $this->getContentRepositoryService( - new class ($this->crImportExportTrait_filesystem, $contentStreamId ? ContentStreamId::fromString($contentStreamId) : null) implements ContentRepositoryServiceFactoryInterface { - public function __construct( - private readonly Filesystem $filesystem, - private readonly ?ContentStreamId $contentStreamId - ) { - } - public function build(ContentRepositoryServiceFactoryDependencies $serviceFactoryDependencies): EventStoreImportProcessor { - return new EventStoreImportProcessor( - false, - $this->filesystem, - $serviceFactoryDependencies->eventStore, - $serviceFactoryDependencies->eventNormalizer, - $this->contentStreamId - ); - } - } - ); - assert($eventImporter instanceof EventStoreImportProcessor); - - $eventImporter->onMessage(function (Severity $severity, string $message) { - if ($severity === Severity::ERROR) { - $this->crImportExportTrait_loggedErrors[] = $message; - } elseif ($severity === Severity::WARNING) { - $this->crImportExportTrait_loggedWarnings[] = $message; - } - }); - $this->crImportExportTrait_lastMigrationResult = $eventImporter->run(); - } - - /** - * @Given /^using the following events\.jsonl:$/ - */ - public function usingTheFollowingEventsJsonl(PyStringNode $string) - { - $this->crImportExportTrait_filesystem->write('events.jsonl', $string->getRaw()); - } - - /** - * @AfterScenario - */ - public function failIfLastMigrationHasErrors(): void - { - if ($this->crImportExportTrait_lastMigrationResult !== null && $this->crImportExportTrait_lastMigrationResult->severity === Severity::ERROR) { - throw new \RuntimeException(sprintf('The last migration run led to an error: %s', $this->crImportExportTrait_lastMigrationResult->message)); - } - if ($this->crImportExportTrait_loggedErrors !== []) { - throw new \RuntimeException(sprintf('The last migration run logged %d error%s', count($this->crImportExportTrait_loggedErrors), count($this->crImportExportTrait_loggedErrors) === 1 ? '' : 's')); - } - } - - /** - * @Then I expect the following jsonl: - */ - public function iExpectTheFollowingJsonL(PyStringNode $string): void - { - if (!$this->crImportExportTrait_filesystem->has('events.jsonl')) { - Assert::fail('No events were exported'); - } - - $jsonL = $this->crImportExportTrait_filesystem->read('events.jsonl'); - - $exportedEvents = ExportedEvents::fromJsonl($jsonL); - $eventsWithoutRandomIds = []; - - foreach ($exportedEvents as $exportedEvent) { - // we have to remove the event id in \Neos\ContentRepository\Core\Feature\Common\NodeAggregateEventPublisher::enrichWithCommand - // and the initiatingTimestamp to make the events diff able - $eventsWithoutRandomIds[] = $exportedEvent - ->withIdentifier('random-event-uuid') - ->processMetadata(function (array $metadata) { - $metadata['initiatingTimestamp'] = 'random-time'; - return $metadata; - }); - } - - Assert::assertSame($string->getRaw(), ExportedEvents::fromIterable($eventsWithoutRandomIds)->toJsonl()); - } - - /** - * @Then I expect the following errors to be logged - */ - public function iExpectTheFollowingErrorsToBeLogged(TableNode $table): void - { - Assert::assertSame($table->getColumn(0), $this->crImportExportTrait_loggedErrors, 'Expected logged errors do not match'); - $this->crImportExportTrait_loggedErrors = []; - } - - /** - * @Then I expect the following warnings to be logged - */ - public function iExpectTheFollowingWarningsToBeLogged(TableNode $table): void - { - Assert::assertSame($table->getColumn(0), $this->crImportExportTrait_loggedWarnings, 'Expected logged warnings do not match'); - $this->crImportExportTrait_loggedWarnings = []; - } - - /** - * @Then I expect a MigrationError - * @Then I expect a MigrationError with the message - */ - public function iExpectAMigrationErrorWithTheMessage(PyStringNode $expectedMessage = null): void - { - Assert::assertNotNull($this->crImportExportTrait_lastMigrationResult, 'Expected the previous migration to contain errors, but no migration has been executed'); - Assert::assertSame(Severity::ERROR, $this->crImportExportTrait_lastMigrationResult->severity, sprintf('Expected the previous migration to contain errors, but it ended with severity "%s"', $this->crImportExportTrait_lastMigrationResult->severity->name)); - if ($expectedMessage !== null) { - Assert::assertSame($expectedMessage->getRaw(), $this->crImportExportTrait_lastMigrationResult->message); - } - $this->crImportExportTrait_lastMigrationResult = null; - } - - /** - * @template T of object - * @param class-string $className - * - * @return T - */ - abstract private function getObject(string $className): object; -} diff --git a/Neos.ContentRepository.Export/Tests/Behavior/Features/Bootstrap/FeatureContext.php b/Neos.ContentRepository.Export/Tests/Behavior/Features/Bootstrap/FeatureContext.php deleted file mode 100644 index 01310c7beff..00000000000 --- a/Neos.ContentRepository.Export/Tests/Behavior/Features/Bootstrap/FeatureContext.php +++ /dev/null @@ -1,78 +0,0 @@ -contentRepositoryRegistry = $this->getObject(ContentRepositoryRegistry::class); - - $this->setupCrImportExportTrait(); - } - - /** - * @BeforeScenario - */ - public function resetContentRepositoryComponents(BeforeScenarioScope $scope): void - { - GherkinTableNodeBasedContentDimensionSourceFactory::reset(); - GherkinPyStringNodeBasedNodeTypeManagerFactory::reset(); - } - - protected function getContentRepositoryService( - ContentRepositoryServiceFactoryInterface $factory - ): ContentRepositoryServiceInterface { - return $this->contentRepositoryRegistry->buildService( - $this->currentContentRepository->id, - $factory - ); - } - - protected function createContentRepository( - ContentRepositoryId $contentRepositoryId - ): ContentRepository { - $this->contentRepositoryRegistry->resetFactoryInstance($contentRepositoryId); - $contentRepository = $this->contentRepositoryRegistry->get($contentRepositoryId); - GherkinTableNodeBasedContentDimensionSourceFactory::reset(); - GherkinPyStringNodeBasedNodeTypeManagerFactory::reset(); - - return $contentRepository; - } -} diff --git a/Neos.ContentRepository.Export/Tests/Behavior/Features/Export/Export.feature b/Neos.ContentRepository.Export/Tests/Behavior/Features/Export/Export.feature deleted file mode 100644 index 99ecc22b627..00000000000 --- a/Neos.ContentRepository.Export/Tests/Behavior/Features/Export/Export.feature +++ /dev/null @@ -1,43 +0,0 @@ -@contentrepository -Feature: As a user of the CR I want to export the event stream - - Background: - Given using the following content dimensions: - | Identifier | Values | Generalizations | - | language | de, gsw, fr | gsw->de | - And using the following node types: - """yaml - 'Neos.ContentRepository.Testing:Document': [] - """ - And using identifier "default", I define a content repository - And I am in content repository "default" - And the command CreateRootWorkspace is executed with payload: - | Key | Value | - | workspaceName | "live" | - | newContentStreamId | "cs-identifier" | - And I am in workspace "live" - And the command CreateRootNodeAggregateWithNode is executed with payload: - | Key | Value | - | nodeAggregateId | "lady-eleonode-rootford" | - | nodeTypeName | "Neos.ContentRepository:Root" | - And the event NodeAggregateWithNodeWasCreated was published with payload: - | Key | Value | - | workspaceName | "live" | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "nody-mc-nodeface" | - | nodeTypeName | "Neos.ContentRepository.Testing:Document" | - | originDimensionSpacePoint | {"language":"de"} | - | coveredDimensionSpacePoints | [{"language":"de"},{"language":"gsw"},{"language":"fr"}] | - | parentNodeAggregateId | "lady-eleonode-rootford" | - | nodeName | "child-document" | - | nodeAggregateClassification | "regular" | - - Scenario: Export the event stream - Then I expect exactly 3 events to be published on stream with prefix "ContentStream:cs-identifier" - When the events are exported - Then I expect the following jsonl: - """ - {"identifier":"random-event-uuid","type":"RootNodeAggregateWithNodeWasCreated","payload":{"workspaceName":"live","contentStreamId":"cs-identifier","nodeAggregateId":"lady-eleonode-rootford","nodeTypeName":"Neos.ContentRepository:Root","coveredDimensionSpacePoints":[{"language":"de"},{"language":"gsw"},{"language":"fr"}],"nodeAggregateClassification":"root"},"metadata":{"commandClass":"Neos\\ContentRepository\\Core\\Feature\\RootNodeCreation\\Command\\CreateRootNodeAggregateWithNode","commandPayload":{"workspaceName":"live","nodeAggregateId":"lady-eleonode-rootford","nodeTypeName":"Neos.ContentRepository:Root","tetheredDescendantNodeAggregateIds":[]},"initiatingUserId":"system","initiatingTimestamp":"random-time"}} - {"identifier":"random-event-uuid","type":"NodeAggregateWithNodeWasCreated","payload":{"workspaceName":"live","contentStreamId":"cs-identifier","nodeAggregateId":"nody-mc-nodeface","nodeTypeName":"Neos.ContentRepository.Testing:Document","originDimensionSpacePoint":{"language":"de"},"succeedingSiblingsForCoverage":[{"dimensionSpacePoint":{"language":"de"},"nodeAggregateId":null},{"dimensionSpacePoint":{"language":"gsw"},"nodeAggregateId":null},{"dimensionSpacePoint":{"language":"fr"},"nodeAggregateId":null}],"parentNodeAggregateId":"lady-eleonode-rootford","nodeName":"child-document","initialPropertyValues":[],"nodeAggregateClassification":"regular"},"metadata":{"initiatingTimestamp":"random-time"}} - - """ diff --git a/Neos.ContentRepository.Export/Tests/Behavior/Features/Import/Import.feature b/Neos.ContentRepository.Export/Tests/Behavior/Features/Import/Import.feature deleted file mode 100644 index 6c61f644b57..00000000000 --- a/Neos.ContentRepository.Export/Tests/Behavior/Features/Import/Import.feature +++ /dev/null @@ -1,81 +0,0 @@ -@contentrepository -Feature: As a user of the CR I want to export the event stream - - Background: - Given using no content dimensions - And using the following node types: - """yaml - Vendor.Site:HomePage': - superTypes: - Neos.Neos:Site: true - """ - And using identifier "default", I define a content repository - And I am in content repository "default" - - Scenario: Import the event stream into a specific content stream - Then I expect exactly 0 events to be published on stream with prefix "ContentStream:cs-identifier" - Given using the following events.jsonl: - """ - {"identifier":"9f64c281-e5b0-48d9-900b-288a8faf92a9","type":"RootNodeAggregateWithNodeWasCreated","payload":{"workspaceName":"workspace-name","contentStreamId":"cs-imported-identifier","nodeAggregateId":"acme-site-sites","nodeTypeName":"Neos.Neos:Sites","coveredDimensionSpacePoints":[[]],"nodeAggregateClassification":"root"},"metadata":[]} - {"identifier":"1640ebbf-7ffe-4526-b0f4-7575cefabfab","type":"NodeAggregateWithNodeWasCreated","payload":{"workspaceName":"workspace-name","contentStreamId":"cs-imported-identifier","nodeAggregateId":"acme-site","nodeTypeName":"Vendor.Site:HomePage","originDimensionSpacePoint":[],"succeedingSiblingsForCoverage":[{"dimensionSpacePoint":[],"nodeAggregateId":null}],"parentNodeAggregateId":"acme-site-sites","nodeName":"acme-site","initialPropertyValues":{"title":{"value":"My Site","type":"string"},"uriPathSegment":{"value":"my-site","type":"string"}},"nodeAggregateClassification":"regular"},"metadata":[]} - """ - And I import the events.jsonl into "cs-identifier" - Then I expect exactly 3 events to be published on stream with prefix "ContentStream:cs-identifier" - And event at index 0 is of type "ContentStreamWasCreated" with payload: - | Key | Expected | - | contentStreamId | "cs-identifier" | - And event at index 1 is of type "RootNodeAggregateWithNodeWasCreated" with payload: - | Key | Expected | - | workspaceName | "workspace-name" | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "acme-site-sites" | - | nodeTypeName | "Neos.Neos:Sites" | - And event at index 2 is of type "NodeAggregateWithNodeWasCreated" with payload: - | Key | Expected | - | workspaceName | "workspace-name" | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "acme-site" | - | nodeTypeName | "Vendor.Site:HomePage" | - - Scenario: Import the event stream - Then I expect exactly 0 events to be published on stream with prefix "ContentStream:cs-imported-identifier" - Given using the following events.jsonl: - """ - {"identifier":"9f64c281-e5b0-48d9-900b-288a8faf92a9","type":"RootNodeAggregateWithNodeWasCreated","payload":{"workspaceName":"workspace-name","contentStreamId":"cs-imported-identifier","nodeAggregateId":"acme-site-sites","nodeTypeName":"Neos.Neos:Sites","coveredDimensionSpacePoints":[[]],"nodeAggregateClassification":"root"},"metadata":[]} - {"identifier":"1640ebbf-7ffe-4526-b0f4-7575cefabfab","type":"NodeAggregateWithNodeWasCreated","payload":{"workspaceName":"workspace-name","contentStreamId":"cs-imported-identifier","nodeAggregateId":"acme-site","nodeTypeName":"Vendor.Site:HomePage","originDimensionSpacePoint":[],"succeedingSiblingsForCoverage":[{"dimensionSpacePoint":[],"nodeAggregateId":null}],"parentNodeAggregateId":"acme-site-sites","nodeName":"acme-site","initialPropertyValues":{"title":{"value":"My Site","type":"string"},"uriPathSegment":{"value":"my-site","type":"string"}},"nodeAggregateClassification":"regular"},"metadata":[]} - """ - And I import the events.jsonl - Then I expect exactly 3 events to be published on stream with prefix "ContentStream:cs-imported-identifier" - And event at index 0 is of type "ContentStreamWasCreated" with payload: - | Key | Expected | - | contentStreamId | "cs-imported-identifier" | - And event at index 1 is of type "RootNodeAggregateWithNodeWasCreated" with payload: - | Key | Expected | - | workspaceName | "workspace-name" | - | contentStreamId | "cs-imported-identifier" | - | nodeAggregateId | "acme-site-sites" | - | nodeTypeName | "Neos.Neos:Sites" | - And event at index 2 is of type "NodeAggregateWithNodeWasCreated" with payload: - | Key | Expected | - | workspaceName | "workspace-name" | - | contentStreamId | "cs-imported-identifier" | - | nodeAggregateId | "acme-site" | - | nodeTypeName | "Vendor.Site:HomePage" | - - Scenario: Import faulty event stream with explicit "ContentStreamWasCreated" does not duplicate content-stream - see issue https://github.com/neos/neos-development-collection/issues/4298 - - Given using the following events.jsonl: - """ - {"identifier":"5f2da12d-7037-4524-acb0-d52037342c77","type":"ContentStreamWasCreated","payload":{"workspaceName":"workspace-name","contentStreamId":"cs-imported-identifier"},"metadata":[]} - {"identifier":"9f64c281-e5b0-48d9-900b-288a8faf92a9","type":"RootNodeAggregateWithNodeWasCreated","payload":{"workspaceName":"workspace-name","contentStreamId":"cs-imported-identifier","nodeAggregateId":"acme-site-sites","nodeTypeName":"Neos.Neos:Sites","coveredDimensionSpacePoints":[[]],"nodeAggregateClassification":"root"},"metadata":[]} - {"identifier":"1640ebbf-7ffe-4526-b0f4-7575cefabfab","type":"NodeAggregateWithNodeWasCreated","payload":{"workspaceName":"workspace-name","contentStreamId":"cs-imported-identifier","nodeAggregateId":"acme-site","nodeTypeName":"Vendor.Site:HomePage","originDimensionSpacePoint":[],"coveredDimensionSpacePoints":[[]],"parentNodeAggregateId":"acme-site-sites","nodeName":"acme-site","initialPropertyValues":{"title":{"value":"My Site","type":"string"},"uriPathSegment":{"value":"my-site","type":"string"}},"nodeAggregateClassification":"regular","succeedingNodeAggregateId":null},"metadata":[]} - """ - And I import the events.jsonl - - And I expect a MigrationError with the message - """ - Failed to read events. ContentStreamWasCreated is not expected in imported event stream. - """ - - Then I expect exactly 0 events to be published on stream with prefix "ContentStream:cs-imported-identifier" diff --git a/Neos.ContentRepository.Export/Tests/Behavior/behat.yml.dist b/Neos.ContentRepository.Export/Tests/Behavior/behat.yml.dist deleted file mode 100644 index 11e2845b599..00000000000 --- a/Neos.ContentRepository.Export/Tests/Behavior/behat.yml.dist +++ /dev/null @@ -1,11 +0,0 @@ - -default: - autoload: - '': "%paths.base%/Features/Bootstrap" - suites: - cr: - paths: - - "%paths.base%/Features" - - contexts: - - FeatureContext diff --git a/Neos.ContentRepository.Export/src/Asset/AssetExporter.php b/Neos.ContentRepository.Export/src/Asset/AssetExporter.php index 3ce343eebb2..0dc915dbf03 100644 --- a/Neos.ContentRepository.Export/src/Asset/AssetExporter.php +++ b/Neos.ContentRepository.Export/src/Asset/AssetExporter.php @@ -1,5 +1,7 @@ $processors */ - $processors = [ - 'Exporting events' => new EventExportProcessor( - $this->filesystem, - $this->targetWorkspace, - $this->eventStore - ), - 'Exporting assets' => new AssetExportProcessor( - $this->contentRepositoryId, - $this->filesystem, - $this->assetRepository, - $this->targetWorkspace, - $this->assetUsageService - ) - ]; - - foreach ($processors as $label => $processor) { - $outputLineFn($label . '...'); - $verbose && $processor->onMessage( - fn(Severity $severity, string $message) => $outputLineFn('<%1$s>%2$s', [$severity === Severity::ERROR ? 'error' : 'comment', $message]) - ); - $result = $processor->run(); - if ($result->severity === Severity::ERROR) { - throw new \RuntimeException($label . ': ' . ($result->message ?? '')); - } - $outputLineFn(' ' . $result->message); - $outputLineFn(); - } - } -} diff --git a/Neos.ContentRepository.Export/src/ExportServiceFactory.php b/Neos.ContentRepository.Export/src/ExportServiceFactory.php deleted file mode 100644 index e36d50be983..00000000000 --- a/Neos.ContentRepository.Export/src/ExportServiceFactory.php +++ /dev/null @@ -1,39 +0,0 @@ - - */ -class ExportServiceFactory implements ContentRepositoryServiceFactoryInterface -{ - - public function __construct( - private readonly Filesystem $filesystem, - private readonly Workspace $targetWorkspace, - private readonly AssetRepository $assetRepository, - private readonly AssetUsageService $assetUsageService, - ) { - } - - public function build(ContentRepositoryServiceFactoryDependencies $serviceFactoryDependencies): ExportService - { - return new ExportService( - $serviceFactoryDependencies->contentRepositoryId, - $this->filesystem, - $this->targetWorkspace, - $this->assetRepository, - $this->assetUsageService, - $serviceFactoryDependencies->eventStore, - ); - } -} diff --git a/Neos.ContentRepository.Export/src/ImportService.php b/Neos.ContentRepository.Export/src/ImportService.php deleted file mode 100644 index de06ced5b3c..00000000000 --- a/Neos.ContentRepository.Export/src/ImportService.php +++ /dev/null @@ -1,86 +0,0 @@ -liveWorkspaceContentStreamExists()) { - throw new LiveWorkspaceContentStreamExistsException(); - } - - /** @var ProcessorInterface[] $processors */ - $processors = [ - 'Importing assets' => new AssetRepositoryImportProcessor( - $this->filesystem, - $this->assetRepository, - $this->resourceRepository, - $this->resourceManager, - $this->persistenceManager, - ), - 'Importing events' => new EventStoreImportProcessor( - false, - $this->filesystem, - $this->eventStore, - $this->eventNormalizer, - $this->contentStreamIdentifier, - ) - ]; - - foreach ($processors as $label => $processor) { - $outputLineFn($label . '...'); - $verbose && $processor->onMessage( - fn(Severity $severity, string $message) => $outputLineFn('<%1$s>%2$s', [$severity === Severity::ERROR ? 'error' : 'comment', $message]) - ); - $result = $processor->run(); - if ($result->severity === Severity::ERROR) { - throw new \RuntimeException($label . ': ' . ($result->message ?? '')); - } - $outputLineFn(' ' . $result->message); - $outputLineFn(); - } - } - - private function liveWorkspaceContentStreamExists(): bool - { - $workspaceStreamName = WorkspaceEventStreamName::fromWorkspaceName(WorkspaceName::forLive())->getEventStreamName(); - $eventStream = $this->eventStore->load($workspaceStreamName); - foreach ($eventStream as $event) { - return true; - } - return false; - } -} diff --git a/Neos.ContentRepository.Export/src/ImportServiceFactory.php b/Neos.ContentRepository.Export/src/ImportServiceFactory.php deleted file mode 100644 index b5504818664..00000000000 --- a/Neos.ContentRepository.Export/src/ImportServiceFactory.php +++ /dev/null @@ -1,45 +0,0 @@ - - */ -class ImportServiceFactory implements ContentRepositoryServiceFactoryInterface -{ - - public function __construct( - private readonly Filesystem $filesystem, - private readonly ContentStreamId $contentStreamIdentifier, - private readonly AssetRepository $assetRepository, - private readonly ResourceRepository $resourceRepository, - private readonly ResourceManager $resourceManager, - private readonly PersistenceManagerInterface $persistenceManager, - ) { - } - - public function build(ContentRepositoryServiceFactoryDependencies $serviceFactoryDependencies): ImportService - { - return new ImportService( - $this->filesystem, - $this->contentStreamIdentifier, - $this->assetRepository, - $this->resourceRepository, - $this->resourceManager, - $this->persistenceManager, - $serviceFactoryDependencies->eventNormalizer, - $serviceFactoryDependencies->eventStore, - ); - } -} diff --git a/Neos.ContentRepository.Export/src/ProcessingContext.php b/Neos.ContentRepository.Export/src/ProcessingContext.php new file mode 100644 index 00000000000..cd6fd16bba0 --- /dev/null +++ b/Neos.ContentRepository.Export/src/ProcessingContext.php @@ -0,0 +1,24 @@ +onEvent)($severity, $message); + } +} diff --git a/Neos.ContentRepository.Export/src/ProcessorInterface.php b/Neos.ContentRepository.Export/src/ProcessorInterface.php index 6ee2a5246d7..e033c200256 100644 --- a/Neos.ContentRepository.Export/src/ProcessorInterface.php +++ b/Neos.ContentRepository.Export/src/ProcessorInterface.php @@ -1,14 +1,13 @@ + */ +final readonly class Processors implements \IteratorAggregate, \Countable +{ + /** + * @param array $processors + */ + private function __construct( + private array $processors + ) { + } + + /** + * @param array $processors + */ + public static function fromArray(array $processors): self + { + return new self($processors); + } + + public function getIterator(): \Traversable + { + yield from $this->processors; + } + + public function count(): int + { + return count($this->processors); + } +} diff --git a/Neos.ContentRepository.Export/src/Processors/AssetExportProcessor.php b/Neos.ContentRepository.Export/src/Processors/AssetExportProcessor.php index 3bceb6dc77f..78deda81ee3 100644 --- a/Neos.ContentRepository.Export/src/Processors/AssetExportProcessor.php +++ b/Neos.ContentRepository.Export/src/Processors/AssetExportProcessor.php @@ -1,14 +1,15 @@ */ - private array $callbacks = []; - public function __construct( private readonly ContentRepositoryId $contentRepositoryId, - private readonly Filesystem $files, private readonly AssetRepository $assetRepository, private readonly Workspace $targetWorkspace, private readonly AssetUsageService $assetUsageService, - ) {} - - public function onMessage(\Closure $callback): void - { - $this->callbacks[] = $callback; + ) { } - - public function run(): ProcessorResult + public function run(ProcessingContext $context): void { $assetFilter = AssetUsageFilter::create()->withWorkspaceName($this->targetWorkspace->workspaceName)->groupByAsset(); - $numberOfExportedAssets = 0; - $numberOfExportedImageVariants = 0; - $numberOfErrors = 0; - foreach ($this->assetUsageService->findByFilter($this->contentRepositoryId, $assetFilter) as $assetUsage) { /** @var Asset|null $asset */ $asset = $this->assetRepository->findByIdentifier($assetUsage->assetId); if ($asset === null) { - $numberOfErrors ++; - $this->dispatch(Severity::ERROR, 'Skipping asset "%s" because it does not exist in the database', $assetUsage->assetId); + $context->dispatch(Severity::ERROR, "Skipping asset \"{$assetUsage->assetId}\" because it does not exist in the database"); continue; } @@ -63,64 +50,47 @@ public function run(): ProcessorResult /** @var Asset $originalAsset */ $originalAsset = $asset->getOriginalAsset(); try { - $this->exportAsset($originalAsset); - $numberOfExportedAssets ++; + $this->exportAsset($context, $originalAsset); } catch (\Throwable $e) { - $numberOfErrors ++; - $this->dispatch(Severity::ERROR, 'Failed to export original asset "%s" (for variant "%s"): %s', $originalAsset->getIdentifier(), $asset->getIdentifier(), $e->getMessage()); + $context->dispatch(Severity::ERROR, "Failed to export original asset \"{$originalAsset->getIdentifier()}\" (for variant \"{$asset->getIdentifier()}\"): {$e->getMessage()}"); } } try { - $this->exportAsset($asset); - if ($asset instanceof AssetVariantInterface) { - $numberOfExportedImageVariants ++; - } else { - $numberOfExportedAssets ++; - } + $this->exportAsset($context, $asset); } catch (\Throwable $e) { - $numberOfErrors ++; - $this->dispatch(Severity::ERROR, 'Failed to export asset "%s": %s', $asset->getIdentifier(), $e->getMessage()); + $context->dispatch(Severity::ERROR, "Failed to export asset \"{$asset->getIdentifier()}\": {$e->getMessage()}"); } } - return ProcessorResult::success(sprintf('Exported %d Asset%s and %d Image Variant%s. Errors: %d', $numberOfExportedAssets, $numberOfExportedAssets === 1 ? '' : 's', $numberOfExportedImageVariants, $numberOfExportedImageVariants === 1 ? '' : 's', $numberOfErrors)); } /** --------------------------------------- */ - private function exportAsset(Asset $asset): void + private function exportAsset(ProcessingContext $context, Asset $asset): void { $fileLocation = $asset instanceof ImageVariant ? "ImageVariants/{$asset->getIdentifier()}.json" : "Assets/{$asset->getIdentifier()}.json"; - if ($this->files->has($fileLocation)) { + if ($context->files->has($fileLocation)) { return; } if ($asset instanceof ImageVariant) { - $this->files->write($fileLocation, SerializedImageVariant::fromImageVariant($asset)->toJson()); + $context->files->write($fileLocation, SerializedImageVariant::fromImageVariant($asset)->toJson()); return; } /** @var PersistentResource|null $resource */ $resource = $asset->getResource(); if ($resource === null) { - $this->dispatch(Severity::ERROR, 'Skipping asset "%s" because the corresponding PersistentResource does not exist in the database', $asset->getIdentifier()); + $context->dispatch(Severity::ERROR, "Skipping asset \"{$asset->getIdentifier()}\" because the corresponding PersistentResource does not exist in the database"); return; } - $this->files->write($fileLocation, SerializedAsset::fromAsset($asset)->toJson()); - $this->exportResource($resource); + $context->files->write($fileLocation, SerializedAsset::fromAsset($asset)->toJson()); + $this->exportResource($context, $resource); } - private function exportResource(PersistentResource $resource): void + private function exportResource(ProcessingContext $context, PersistentResource $resource): void { $fileLocation = "Resources/{$resource->getSha1()}"; - if ($this->files->has($fileLocation)) { + if ($context->files->has($fileLocation)) { return; } - $this->files->writeStream($fileLocation, $resource->getStream()); - } - - private function dispatch(Severity $severity, string $message, mixed ...$args): void - { - $renderedMessage = sprintf($message, ...$args); - foreach ($this->callbacks as $callback) { - $callback($severity, $renderedMessage); - } + $context->files->writeStream($fileLocation, $resource->getStream()); } } diff --git a/Neos.ContentRepository.Export/src/Processors/AssetRepositoryImportProcessor.php b/Neos.ContentRepository.Export/src/Processors/AssetRepositoryImportProcessor.php index b389adababa..2021098fb19 100644 --- a/Neos.ContentRepository.Export/src/Processors/AssetRepositoryImportProcessor.php +++ b/Neos.ContentRepository.Export/src/Processors/AssetRepositoryImportProcessor.php @@ -1,15 +1,16 @@ */ - private array $callbacks = []; - public function __construct( - private readonly Filesystem $files, private readonly AssetRepository $assetRepository, private readonly ResourceRepository $resourceRepository, private readonly ResourceManager $resourceManager, private readonly PersistenceManagerInterface $persistenceManager, - ) {} - - public function onMessage(\Closure $callback): void - { - $this->callbacks[] = $callback; + ) { } - public function run(): ProcessorResult + public function run(ProcessingContext $context): void { $this->persistenceManager->clearState(); - $numberOfErrors = 0; - $numberOfImportedAssets = 0; - foreach ($this->files->listContents('/Assets') as $file) { + foreach ($context->files->listContents('/Assets') as $file) { if (!$file->isFile()) { continue; } try { - $this->importAsset($file); - $numberOfImportedAssets ++; + $this->importAsset($context, $file); } catch (\Throwable $e) { - $numberOfErrors ++; - $this->dispatch(Severity::ERROR, 'Failed to import asset from file "%s": %s', $file->path(), $e->getMessage()); + $context->dispatch(Severity::ERROR, "Failed to import asset from file \"{$file->path()}\": {$e->getMessage()}"); } } - $numberOfImportedImageVariants = 0; - foreach ($this->files->listContents('/ImageVariants') as $file) { + foreach ($context->files->listContents('/ImageVariants') as $file) { if (!$file->isFile()) { continue; } try { - $this->importImageVariant($file); - $numberOfImportedImageVariants ++; + $this->importImageVariant($context, $file); } catch (\Throwable $e) { - $numberOfErrors ++; - $this->dispatch(Severity::ERROR, 'Failed to import image variant from file "%s": %s', $file->path(), $e->getMessage()); + $context->dispatch(Severity::ERROR, "Failed to import image variant from file \"{$file->path()}\": {$e->getMessage()}"); } } - return ProcessorResult::success(sprintf('Imported %d Asset%s and %d Image Variant%s. Errors: %d', $numberOfImportedAssets, $numberOfImportedAssets === 1 ? '' : 's', $numberOfImportedImageVariants, $numberOfImportedImageVariants === 1 ? '' : 's', $numberOfErrors)); } /** --------------------------------------- */ - private function importAsset(StorageAttributes $file): void + private function importAsset(ProcessingContext $context, StorageAttributes $file): void { - $fileContents = $this->files->read($file->path()); + $fileContents = $context->files->read($file->path()); $serializedAsset = SerializedAsset::fromJson($fileContents); /** @var Asset|null $existingAsset */ $existingAsset = $this->assetRepository->findByIdentifier($serializedAsset->identifier); if ($existingAsset !== null) { if ($serializedAsset->matches($existingAsset)) { - $this->dispatch(Severity::NOTICE, 'Asset "%s" was skipped because it already exists!', $serializedAsset->identifier); + $context->dispatch(Severity::NOTICE, "Asset \"{$serializedAsset->identifier}\" was skipped because it already exists!"); } else { - $this->dispatch(Severity::ERROR, 'Asset "%s" has been changed in the meantime, it was NOT updated!', $serializedAsset->identifier); + $context->dispatch(Severity::ERROR, "Asset \"{$serializedAsset->identifier}\" has been changed in the meantime, it was NOT updated!"); } return; } /** @var PersistentResource|null $resource */ $resource = $this->resourceRepository->findBySha1AndCollectionName($serializedAsset->resource->sha1, $serializedAsset->resource->collectionName)[0] ?? null; if ($resource === null) { - $content = $this->files->read('/Resources/' . $serializedAsset->resource->sha1); + $content = $context->files->read('/Resources/' . $serializedAsset->resource->sha1); $resource = $this->resourceManager->importResourceFromContent($content, $serializedAsset->resource->filename, $serializedAsset->resource->collectionName); $resource->setMediaType($serializedAsset->resource->mediaType); } @@ -120,23 +105,23 @@ private function importAsset(StorageAttributes $file): void $this->persistenceManager->persistAll(); } - private function importImageVariant(StorageAttributes $file): void + private function importImageVariant(ProcessingContext $context, StorageAttributes $file): void { - $fileContents = $this->files->read($file->path()); + $fileContents = $context->files->read($file->path()); $serializedImageVariant = SerializedImageVariant::fromJson($fileContents); $existingImageVariant = $this->assetRepository->findByIdentifier($serializedImageVariant->identifier); assert($existingImageVariant === null || $existingImageVariant instanceof ImageVariant); if ($existingImageVariant !== null) { if ($serializedImageVariant->matches($existingImageVariant)) { - $this->dispatch(Severity::NOTICE, 'Image Variant "%s" was skipped because it already exists!', $serializedImageVariant->identifier); + $context->dispatch(Severity::NOTICE, "Image Variant \"{$serializedImageVariant->identifier}\" was skipped because it already exists!"); } else { - $this->dispatch(Severity::ERROR, 'Image Variant "%s" has been changed in the meantime, it was NOT updated!', $serializedImageVariant->identifier); + $context->dispatch(Severity::ERROR, "Image Variant \"{$serializedImageVariant->identifier}\" has been changed in the meantime, it was NOT updated!"); } return; } $originalImage = $this->assetRepository->findByIdentifier($serializedImageVariant->originalAssetIdentifier); if ($originalImage === null) { - $this->dispatch(Severity::ERROR, 'Failed to find original asset "%s", skipping image variant "%s"', $serializedImageVariant->originalAssetIdentifier, $serializedImageVariant->identifier); + $context->dispatch(Severity::ERROR, "Failed to find original asset \"{$serializedImageVariant->originalAssetIdentifier}\", skipping image variant \"{$serializedImageVariant->identifier}\""); return; } assert($originalImage instanceof Image); @@ -154,12 +139,4 @@ private function importImageVariant(StorageAttributes $file): void $this->assetRepository->add($imageVariant); $this->persistenceManager->persistAll(); } - - private function dispatch(Severity $severity, string $message, mixed ...$args): void - { - $renderedMessage = sprintf($message, ...$args); - foreach ($this->callbacks as $callback) { - $callback($severity, $renderedMessage); - } - } } diff --git a/Neos.ContentRepository.Export/src/Processors/ContentRepositorySetupProcessor.php b/Neos.ContentRepository.Export/src/Processors/ContentRepositorySetupProcessor.php new file mode 100644 index 00000000000..57d322921da --- /dev/null +++ b/Neos.ContentRepository.Export/src/Processors/ContentRepositorySetupProcessor.php @@ -0,0 +1,28 @@ +dispatch(Severity::NOTICE, "Setting up content repository \"{$this->contentRepository->id->value}\""); + $this->contentRepository->setUp(); + } +} diff --git a/Neos.ContentRepository.Export/src/Processors/EventExportProcessor.php b/Neos.ContentRepository.Export/src/Processors/EventExportProcessor.php index b7d0b486188..f32019a6e65 100644 --- a/Neos.ContentRepository.Export/src/Processors/EventExportProcessor.php +++ b/Neos.ContentRepository.Export/src/Processors/EventExportProcessor.php @@ -1,49 +1,43 @@ */ - private array $callbacks = []; - + /** + * @param ContentStreamId $contentStreamId Identifier of the content stream to export + */ public function __construct( - private readonly Filesystem $files, - private readonly Workspace $targetWorkspace, - private readonly EventStoreInterface $eventStore, + private ContentStreamId $contentStreamId, + private EventStoreInterface $eventStore, ) { } - public function onMessage(\Closure $callback): void - { - $this->callbacks[] = $callback; - } - - public function run(): ProcessorResult + public function run(ProcessingContext $context): void { - $streamName = ContentStreamEventStreamName::fromContentStreamId($this->targetWorkspace->currentContentStreamId)->getEventStreamName(); + $streamName = ContentStreamEventStreamName::fromContentStreamId($this->contentStreamId)->getEventStreamName(); $eventStream = $this->eventStore->load($streamName); $eventFileResource = fopen('php://temp/maxmemory:5242880', 'rb+'); if ($eventFileResource === false) { - return ProcessorResult::error('Failed to create temporary event file resource'); + throw new \RuntimeException('Failed to create temporary event file resource', 1729506599); } - $numberOfExportedEvents = 0; foreach ($eventStream as $eventEnvelope) { if ($eventEnvelope->event->type->value === 'ContentStreamWasCreated') { // the content stream will be created in the import dynamically, so we prevent duplication here @@ -51,28 +45,12 @@ public function run(): ProcessorResult } $event = ExportedEvent::fromRawEvent($eventEnvelope->event); fwrite($eventFileResource, $event->toJson() . chr(10)); - $numberOfExportedEvents ++; } try { - $this->files->writeStream('events.jsonl', $eventFileResource); + $context->files->writeStream('events.jsonl', $eventFileResource); } catch (FilesystemException $e) { - return ProcessorResult::error(sprintf('Failed to write events.jsonl: %s', $e->getMessage())); + throw new \RuntimeException(sprintf('Failed to write events.jsonl: %s', $e->getMessage()), 1729506623, $e); } fclose($eventFileResource); - return ProcessorResult::success(sprintf('Exported %d event%s', $numberOfExportedEvents, $numberOfExportedEvents === 1 ? '' : 's')); - } - - /** --------------------------------------- */ - - - /** - * @phpstan-ignore-next-line currently this private method is unused ... but it does no harm keeping it - */ - private function dispatch(Severity $severity, string $message, mixed ...$args): void - { - $renderedMessage = sprintf($message, ...$args); - foreach ($this->callbacks as $callback) { - $callback($severity, $renderedMessage); - } } } diff --git a/Neos.ContentRepository.Export/src/Processors/EventStoreImportProcessor.php b/Neos.ContentRepository.Export/src/Processors/EventStoreImportProcessor.php index 50f4474f485..e35d882db95 100644 --- a/Neos.ContentRepository.Export/src/Processors/EventStoreImportProcessor.php +++ b/Neos.ContentRepository.Export/src/Processors/EventStoreImportProcessor.php @@ -1,9 +1,9 @@ */ - private array $callbacks = []; - private ?ContentStreamId $contentStreamId = null; public function __construct( private readonly bool $keepEventIds, - private readonly Filesystem $files, private readonly EventStoreInterface $eventStore, private readonly EventNormalizer $eventNormalizer, ?ContentStreamId $overrideContentStreamId @@ -49,16 +44,11 @@ public function __construct( } } - public function onMessage(\Closure $callback): void - { - $this->callbacks[] = $callback; - } - - public function run(): ProcessorResult + public function run(ProcessingContext $context): void { /** @var array $domainEvents */ $domainEvents = []; - $eventFileResource = $this->files->readStream('events.jsonl'); + $eventFileResource = $context->files->readStream('events.jsonl'); /** @var array $eventIdMap */ $eventIdMap = []; @@ -106,7 +96,7 @@ public function run(): ProcessorResult ) ); if (in_array($domainEvent::class, [ContentStreamWasCreated::class, ContentStreamWasForked::class, ContentStreamWasRemoved::class], true)) { - return ProcessorResult::error(sprintf('Failed to read events. %s is not expected in imported event stream.', $event->type)); + throw new \RuntimeException(sprintf('Failed to read events. %s is not expected in imported event stream.', $event->type), 1729506757); } $domainEvent = DecoratedEvent::create($domainEvent, eventId: EventId::fromString($event->identifier), metadata: $event->metadata); $domainEvents[] = $this->eventNormalizer->normalize($domainEvent); @@ -125,7 +115,7 @@ public function run(): ProcessorResult try { $contentStreamCreationCommitResult = $this->eventStore->commit($contentStreamStreamName, $events, ExpectedVersion::NO_STREAM()); } catch (ConcurrencyException $e) { - return ProcessorResult::error(sprintf('Failed to publish workspace events because the event stream "%s" already exists (1)', $this->contentStreamId->value)); + throw new \RuntimeException(sprintf('Failed to publish workspace events because the event stream "%s" already exists (1)', $this->contentStreamId->value), 1729506776, $e); } $workspaceName = WorkspaceName::forLive(); @@ -141,15 +131,14 @@ public function run(): ProcessorResult try { $this->eventStore->commit($workspaceStreamName, $events, ExpectedVersion::NO_STREAM()); } catch (ConcurrencyException $e) { - return ProcessorResult::error(sprintf('Failed to publish workspace events because the event stream "%s" already exists (2)', $workspaceStreamName->value)); + throw new \RuntimeException(sprintf('Failed to publish workspace events because the event stream "%s" already exists (2)', $workspaceStreamName->value), 1729506798, $e); } try { $this->eventStore->commit($contentStreamStreamName, Events::fromArray($domainEvents), ExpectedVersion::fromVersion($contentStreamCreationCommitResult->highestCommittedVersion)); } catch (ConcurrencyException $e) { - return ProcessorResult::error(sprintf('Failed to publish %d events because the event stream "%s" already exists (3)', count($domainEvents), $contentStreamStreamName->value)); + throw new \RuntimeException(sprintf('Failed to publish %d events because the event stream "%s" already exists (3)', count($domainEvents), $contentStreamStreamName->value), 1729506818, $e); } - return ProcessorResult::success(sprintf('Imported %d event%s into stream "%s"', count($domainEvents), count($domainEvents) === 1 ? '' : 's', $contentStreamStreamName->value)); } /** --------------------------- */ @@ -165,15 +154,4 @@ private static function extractContentStreamId(array $payload): ContentStreamId } return ContentStreamId::fromString($payload['contentStreamId']); } - - /** - * @phpstan-ignore-next-line currently this private method is unused ... but it does no harm keeping it - */ - private function dispatch(Severity $severity, string $message, mixed ...$args): void - { - $renderedMessage = sprintf($message, ...$args); - foreach ($this->callbacks as $callback) { - $callback($severity, $renderedMessage); - } - } } diff --git a/Neos.ContentRepository.LegacyNodeMigration/Classes/LegacyMigrationService.php b/Neos.ContentRepository.LegacyNodeMigration/Classes/LegacyMigrationService.php index e95c5b27dc2..3c2ab1350d2 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Classes/LegacyMigrationService.php +++ b/Neos.ContentRepository.LegacyNodeMigration/Classes/LegacyMigrationService.php @@ -1,4 +1,5 @@ connection), new FileSystemResourceLoader($this->resourcesPath)); - /** @var ProcessorInterface[] $processors */ - $processors = [ + $processors = Processors::fromArray([ 'Exporting assets' => new NodeDataToAssetsProcessor($this->nodeTypeManager, $assetExporter, new NodeDataLoader($this->connection)), - 'Exporting node data' => new NodeDataToEventsProcessor($this->nodeTypeManager, $this->propertyMapper, $this->propertyConverter, $this->interDimensionalVariationGraph, $this->eventNormalizer, $filesystem, new NodeDataLoader($this->connection)), - 'Importing assets' => new AssetRepositoryImportProcessor($filesystem, $this->assetRepository, $this->resourceRepository, $this->resourceManager, $this->persistenceManager), - 'Importing events' => new EventStoreImportProcessor(true, $filesystem, $this->eventStore, $this->eventNormalizer, $this->contentStreamId), - ]; - + 'Exporting node data' => new NodeDataToEventsProcessor($this->nodeTypeManager, $this->propertyMapper, $this->propertyConverter, $this->interDimensionalVariationGraph, $this->eventNormalizer, new NodeDataLoader($this->connection)), + 'Importing assets' => new AssetRepositoryImportProcessor($this->assetRepository, $this->resourceRepository, $this->resourceManager, $this->persistenceManager), + 'Importing events' => new EventStoreImportProcessor(true, $this->eventStore, $this->eventNormalizer, $this->contentStreamId), + ]); + $processingContext = new ProcessingContext($filesystem, function (Severity $severity, string $message) use ($verbose, $outputLineFn) { + if ($severity !== Severity::NOTICE || $verbose) { + $outputLineFn('<%1$s>%2$s', [$severity === Severity::ERROR ? 'error' : 'comment', $message]); + } + }); foreach ($processors as $label => $processor) { $outputLineFn($label . '...'); - $processor->onMessage(function (Severity $severity, string $message) use ($verbose, $outputLineFn) { - if ($severity !== Severity::NOTICE || $verbose) { - $outputLineFn('<%1$s>%2$s', [$severity === Severity::ERROR ? 'error' : 'comment', $message]); - } - }); - $result = $processor->run(); - if ($result->severity === Severity::ERROR) { - throw new \RuntimeException($label . ': ' . $result->message); - } - $outputLineFn(' ' . $result->message); + $processor->run($processingContext); $outputLineFn(); } Files::unlink($temporaryFilePath); diff --git a/Neos.ContentRepository.LegacyNodeMigration/Classes/NodeDataToAssetsProcessor.php b/Neos.ContentRepository.LegacyNodeMigration/Classes/NodeDataToAssetsProcessor.php index 86d38a6d8a5..6bde0f844e5 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Classes/NodeDataToAssetsProcessor.php +++ b/Neos.ContentRepository.LegacyNodeMigration/Classes/NodeDataToAssetsProcessor.php @@ -1,5 +1,5 @@ */ private array $processedAssetIds = []; - /** - * @var array<\Closure> - */ - private array $callbacks = []; /** * @param iterable> $nodeDataRows @@ -32,16 +28,11 @@ public function __construct( private readonly NodeTypeManager $nodeTypeManager, private readonly AssetExporter $assetExporter, private readonly iterable $nodeDataRows, - ) {} - - public function onMessage(\Closure $callback): void - { - $this->callbacks[] = $callback; + ) { } - public function run(): ProcessorResult + public function run(ProcessingContext $context): void { - $numberOfErrors = 0; foreach ($this->nodeDataRows as $nodeDataRow) { if ($nodeDataRow['path'] === '/sites') { // the sites node has no properties and is unstructured @@ -50,21 +41,20 @@ public function run(): ProcessorResult $nodeTypeName = NodeTypeName::fromString($nodeDataRow['nodetype']); $nodeType = $this->nodeTypeManager->getNodeType($nodeTypeName); if (!$nodeType) { - $this->dispatch(Severity::ERROR, 'The node type "%s" is not available. Node: "%s"', $nodeTypeName->value, $nodeDataRow['identifier']); + $context->dispatch(Severity::ERROR, "The node type \"{$nodeTypeName->value}\" is not available. Node: \"{$nodeDataRow['identifier']}\""); continue; } try { $properties = json_decode($nodeDataRow['properties'], true, 512, JSON_THROW_ON_ERROR); } catch (\JsonException $exception) { - $numberOfErrors ++; - $this->dispatch(Severity::ERROR, 'Failed to JSON-decode properties %s of node "%s" (type: "%s"): %s', $nodeDataRow['properties'], $nodeDataRow['identifier'], $nodeTypeName->value, $exception->getMessage()); + $context->dispatch(Severity::ERROR, "Failed to JSON-decode properties {$nodeDataRow['properties']} of node \"{$nodeDataRow['identifier']}\" (type: \"{$nodeTypeName->value}\"): {$exception->getMessage()}"); continue; } foreach ($properties as $propertyName => $propertyValue) { try { $propertyType = $nodeType->getPropertyType($propertyName); - } catch (\InvalidArgumentException $exception) { - $this->dispatch(Severity::WARNING, 'Skipped node data processing for the property "%s". The property name is not part of the NodeType schema for the NodeType "%s". (Node: %s)', $propertyName, $nodeType->name->value, $nodeDataRow['identifier']); + } catch (\InvalidArgumentException $e) { + $context->dispatch(Severity::WARNING, "Skipped node data processing for the property \"{$propertyName}\". The property name is not part of the NodeType schema for the NodeType \"{$nodeType->name->value}\". (Node: {$nodeDataRow['identifier']})"); continue; } foreach ($this->extractAssetIdentifiers($propertyType, $propertyValue) as $assetId) { @@ -75,15 +65,11 @@ public function run(): ProcessorResult try { $this->assetExporter->exportAsset($assetId); } catch (\Exception $exception) { - $numberOfErrors ++; - $this->dispatch(Severity::ERROR, 'Failed to extract assets of property "%s" of node "%s" (type: "%s"): %s', $propertyName, $nodeDataRow['identifier'], $nodeTypeName->value, $exception->getMessage()); + $context->dispatch(Severity::ERROR, "Failed to extract assets of property \"{$propertyName}\" of node \"{$nodeDataRow['identifier']}\" (type: \"{$nodeTypeName->value}\"): {$exception->getMessage()}"); } } } } - $numberOfExportedAssets = count($this->processedAssetIds); - $this->processedAssetIds = []; - return ProcessorResult::success(sprintf('Exported %d asset%s. Errors: %d', $numberOfExportedAssets, $numberOfExportedAssets === 1 ? '' : 's', $numberOfErrors)); } /** ----------------------------- */ @@ -110,8 +96,7 @@ private function extractAssetIdentifiers(string $type, mixed $value): array if ($parsedType['elementType'] === null) { return []; } - if (!is_subclass_of($parsedType['elementType'], ResourceBasedInterface::class, true) - && !is_subclass_of($parsedType['elementType'], \Stringable::class, true)) { + if (!is_subclass_of($parsedType['elementType'], ResourceBasedInterface::class) && !is_subclass_of($parsedType['elementType'], \Stringable::class)) { return []; } /** @var array> $assetIdentifiers */ @@ -122,13 +107,4 @@ private function extractAssetIdentifiers(string $type, mixed $value): array } return array_merge(...$assetIdentifiers); } - - private function dispatch(Severity $severity, string $message, mixed ...$args): void - { - $renderedMessage = sprintf($message, ...$args); - foreach ($this->callbacks as $callback) { - $callback($severity, $renderedMessage); - } - } - } diff --git a/Neos.ContentRepository.LegacyNodeMigration/Classes/NodeDataToEventsProcessor.php b/Neos.ContentRepository.LegacyNodeMigration/Classes/NodeDataToEventsProcessor.php index 9379206d5ee..48ea7dd0bdb 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Classes/NodeDataToEventsProcessor.php +++ b/Neos.ContentRepository.LegacyNodeMigration/Classes/NodeDataToEventsProcessor.php @@ -1,12 +1,11 @@ - */ - private array $callbacks = []; private NodeTypeName $sitesNodeTypeName; private WorkspaceName $workspaceName; private ContentStreamId $contentStreamId; @@ -89,7 +84,6 @@ public function __construct( private readonly PropertyConverter $propertyConverter, private readonly InterDimensionalVariationGraph $interDimensionalVariationGraph, private readonly EventNormalizer $eventNormalizer, - private readonly Filesystem $files, private readonly iterable $nodeDataRows, ) { $this->sitesNodeTypeName = NodeTypeNameFactory::forSites(); @@ -115,12 +109,7 @@ public function setSitesNodeType(NodeTypeName $nodeTypeName): void $this->sitesNodeTypeName = $nodeTypeName; } - public function onMessage(\Closure $callback): void - { - $this->callbacks[] = $callback; - } - - public function run(): ProcessorResult + public function run(ProcessingContext $context): void { $this->resetRuntimeState(); @@ -132,13 +121,13 @@ public function run(): ProcessorResult continue; } if ($this->metaDataExported === false && $nodeDataRow['parentpath'] === '/sites') { - $this->exportMetaData($nodeDataRow); + $this->exportMetaData($context, $nodeDataRow); $this->metaDataExported = true; } try { - $this->processNodeData($nodeDataRow); - } catch (MigrationException $exception) { - return ProcessorResult::error($exception->getMessage()); + $this->processNodeData($context, $nodeDataRow); + } catch (MigrationException $e) { + throw new \RuntimeException($e->getMessage(), 1729506899, $e); } } // Set References, now when the full import is done. @@ -147,11 +136,10 @@ public function run(): ProcessorResult } try { - $this->files->writeStream('events.jsonl', $this->eventFileResource); - } catch (FilesystemException $exception) { - return ProcessorResult::error(sprintf('Failed to write events.jsonl: %s', $exception->getMessage())); + $context->files->writeStream('events.jsonl', $this->eventFileResource); + } catch (FilesystemException $e) { + throw new \RuntimeException(sprintf('Failed to write events.jsonl: %s', $e->getMessage()), 1729506930, $e); } - return ProcessorResult::success(sprintf('Exported %d event%s', $this->numberOfExportedEvents, $this->numberOfExportedEvents === 1 ? '' : 's')); } /** ----------------------------- */ @@ -188,10 +176,10 @@ private function exportEvent(EventInterface $event): void /** * @param array $nodeDataRow */ - private function exportMetaData(array $nodeDataRow): void + private function exportMetaData(ProcessingContext $context, array $nodeDataRow): void { - if ($this->files->fileExists('meta.json')) { - $data = json_decode($this->files->read('meta.json'), true, 512, JSON_THROW_ON_ERROR); + if ($context->files->fileExists('meta.json')) { + $data = json_decode($context->files->read('meta.json'), true, 512, JSON_THROW_ON_ERROR); } else { $data = []; } @@ -199,13 +187,13 @@ private function exportMetaData(array $nodeDataRow): void $data['sitePackageKey'] = strtok($nodeDataRow['nodetype'], ':'); $data['siteNodeName'] = substr($nodeDataRow['path'], 7); $data['siteNodeType'] = $nodeDataRow['nodetype']; - $this->files->write('meta.json', json_encode($data, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT)); + $context->files->write('meta.json', json_encode($data, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT)); } /** * @param array $nodeDataRow */ - private function processNodeData(array $nodeDataRow): void + private function processNodeData(ProcessingContext $context, array $nodeDataRow): void { $nodeAggregateId = NodeAggregateId::fromString($nodeDataRow['identifier']); @@ -235,11 +223,11 @@ private function processNodeData(array $nodeDataRow): void foreach ($this->interDimensionalVariationGraph->getDimensionSpacePoints() as $dimensionSpacePoint) { $originDimensionSpacePoint = OriginDimensionSpacePoint::fromDimensionSpacePoint($dimensionSpacePoint); if (!$this->visitedNodes->alreadyVisitedOriginDimensionSpacePoints($nodeAggregateId)->contains($originDimensionSpacePoint)) { - $this->processNodeDataWithoutFallbackToEmptyDimension($nodeAggregateId, $originDimensionSpacePoint, $nodeDataRow); + $this->processNodeDataWithoutFallbackToEmptyDimension($context, $nodeAggregateId, $originDimensionSpacePoint, $nodeDataRow); } } } else { - $this->processNodeDataWithoutFallbackToEmptyDimension($nodeAggregateId, $originDimensionSpacePoint, $nodeDataRow); + $this->processNodeDataWithoutFallbackToEmptyDimension($context, $nodeAggregateId, $originDimensionSpacePoint, $nodeDataRow); } } @@ -250,12 +238,12 @@ private function processNodeData(array $nodeDataRow): void * @param array $nodeDataRow * @return NodeName[]|void */ - public function processNodeDataWithoutFallbackToEmptyDimension(NodeAggregateId $nodeAggregateId, OriginDimensionSpacePoint $originDimensionSpacePoint, array $nodeDataRow) + public function processNodeDataWithoutFallbackToEmptyDimension(ProcessingContext $context, NodeAggregateId $nodeAggregateId, OriginDimensionSpacePoint $originDimensionSpacePoint, array $nodeDataRow) { $nodePath = NodePath::fromString(strtolower($nodeDataRow['path'])); $parentNodeAggregate = $this->visitedNodes->findMostSpecificParentNodeInDimensionGraph($nodePath, $originDimensionSpacePoint, $this->interDimensionalVariationGraph); if ($parentNodeAggregate === null) { - $this->dispatch(Severity::ERROR, 'Failed to find parent node for node with id "%s" and dimensions: %s. Please ensure that the new content repository has a valid content dimension configuration. Also note that the old CR can sometimes have orphaned nodes.', $nodeAggregateId->value, $originDimensionSpacePoint->toJson()); + $context->dispatch(Severity::ERROR, "Failed to find parent node for node with id \"{$nodeAggregateId->value}\" and dimensions: {$originDimensionSpacePoint->toJson()}. Please ensure that the new content repository has a valid content dimension configuration. Also note that the old CR can sometimes have orphaned nodes."); return; } $pathParts = $nodePath->getParts(); @@ -267,17 +255,15 @@ public function processNodeDataWithoutFallbackToEmptyDimension(NodeAggregateId $ $isSiteNode = $nodeDataRow['parentpath'] === '/sites'; if ($isSiteNode && !$nodeType?->isOfType(NodeTypeNameFactory::NAME_SITE)) { - throw new MigrationException(sprintf( - 'The site node "%s" (type: "%s") must be of type "%s"', $nodeDataRow['identifier'], $nodeTypeName->value, NodeTypeNameFactory::NAME_SITE - ), 1695801620); + throw new MigrationException(sprintf('The site node "%s" (type: "%s") must be of type "%s"', $nodeDataRow['identifier'], $nodeTypeName->value, NodeTypeNameFactory::NAME_SITE), 1695801620); } if (!$nodeType) { - $this->dispatch(Severity::ERROR, 'The node type "%s" is not available. Node: "%s"', $nodeTypeName->value, $nodeDataRow['identifier']); + $context->dispatch(Severity::ERROR, "The node type \"{$nodeTypeName->value}\" is not available. Node: \"{$nodeDataRow['identifier']}\""); return; } - $serializedPropertyValuesAndReferences = $this->extractPropertyValuesAndReferences($nodeDataRow, $nodeType); + $serializedPropertyValuesAndReferences = $this->extractPropertyValuesAndReferences($context, $nodeDataRow, $nodeType); if ($this->isAutoCreatedChildNode($parentNodeAggregate->nodeTypeName, $nodeName) && !$this->visitedNodes->containsNodeAggregate($nodeAggregateId)) { // Create tethered node if the node was not found before. @@ -335,7 +321,7 @@ public function processNodeDataWithoutFallbackToEmptyDimension(NodeAggregateId $ /** * @param array $nodeDataRow */ - public function extractPropertyValuesAndReferences(array $nodeDataRow, NodeType $nodeType): SerializedPropertyValuesAndReferences + public function extractPropertyValuesAndReferences(ProcessingContext $context, array $nodeDataRow, NodeType $nodeType): SerializedPropertyValuesAndReferences { $properties = []; $references = []; @@ -362,7 +348,7 @@ public function extractPropertyValuesAndReferences(array $nodeDataRow, NodeType } if (!$nodeType->hasProperty($propertyName)) { - $this->dispatch(Severity::WARNING, 'Skipped node data processing for the property "%s". The property name is not part of the NodeType schema for the NodeType "%s". (Node: %s)', $propertyName, $nodeType->name->value, $nodeDataRow['identifier']); + $context->dispatch(Severity::WARNING, "Skipped node data processing for the property \"{$propertyName}\". The property name is not part of the NodeType schema for the NodeType \"{$nodeType->name->value}\". (Node: {$nodeDataRow['identifier']})"); continue; } $type = $nodeType->getPropertyType($propertyName); @@ -375,7 +361,6 @@ public function extractPropertyValuesAndReferences(array $nodeDataRow, NodeType } else { $properties[$propertyName] = $this->propertyMapper->convert($propertyValue, $type); } - } catch (\Exception $e) { throw new MigrationException(sprintf('Failed to convert property "%s" of type "%s" (Node: %s): %s', $propertyName, $type, $nodeDataRow['identifier'], $e->getMessage()), 1655912878, $e); } @@ -397,7 +382,7 @@ public function extractPropertyValuesAndReferences(array $nodeDataRow, NodeType } } else { if ($nodeDataRow['hiddenbeforedatetime'] || $nodeDataRow['hiddenafterdatetime']) { - $this->dispatch(Severity::WARNING, 'Skipped the migration of your "hiddenBeforeDateTime" and "hiddenAfterDateTime" properties as your target NodeTypes do not inherit "Neos.TimeableNodeVisibility:Timeable". Please install neos/timeable-node-visibility, if you want to migrate them.'); + $context->dispatch(Severity::WARNING, 'Skipped the migration of your "hiddenBeforeDateTime" and "hiddenAfterDateTime" properties as your target NodeTypes do not inherit "Neos.TimeableNodeVisibility:Timeable". Please install neos/timeable-node-visibility, if you want to migrate them.'); } } @@ -505,14 +490,6 @@ private function isAutoCreatedChildNode(NodeTypeName $parentNodeTypeName, NodeNa return $nodeTypeOfParent->tetheredNodeTypeDefinitions->contain($nodeName); } - private function dispatch(Severity $severity, string $message, mixed ...$args): void - { - $renderedMessage = sprintf($message, ...$args); - foreach ($this->callbacks as $callback) { - $callback($severity, $renderedMessage); - } - } - /** * Determines actual hidden state based on "hidden", "hiddenafterdatetime" and "hiddenbeforedatetime" * @@ -530,19 +507,21 @@ private function isNodeHidden(array $nodeDataRow): bool $hiddenBeforeDateTime = $nodeDataRow['hiddenbeforedatetime'] ? new \DateTimeImmutable($nodeDataRow['hiddenbeforedatetime']) : null; // Hidden after a date time, without getting already re-enabled by hidden before date time - afterward - if ($hiddenAfterDateTime != null + if ( + $hiddenAfterDateTime != null && $hiddenAfterDateTime < $now && ( $hiddenBeforeDateTime == null || $hiddenBeforeDateTime > $now - || $hiddenBeforeDateTime<= $hiddenAfterDateTime + || $hiddenBeforeDateTime <= $hiddenAfterDateTime ) ) { return true; } // Hidden before a date time, without getting enabled by hidden after date time - before - if ($hiddenBeforeDateTime != null + if ( + $hiddenBeforeDateTime != null && $hiddenBeforeDateTime > $now && ( $hiddenAfterDateTime == null @@ -553,6 +532,5 @@ private function isNodeHidden(array $nodeDataRow): bool } return false; - } } diff --git a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Bootstrap/FeatureContext.php b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Bootstrap/FeatureContext.php index 00421a6bea4..0e10464c0fc 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Bootstrap/FeatureContext.php +++ b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Bootstrap/FeatureContext.php @@ -12,7 +12,6 @@ use Neos\ContentRepository\BehavioralTests\TestSuite\Behavior\GherkinPyStringNodeBasedNodeTypeManagerFactory; use Neos\ContentRepository\BehavioralTests\TestSuite\Behavior\GherkinTableNodeBasedContentDimensionSourceFactory; use Neos\ContentRepository\Core\ContentRepository; -use Neos\ContentRepository\Core\ContentRepositoryReadModel; use Neos\ContentRepository\Core\EventStore\EventNormalizer; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceFactoryDependencies; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceFactoryInterface; @@ -21,7 +20,6 @@ use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; -use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepository\Export\Asset\AssetExporter; use Neos\ContentRepository\Export\Asset\AssetLoaderInterface; use Neos\ContentRepository\Export\Asset\ResourceLoaderInterface; @@ -29,7 +27,7 @@ use Neos\ContentRepository\Export\Asset\ValueObject\SerializedImageVariant; use Neos\ContentRepository\Export\Asset\ValueObject\SerializedResource; use Neos\ContentRepository\Export\Event\ValueObject\ExportedEvents; -use Neos\ContentRepository\Export\ProcessorResult; +use Neos\ContentRepository\Export\ProcessingContext; use Neos\ContentRepository\Export\Severity; use Neos\ContentRepository\LegacyNodeMigration\NodeDataToAssetsProcessor; use Neos\ContentRepository\LegacyNodeMigration\NodeDataToEventsProcessor; @@ -59,7 +57,7 @@ class FeatureContext implements Context private InMemoryFilesystemAdapter $mockFilesystemAdapter; private Filesystem $mockFilesystem; - private ProcessorResult|null $lastMigrationResult = null; + private \Throwable|null $lastMigrationException = null; /** * @var array @@ -89,8 +87,8 @@ public function __construct() */ public function failIfLastMigrationHasErrors(): void { - if ($this->lastMigrationResult !== null && $this->lastMigrationResult->severity === Severity::ERROR) { - throw new \RuntimeException(sprintf('The last migration run led to an error: %s', $this->lastMigrationResult->message)); + if ($this->lastMigrationException !== null) { + throw new \RuntimeException(sprintf('The last migration run led to an exception: %s', $this->lastMigrationException->getMessage())); } if ($this->loggedErrors !== []) { throw new \RuntimeException(sprintf('The last migration run logged %d error%s', count($this->loggedErrors), count($this->loggedErrors) === 1 ? '' : 's')); @@ -146,20 +144,23 @@ public function build(ContentRepositoryServiceFactoryDependencies $serviceFactor $propertyConverterAccess->propertyConverter, $this->currentContentRepository->getVariationGraph(), $this->getObject(EventNormalizer::class), - $this->mockFilesystem, $this->nodeDataRows ); if ($contentStream !== null) { $migration->setContentStreamId(ContentStreamId::fromString($contentStream)); } - $migration->onMessage(function (Severity $severity, string $message) { + $processingContext = new ProcessingContext($this->mockFilesystem, function (Severity $severity, string $message) { if ($severity === Severity::ERROR) { $this->loggedErrors[] = $message; } elseif ($severity === Severity::WARNING) { $this->loggedWarnings[] = $message; } }); - $this->lastMigrationResult = $migration->run(); + try { + $migration->run($processingContext); + } catch (\Throwable $e) { + $this->lastMigrationException = $e; + } } /** @@ -223,12 +224,11 @@ public function iExpectTheFollowingWarningsToBeLogged(TableNode $table): void */ public function iExpectAMigrationErrorWithTheMessage(PyStringNode $expectedMessage = null): void { - Assert::assertNotNull($this->lastMigrationResult, 'Expected the previous migration to contain errors, but no migration has been executed'); - Assert::assertSame(Severity::ERROR, $this->lastMigrationResult->severity, sprintf('Expected the previous migration to contain errors, but it ended with severity "%s"', $this->lastMigrationResult->severity->name)); + Assert::assertNotNull($this->lastMigrationException, 'Expected the previous migration to lead to an exception, but no exception was thrown'); if ($expectedMessage !== null) { - Assert::assertSame($expectedMessage->getRaw(), $this->lastMigrationResult->message); + Assert::assertSame($expectedMessage->getRaw(), $this->lastMigrationException->getMessage()); } - $this->lastMigrationResult = null; + $this->lastMigrationException = null; } /** @@ -293,8 +293,8 @@ public function theFollowingImageVariantsExist(TableNode $imageVariants): void public function iRunTheAssetMigration(): void { $nodeTypeManager = $this->currentContentRepository->getNodeTypeManager(); - $mockResourceLoader = new class ($this->mockResources) implements ResourceLoaderInterface { - + $mockResourceLoader = new class ($this->mockResources) implements ResourceLoaderInterface + { /** * @param array $mockResources */ @@ -329,14 +329,18 @@ public function findAssetById(string $assetId): SerializedAsset|SerializedImageV $this->mockFilesystemAdapter->deleteEverything(); $assetExporter = new AssetExporter($this->mockFilesystem, $mockAssetLoader, $mockResourceLoader); $migration = new NodeDataToAssetsProcessor($nodeTypeManager, $assetExporter, $this->nodeDataRows); - $migration->onMessage(function (Severity $severity, string $message) { + $processingContext = new ProcessingContext($this->mockFilesystem, function (Severity $severity, string $message) { if ($severity === Severity::ERROR) { $this->loggedErrors[] = $message; } elseif ($severity === Severity::WARNING) { $this->loggedWarnings[] = $message; } }); - $this->lastMigrationResult = $migration->run(); + try { + $migration->run($processingContext); + } catch (\Throwable $e) { + $this->lastMigrationException = $e; + } } /** diff --git a/Neos.Neos/Classes/Command/CrCommandController.php b/Neos.Neos/Classes/Command/CrCommandController.php deleted file mode 100644 index a6c2f0606ad..00000000000 --- a/Neos.Neos/Classes/Command/CrCommandController.php +++ /dev/null @@ -1,128 +0,0 @@ -contentRepositoryRegistry->get($contentRepositoryId); - - Files::createDirectoryRecursively($path); - $filesystem = new Filesystem(new LocalFilesystemAdapter($path)); - - $liveWorkspace = $contentRepositoryInstance->findWorkspaceByName(WorkspaceName::forLive()); - if ($liveWorkspace === null) { - throw new \RuntimeException('Failed to find live workspace', 1716652280); - } - - $exportService = $this->contentRepositoryRegistry->buildService( - $contentRepositoryId, - new ExportServiceFactory( - $filesystem, - $liveWorkspace, - $this->assetRepository, - $this->assetUsageService, - ) - ); - assert($exportService instanceof ExportService); - $exportService->runAllProcessors($this->outputLine(...), $verbose); - $this->outputLine('Done'); - } - - /** - * Import the events from the path into the specified content repository - * - * @param string $path The path of the stored events like resource://Neos.Demo/Private/Content - * @param string $contentRepository The content repository identifier - * @param bool $verbose If set, all notices will be rendered - * @throws \Exception - */ - public function importCommand(string $path, string $contentRepository = 'default', bool $verbose = false): void - { - $filesystem = new Filesystem(new LocalFilesystemAdapter($path)); - - $contentRepositoryId = ContentRepositoryId::fromString($contentRepository); - $contentStreamIdentifier = ContentStreamId::create(); - - $importService = $this->contentRepositoryRegistry->buildService( - $contentRepositoryId, - new ImportServiceFactory( - $filesystem, - $contentStreamIdentifier, - $this->assetRepository, - $this->resourceRepository, - $this->resourceManager, - $this->persistenceManager, - ) - ); - assert($importService instanceof ImportService); - try { - $importService->runAllProcessors($this->outputLine(...), $verbose); - } catch (\RuntimeException $exception) { - $this->outputLine('Error: ' . $exception->getMessage() . ''); - $this->outputLine('Import stopped.'); - return; - } - - $this->outputLine('Replaying projections'); - - $projectionService = $this->contentRepositoryRegistry->buildService($contentRepositoryId, $this->projectionReplayServiceFactory); - $projectionService->replayAllProjections(CatchUpOptions::create()); - - $this->outputLine('Assigning live workspace role'); - // set the live-workspace title to (implicitly) create the metadata record for this workspace - $this->workspaceService->setWorkspaceTitle($contentRepositoryId, WorkspaceName::forLive(), WorkspaceTitle::fromString('Live workspace')); - $this->workspaceService->assignWorkspaceRole($contentRepositoryId, WorkspaceName::forLive(), WorkspaceRoleAssignment::createForGroup('Neos.Neos:LivePublisher', WorkspaceRole::COLLABORATOR)); - - $this->outputLine('Done'); - } -} diff --git a/Neos.Neos/Classes/Command/SiteCommandController.php b/Neos.Neos/Classes/Command/SiteCommandController.php index c3763cf7252..137a751488d 100644 --- a/Neos.Neos/Classes/Command/SiteCommandController.php +++ b/Neos.Neos/Classes/Command/SiteCommandController.php @@ -14,11 +14,16 @@ namespace Neos\Neos\Command; +use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\Exception\NodeNameIsAlreadyCovered; use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeNotFound; +use Neos\ContentRepository\Export\ProcessorEventInterface; +use Neos\ContentRepository\Export\Severity; +use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Annotations as Flow; use Neos\Flow\Cli\CommandController; use Neos\Flow\Cli\Exception\StopCommandException; +use Neos\Flow\ObjectManagement\DependencyInjection\DependencyProxy; use Neos\Flow\Package\PackageManager; use Neos\Flow\Persistence\PersistenceManagerInterface; use Neos\Neos\Domain\Exception\SiteNodeNameIsAlreadyInUseByAnotherSite; @@ -26,6 +31,8 @@ use Neos\Neos\Domain\Model\Site; use Neos\Neos\Domain\Repository\SiteRepository; use Neos\Neos\Domain\Service\NodeTypeNameFactory; +use Neos\Neos\Domain\Service\SiteImportService; +use Neos\Neos\Domain\Service\SiteImportServiceFactory; use Neos\Neos\Domain\Service\SiteService; /** @@ -53,12 +60,24 @@ class SiteCommandController extends CommandController */ protected $packageManager; + /** + * @Flow\Inject + * @var ContentRepositoryRegistry + */ + protected $contentRepositoryRegistry; + /** * @Flow\Inject * @var PersistenceManagerInterface */ protected $persistenceManager; + /** + * @Flow\Inject(lazy=false) + * @var SiteImportServiceFactory + */ + protected $siteImportServiceFactory; + /** * Create a new site * @@ -110,6 +129,28 @@ public function createCommand($name, $packageKey, $nodeType, $nodeName = null, $ ); } + public function importCommand(string $packageKey, string $contentRepository = 'default', bool $verbose = false): void + { + $contentRepositoryId = ContentRepositoryId::fromString($contentRepository); + $importService = $this->contentRepositoryRegistry->buildService($contentRepositoryId, $this->siteImportServiceFactory); + assert($importService instanceof SiteImportService); + + $onProcessor = function (string $processorLabel) { + $this->outputLine('%s...', [$processorLabel]); + }; + $onMessage = function (Severity $severity, string $message) use ($verbose) { + if (!$verbose && $severity === Severity::NOTICE) { + return; + } + $this->outputLine(match ($severity) { + Severity::NOTICE => $message, + Severity::WARNING => sprintf('Warning: %s', $message), + Severity::ERROR => sprintf('Error: %s', $message), + }); + }; + $importService->importFromPackage($packageKey, $onProcessor, $onMessage); + } + /** * Remove site with content and related data (with globbing) * diff --git a/Neos.Neos/Classes/Domain/Import/DoctrineMigrateProcessor.php b/Neos.Neos/Classes/Domain/Import/DoctrineMigrateProcessor.php new file mode 100644 index 00000000000..632cdc8a6b5 --- /dev/null +++ b/Neos.Neos/Classes/Domain/Import/DoctrineMigrateProcessor.php @@ -0,0 +1,32 @@ +doctrineService->executeMigrations(); + } +} diff --git a/Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php b/Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php new file mode 100644 index 00000000000..e4d6c0c6d92 --- /dev/null +++ b/Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php @@ -0,0 +1,104 @@ +files->has('sites.json')) { + $sitesJson = $context->files->read('sites.json'); + try { + $sites = json_decode($sitesJson, true, 512, JSON_THROW_ON_ERROR); + } catch (JsonException $e) { + throw new \RuntimeException("Failed to decode sites.json: {$e->getMessage()}", 1729506117, $e); + } + } else { + $sites = self::extractSitesFromEventStream($context); + } + /** @var SiteShape $site */ + foreach ($sites as $site) { + $context->dispatch(Severity::NOTICE, "Creating site \"{$site['name']}\""); + + $siteNodeName = !empty($site['nodeName']) ? NodeName::fromString($site['nodeName']) : NodeName::transliterateFromString($site['name']); + if ($this->siteRepository->findOneByNodeName($siteNodeName->value)) { + $context->dispatch(Severity::NOTICE, "Site for node name \"{$siteNodeName->value}\" already exists, skipping"); + continue; + } + + // TODO use node aggregate identifier instead of node name + $siteInstance = new Site($siteNodeName->value); + $siteInstance->setSiteResourcesPackageKey($site['packageKey']); + $siteInstance->setState(($site['inactive'] ?? false) ? Site::STATE_OFFLINE : Site::STATE_ONLINE); + $siteInstance->setName($site['name']); + $this->siteRepository->add($siteInstance); + + // TODO add domains? + } + } + + /** + * @return array + */ + private static function extractSitesFromEventStream(ProcessingContext $context): array + { + $eventFileResource = $context->files->readStream('events.jsonl'); + $rootNodeAggregateIds = []; + $sites = []; + while (($line = fgets($eventFileResource)) !== false) { + $event = ExportedEvent::fromJson($line); + if ($event->type === 'RootNodeAggregateWithNodeWasCreated') { + $rootNodeAggregateIds[] = $event->payload['nodeAggregateId']; + continue; + } + if ($event->type === 'NodeAggregateWithNodeWasCreated' && in_array($event->payload['parentNodeAggregateId'], $rootNodeAggregateIds, true)) { + $sites[] = [ + 'packageKey' => self::extractPackageKeyFromNodeTypeName($event->payload['nodeTypeName']), + 'name' => $event->payload['initialPropertyValues']['title']['value'] ?? $event->payload['nodeTypeName'], + 'nodeTypeName' => $event->payload['nodeTypeName'], + 'nodeName' => $event->payload['nodeName'] ?? null, + ]; + } + }; + return $sites; + } + + private static function extractPackageKeyFromNodeTypeName(string $nodeTypeName): string + { + if (preg_match('/^([^:])+/', $nodeTypeName, $matches) !== 1) { + throw new \RuntimeException("Failed to extract package key from '$nodeTypeName'.", 1729505701); + } + return $matches[0]; + } +} diff --git a/Neos.Neos/Classes/Domain/Service/SiteImportService.php b/Neos.Neos/Classes/Domain/Service/SiteImportService.php new file mode 100644 index 00000000000..39352751821 --- /dev/null +++ b/Neos.Neos/Classes/Domain/Service/SiteImportService.php @@ -0,0 +1,53 @@ +packageManager->getPackage($packageKey); + $path = Files::concatenatePaths([$package->getPackagePath(), 'Resources/Private/Content']); + if (!is_dir($path)) { + throw new \InvalidArgumentException(sprintf('No contents for package "%s" at path "%s"', $packageKey, $path), 1728912269); + } + $filesystem = new Filesystem(new LocalFilesystemAdapter($path)); + $context = new ProcessingContext($filesystem, $onMessage); + foreach ($this->processors as $processorLabel => $processor) { + ($onProcessor)($processorLabel); + $processor->run($context); + } + } +} diff --git a/Neos.Neos/Classes/Domain/Service/SiteImportServiceFactory.php b/Neos.Neos/Classes/Domain/Service/SiteImportServiceFactory.php new file mode 100644 index 00000000000..3ef71079a39 --- /dev/null +++ b/Neos.Neos/Classes/Domain/Service/SiteImportServiceFactory.php @@ -0,0 +1,67 @@ + + */ +#[Flow\Scope('singleton')] +final readonly class SiteImportServiceFactory implements ContentRepositoryServiceFactoryInterface +{ + public function __construct( + private PackageManager $packageManager, + private DoctrineService $doctrineService, + private SiteRepository $siteRepository, + private AssetRepository $assetRepository, + private ResourceRepository $resourceRepository, + private ResourceManager $resourceManager, + private PersistenceManagerInterface $persistenceManager, + ) { + } + + public function build(ContentRepositoryServiceFactoryDependencies $serviceFactoryDependencies): SiteImportService + { + // TODO: make configurable(?) + $processors = Processors::fromArray([ + 'Run doctrine migrations' => new DoctrineMigrateProcessor($this->doctrineService), + 'Setup content repository' => new ContentRepositorySetupProcessor($serviceFactoryDependencies->contentRepository), + 'Create Neos sites' => new SiteCreationProcessor($this->siteRepository), + // TODO create live workspace, etc + 'Import events' => new EventStoreImportProcessor(false, $serviceFactoryDependencies->eventStore, $serviceFactoryDependencies->eventNormalizer, null), + 'Import assets' => new AssetRepositoryImportProcessor($this->assetRepository, $this->resourceRepository, $this->resourceManager, $this->persistenceManager), + ]); + return new SiteImportService( + $processors, + $this->packageManager, + ); + } +} diff --git a/Neos.Neos/Classes/Domain/Service/SiteService.php b/Neos.Neos/Classes/Domain/Service/SiteService.php index 6041fceb202..75dc08b5aea 100644 --- a/Neos.Neos/Classes/Domain/Service/SiteService.php +++ b/Neos.Neos/Classes/Domain/Service/SiteService.php @@ -167,7 +167,7 @@ public function createSite( ?string $nodeName = null, bool $inactive = false ): Site { - $siteNodeName = NodeName::fromString($nodeName ?: $siteName); + $siteNodeName = NodeName::transliterateFromString($nodeName ?: $siteName); if ($this->siteRepository->findOneByNodeName($siteNodeName->value)) { throw SiteNodeNameIsAlreadyInUseByAnotherSite::butWasAttemptedToBeClaimed($siteNodeName); From d192496a6673ec38d139c36cd9e8ae219701b7e5 Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Mon, 21 Oct 2024 19:40:41 +0200 Subject: [PATCH 012/214] Extract workspace creation to separate processor --- .../Processors/EventStoreImportProcessor.php | 83 ++++--------------- .../Classes/LegacyMigrationService.php | 6 +- .../Classes/LegacyMigrationServiceFactory.php | 9 +- .../Import/LiveWorkspaceCreationProcessor.php | 50 +++++++++++ .../Service/SiteImportServiceFactory.php | 9 +- 5 files changed, 81 insertions(+), 76 deletions(-) create mode 100644 Neos.Neos/Classes/Domain/Import/LiveWorkspaceCreationProcessor.php diff --git a/Neos.ContentRepository.Export/src/Processors/EventStoreImportProcessor.php b/Neos.ContentRepository.Export/src/Processors/EventStoreImportProcessor.php index e35d882db95..7af1561c16f 100644 --- a/Neos.ContentRepository.Export/src/Processors/EventStoreImportProcessor.php +++ b/Neos.ContentRepository.Export/src/Processors/EventStoreImportProcessor.php @@ -4,6 +4,7 @@ namespace Neos\ContentRepository\Export\Processors; +use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\EventStore\DecoratedEvent; use Neos\ContentRepository\Core\EventStore\EventNormalizer; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface; @@ -29,19 +30,15 @@ /** * Processor that imports all events from an "events.jsonl" file to the event store */ -final class EventStoreImportProcessor implements ProcessorInterface, ContentRepositoryServiceInterface +final readonly class EventStoreImportProcessor implements ProcessorInterface, ContentRepositoryServiceInterface { - private ?ContentStreamId $contentStreamId = null; - public function __construct( - private readonly bool $keepEventIds, - private readonly EventStoreInterface $eventStore, - private readonly EventNormalizer $eventNormalizer, - ?ContentStreamId $overrideContentStreamId + private WorkspaceName $targetWorkspaceName, + private bool $keepEventIds, + private EventStoreInterface $eventStore, + private EventNormalizer $eventNormalizer, + private ContentRepository $contentRepository, ) { - if ($overrideContentStreamId) { - $this->contentStreamId = $overrideContentStreamId; - } } public function run(ProcessingContext $context): void @@ -53,16 +50,15 @@ public function run(ProcessingContext $context): void /** @var array $eventIdMap */ $eventIdMap = []; - $keepStreamName = false; + $workspace = $this->contentRepository->findWorkspaceByName($this->targetWorkspaceName); + if ($workspace === null) { + throw new \InvalidArgumentException("Workspace {$this->targetWorkspaceName} does not exist", 1729530978); + } + while (($line = fgets($eventFileResource)) !== false) { - $event = ExportedEvent::fromJson(trim($line)); - if ($this->contentStreamId === null) { - $this->contentStreamId = self::extractContentStreamId($event->payload); - $keepStreamName = true; - } - if (!$keepStreamName) { - $event = $event->processPayload(fn(array $payload) => isset($payload['contentStreamId']) ? [...$payload, 'contentStreamId' => $this->contentStreamId->value] : $payload); - } + $event = + ExportedEvent::fromJson(trim($line)) + ->processPayload(fn (array $payload) => [...$payload, 'contentStreamId' => $workspace->currentContentStreamId->value, 'workspaceName' => $this->targetWorkspaceName->value]); if (!$this->keepEventIds) { try { $newEventId = Algorithms::generateUUID(); @@ -102,56 +98,11 @@ public function run(ProcessingContext $context): void $domainEvents[] = $this->eventNormalizer->normalize($domainEvent); } - assert($this->contentStreamId !== null); - - $contentStreamStreamName = ContentStreamEventStreamName::fromContentStreamId($this->contentStreamId)->getEventStreamName(); - $events = Events::with( - $this->eventNormalizer->normalize( - new ContentStreamWasCreated( - $this->contentStreamId, - ) - ) - ); - try { - $contentStreamCreationCommitResult = $this->eventStore->commit($contentStreamStreamName, $events, ExpectedVersion::NO_STREAM()); - } catch (ConcurrencyException $e) { - throw new \RuntimeException(sprintf('Failed to publish workspace events because the event stream "%s" already exists (1)', $this->contentStreamId->value), 1729506776, $e); - } - - $workspaceName = WorkspaceName::forLive(); - $workspaceStreamName = WorkspaceEventStreamName::fromWorkspaceName($workspaceName)->getEventStreamName(); - $events = Events::with( - $this->eventNormalizer->normalize( - new RootWorkspaceWasCreated( - $workspaceName, - $this->contentStreamId - ) - ) - ); - try { - $this->eventStore->commit($workspaceStreamName, $events, ExpectedVersion::NO_STREAM()); - } catch (ConcurrencyException $e) { - throw new \RuntimeException(sprintf('Failed to publish workspace events because the event stream "%s" already exists (2)', $workspaceStreamName->value), 1729506798, $e); - } - + $contentStreamStreamName = ContentStreamEventStreamName::fromContentStreamId($workspace->currentContentStreamId)->getEventStreamName(); try { - $this->eventStore->commit($contentStreamStreamName, Events::fromArray($domainEvents), ExpectedVersion::fromVersion($contentStreamCreationCommitResult->highestCommittedVersion)); + $this->eventStore->commit($contentStreamStreamName, Events::fromArray($domainEvents), ExpectedVersion::ANY()); } catch (ConcurrencyException $e) { throw new \RuntimeException(sprintf('Failed to publish %d events because the event stream "%s" already exists (3)', count($domainEvents), $contentStreamStreamName->value), 1729506818, $e); } } - - /** --------------------------- */ - - /** - * @param array $payload - * @return ContentStreamId - */ - private static function extractContentStreamId(array $payload): ContentStreamId - { - if (!isset($payload['contentStreamId']) || !is_string($payload['contentStreamId'])) { - throw new \RuntimeException('Failed to extract "contentStreamId" from event', 1646404169); - } - return ContentStreamId::fromString($payload['contentStreamId']); - } } diff --git a/Neos.ContentRepository.LegacyNodeMigration/Classes/LegacyMigrationService.php b/Neos.ContentRepository.LegacyNodeMigration/Classes/LegacyMigrationService.php index 3c2ab1350d2..d057db43fb9 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Classes/LegacyMigrationService.php +++ b/Neos.ContentRepository.LegacyNodeMigration/Classes/LegacyMigrationService.php @@ -18,12 +18,14 @@ use Doctrine\DBAL\Connection; use League\Flysystem\Filesystem; use League\Flysystem\Local\LocalFilesystemAdapter; +use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\DimensionSpace\InterDimensionalVariationGraph; use Neos\ContentRepository\Core\EventStore\EventNormalizer; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface; use Neos\ContentRepository\Core\Infrastructure\Property\PropertyConverter; use Neos\ContentRepository\Core\NodeType\NodeTypeManager; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; +use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepository\Export\Asset\Adapters\DbalAssetLoader; use Neos\ContentRepository\Export\Asset\Adapters\FileSystemResourceLoader; use Neos\ContentRepository\Export\Asset\AssetExporter; @@ -58,7 +60,7 @@ public function __construct( private readonly EventNormalizer $eventNormalizer, private readonly PropertyConverter $propertyConverter, private readonly EventStoreInterface $eventStore, - private readonly ContentStreamId $contentStreamId, + private readonly ContentRepository $contentRepository, ) { } @@ -75,7 +77,7 @@ public function runAllProcessors(\Closure $outputLineFn, bool $verbose = false): 'Exporting assets' => new NodeDataToAssetsProcessor($this->nodeTypeManager, $assetExporter, new NodeDataLoader($this->connection)), 'Exporting node data' => new NodeDataToEventsProcessor($this->nodeTypeManager, $this->propertyMapper, $this->propertyConverter, $this->interDimensionalVariationGraph, $this->eventNormalizer, new NodeDataLoader($this->connection)), 'Importing assets' => new AssetRepositoryImportProcessor($this->assetRepository, $this->resourceRepository, $this->resourceManager, $this->persistenceManager), - 'Importing events' => new EventStoreImportProcessor(true, $this->eventStore, $this->eventNormalizer, $this->contentStreamId), + 'Importing events' => new EventStoreImportProcessor(WorkspaceName::forLive(), true, $this->eventStore, $this->eventNormalizer, $this->contentRepository), ]); $processingContext = new ProcessingContext($filesystem, function (Severity $severity, string $message) use ($verbose, $outputLineFn) { if ($severity !== Severity::NOTICE || $verbose) { diff --git a/Neos.ContentRepository.LegacyNodeMigration/Classes/LegacyMigrationServiceFactory.php b/Neos.ContentRepository.LegacyNodeMigration/Classes/LegacyMigrationServiceFactory.php index 67bd7df05c5..80ab8a5f84a 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Classes/LegacyMigrationServiceFactory.php +++ b/Neos.ContentRepository.LegacyNodeMigration/Classes/LegacyMigrationServiceFactory.php @@ -1,4 +1,5 @@ connection, $this->resourcesPath, @@ -62,7 +59,7 @@ public function build( $serviceFactoryDependencies->eventNormalizer, $serviceFactoryDependencies->propertyConverter, $serviceFactoryDependencies->eventStore, - $this->contentStreamId, + $serviceFactoryDependencies->contentRepository, ); } } diff --git a/Neos.Neos/Classes/Domain/Import/LiveWorkspaceCreationProcessor.php b/Neos.Neos/Classes/Domain/Import/LiveWorkspaceCreationProcessor.php new file mode 100644 index 00000000000..34a140eb7bd --- /dev/null +++ b/Neos.Neos/Classes/Domain/Import/LiveWorkspaceCreationProcessor.php @@ -0,0 +1,50 @@ +dispatch(Severity::NOTICE, 'Creating live workspace'); + $existingWorkspace = $this->contentRepository->findWorkspaceByName(WorkspaceName::forLive()); + if ($existingWorkspace !== null) { + $context->dispatch(Severity::NOTICE, 'Workspace already exists, skipping'); + return; + } + $this->workspaceService->createRootWorkspace($this->contentRepository->id, WorkspaceName::forLive(), WorkspaceTitle::fromString('Live workspace'), WorkspaceDescription::fromString('')); + $this->workspaceService->assignWorkspaceRole($this->contentRepository->id, WorkspaceName::forLive(), WorkspaceRoleAssignment::createForGroup('Neos.Neos:LivePublisher', WorkspaceRole::COLLABORATOR)); + } +} diff --git a/Neos.Neos/Classes/Domain/Service/SiteImportServiceFactory.php b/Neos.Neos/Classes/Domain/Service/SiteImportServiceFactory.php index 3ef71079a39..10873e40d3b 100644 --- a/Neos.Neos/Classes/Domain/Service/SiteImportServiceFactory.php +++ b/Neos.Neos/Classes/Domain/Service/SiteImportServiceFactory.php @@ -16,6 +16,8 @@ use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceFactoryDependencies; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceFactoryInterface; +use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; +use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepository\Export\Processors; use Neos\ContentRepository\Export\Processors\AssetRepositoryImportProcessor; use Neos\ContentRepository\Export\Processors\ContentRepositorySetupProcessor; @@ -29,6 +31,7 @@ use Neos\Media\Domain\Repository\AssetRepository; use Neos\Neos\Domain\Import\DoctrineMigrateProcessor; use Neos\Neos\Domain\Import\SiteCreationProcessor; +use Neos\Neos\Domain\Import\LiveWorkspaceCreationProcessor; use Neos\Neos\Domain\Repository\SiteRepository; /** @@ -45,6 +48,7 @@ public function __construct( private ResourceRepository $resourceRepository, private ResourceManager $resourceManager, private PersistenceManagerInterface $persistenceManager, + private WorkspaceService $workspaceService, ) { } @@ -54,9 +58,10 @@ public function build(ContentRepositoryServiceFactoryDependencies $serviceFactor $processors = Processors::fromArray([ 'Run doctrine migrations' => new DoctrineMigrateProcessor($this->doctrineService), 'Setup content repository' => new ContentRepositorySetupProcessor($serviceFactoryDependencies->contentRepository), + // TODO Check if target content stream is empty, otherwise => nice error "prune..." 'Create Neos sites' => new SiteCreationProcessor($this->siteRepository), - // TODO create live workspace, etc - 'Import events' => new EventStoreImportProcessor(false, $serviceFactoryDependencies->eventStore, $serviceFactoryDependencies->eventNormalizer, null), + 'Create Live workspace' => new LiveWorkspaceCreationProcessor($serviceFactoryDependencies->contentRepository, $this->workspaceService), + 'Import events' => new EventStoreImportProcessor(WorkspaceName::forLive(), true, $serviceFactoryDependencies->eventStore, $serviceFactoryDependencies->eventNormalizer, $serviceFactoryDependencies->contentRepository), 'Import assets' => new AssetRepositoryImportProcessor($this->assetRepository, $this->resourceRepository, $this->resourceManager, $this->persistenceManager), ]); return new SiteImportService( From 7f509682079d538233b64b13a12ef8cb07db5513 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Tue, 22 Oct 2024 10:50:20 +0200 Subject: [PATCH 013/214] WIP: Introduce `FusionAutoIncludeHandler` to be mocked during testing --- .../Core/FusionSourceCodeCollection.php | 6 --- .../Service/FusionAutoIncludeHandler.php | 15 +++++++ .../Classes/Domain/Service/FusionService.php | 27 +++++------- .../Service/FusionSourceCodeFactory.php | 12 ++---- .../ResourceFusionAutoIncludeHandler.php | 22 ++++++++++ .../TestingFusionAutoIncludeHandler.php | 43 +++++++++++++++++++ Neos.Neos/Configuration/Objects.yaml | 3 ++ Neos.Neos/Configuration/Testing/Objects.yaml | 2 + .../Features/Bootstrap/DispatcherTrait.php | 36 +++++++++++++--- .../Features/Bootstrap/RoutingTrait.php | 5 ++- .../DefaultFusionRendering.feature | 4 +- 11 files changed, 134 insertions(+), 41 deletions(-) create mode 100644 Neos.Neos/Classes/Domain/Service/FusionAutoIncludeHandler.php create mode 100644 Neos.Neos/Classes/Domain/Service/ResourceFusionAutoIncludeHandler.php create mode 100644 Neos.Neos/Classes/Testing/TestingFusionAutoIncludeHandler.php create mode 100644 Neos.Neos/Configuration/Testing/Objects.yaml diff --git a/Neos.Fusion/Classes/Core/FusionSourceCodeCollection.php b/Neos.Fusion/Classes/Core/FusionSourceCodeCollection.php index 54218f62f23..7b04aa1c5dd 100644 --- a/Neos.Fusion/Classes/Core/FusionSourceCodeCollection.php +++ b/Neos.Fusion/Classes/Core/FusionSourceCodeCollection.php @@ -52,12 +52,6 @@ public static function tryFromFilePath(string $filePath): self return self::fromFilePath($filePath); } - public static function tryFromPackageRootFusion(string $packageKey): self - { - $fusionPathAndFilename = sprintf('resource://%s/Private/Fusion/Root.fusion', $packageKey); - return self::tryFromFilePath($fusionPathAndFilename); - } - public static function empty(): self { return new self(); diff --git a/Neos.Neos/Classes/Domain/Service/FusionAutoIncludeHandler.php b/Neos.Neos/Classes/Domain/Service/FusionAutoIncludeHandler.php new file mode 100644 index 00000000000..b167eff1bdf --- /dev/null +++ b/Neos.Neos/Classes/Domain/Service/FusionAutoIncludeHandler.php @@ -0,0 +1,15 @@ +additionalFusionSourceCode = FusionSourceCodeCollection::fromString($additionalFusionSourceCode); - } + protected $fusionAutoIncludeHandler; public function createFusionConfigurationFromSite(Site $site): FusionConfiguration { return $this->fusionConfigurationCache->cacheFusionConfigurationBySite($site, function () use ($site) { $siteResourcesPackageKey = $site->getSiteResourcesPackageKey(); - return $this->fusionParser->parseFromSource( - $this->fusionSourceCodeFactory->createFromNodeTypeDefinitions($site->getConfiguration()->contentRepositoryId) - ->union( - $this->fusionSourceCodeFactory->createFromAutoIncludes() - ) - ->union( - FusionSourceCodeCollection::tryFromPackageRootFusion($siteResourcesPackageKey) - )->union( - $this->additionalFusionSourceCode ?? FusionSourceCodeCollection::empty() - ) + $this->fusionAutoIncludeHandler->loadFusionFromPackage( + $siteResourcesPackageKey, + $this->fusionSourceCodeFactory->createFromNodeTypeDefinitions($site->getConfiguration()->contentRepositoryId) + ->union( + $this->fusionSourceCodeFactory->createFromAutoIncludes() + ) + ) ); }); } diff --git a/Neos.Neos/Classes/Domain/Service/FusionSourceCodeFactory.php b/Neos.Neos/Classes/Domain/Service/FusionSourceCodeFactory.php index 948994053cb..657d2ef94ff 100644 --- a/Neos.Neos/Classes/Domain/Service/FusionSourceCodeFactory.php +++ b/Neos.Neos/Classes/Domain/Service/FusionSourceCodeFactory.php @@ -36,6 +36,9 @@ class FusionSourceCodeFactory #[Flow\InjectConfiguration("fusion.autoInclude")] protected array $autoIncludeConfiguration = []; + #[Flow\Inject] + protected FusionAutoIncludeHandler $fusionAutoIncludeHandler; + #[Flow\Inject] protected ContentRepositoryRegistry $contentRepositoryRegistry; @@ -50,19 +53,12 @@ public function createFromAutoIncludes(): FusionSourceCodeCollection $sourcecode = FusionSourceCodeCollection::empty(); foreach (array_keys($this->packageManager->getAvailablePackages()) as $packageKey) { if (isset($this->autoIncludeConfiguration[$packageKey]) && $this->autoIncludeConfiguration[$packageKey] === true) { - $sourcecode = $sourcecode->union( - FusionSourceCodeCollection::tryFromPackageRootFusion($packageKey) - ); + $sourcecode = $this->fusionAutoIncludeHandler->loadFusionFromPackage($packageKey, $sourcecode); } } return $sourcecode; } - public function createFromSite(Site $site): FusionSourceCodeCollection - { - return FusionSourceCodeCollection::tryFromPackageRootFusion($site->getSiteResourcesPackageKey()); - } - /** * Generate Fusion prototype definitions for all node types * diff --git a/Neos.Neos/Classes/Domain/Service/ResourceFusionAutoIncludeHandler.php b/Neos.Neos/Classes/Domain/Service/ResourceFusionAutoIncludeHandler.php new file mode 100644 index 00000000000..38365f9ea68 --- /dev/null +++ b/Neos.Neos/Classes/Domain/Service/ResourceFusionAutoIncludeHandler.php @@ -0,0 +1,22 @@ +union( + FusionSourceCodeCollection::tryFromFilePath(sprintf('resource://%s/Private/Fusion/Root.fusion', $packageKey)) + ); + } +} diff --git a/Neos.Neos/Classes/Testing/TestingFusionAutoIncludeHandler.php b/Neos.Neos/Classes/Testing/TestingFusionAutoIncludeHandler.php new file mode 100644 index 00000000000..356f1aa318b --- /dev/null +++ b/Neos.Neos/Classes/Testing/TestingFusionAutoIncludeHandler.php @@ -0,0 +1,43 @@ +overrideHandler = $overrideHandler; + } + + public function resetOverride(): void + { + $this->overrideHandler = null; + } + + public function loadFusionFromPackage(string $packageKey, FusionSourceCodeCollection $sourceCodeCollection): FusionSourceCodeCollection + { + if ($this->overrideHandler !== null) { + return $this->overrideHandler->loadFusionFromPackage($packageKey, $sourceCodeCollection); + } else { + return $this->resourceFusionAutoIncludeHandler->loadFusionFromPackage($packageKey, $sourceCodeCollection); + } + } +} diff --git a/Neos.Neos/Configuration/Objects.yaml b/Neos.Neos/Configuration/Objects.yaml index be5cd60dd91..20da5dd98aa 100644 --- a/Neos.Neos/Configuration/Objects.yaml +++ b/Neos.Neos/Configuration/Objects.yaml @@ -20,6 +20,9 @@ Neos\Neos\Domain\Service\FusionConfigurationCache: 2: setting: "Neos.Neos.fusion.enableObjectTreeCache" +Neos\Neos\Domain\Service\FusionAutoIncludeHandler: + className: Neos\Neos\Domain\Service\ResourceFusionAutoIncludeHandler + Neos\Fusion\Core\Cache\RuntimeContentCache: properties: serializer: diff --git a/Neos.Neos/Configuration/Testing/Objects.yaml b/Neos.Neos/Configuration/Testing/Objects.yaml new file mode 100644 index 00000000000..faa9e83523a --- /dev/null +++ b/Neos.Neos/Configuration/Testing/Objects.yaml @@ -0,0 +1,2 @@ +Neos\Neos\Domain\Service\FusionAutoIncludeHandler: + className: Neos\Neos\Testing\TestingFusionAutoIncludeHandler diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/DispatcherTrait.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/DispatcherTrait.php index fa9a6d68f0e..9293d79559d 100644 --- a/Neos.Neos/Tests/Behavior/Features/Bootstrap/DispatcherTrait.php +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/DispatcherTrait.php @@ -44,18 +44,42 @@ abstract private function getObject(string $className): object; public function setupDispatcherTest(): void { $this->getObject(ContentCache::class)->flush(); + $this->getObject(\Neos\Neos\Testing\TestingFusionAutoIncludeHandler::class)->resetOverride(); $this->response = null; } /** - * @When the sites Fusion code is: + * @When the Fusion code for package :package is: */ - public function iHaveTheFollowingFusionCodeForTheSite(PyStringNode $fusionCode) + public function iHaveTheFollowingFusionCodeForTheSite(PyStringNode $fusionCode, string $package) { - $this->getObject( - FusionService::class - )->unsafeSetAdditionalFusionSourceCodeToThisSingleton( - $fusionCode->getRaw() + $this->getObject(\Neos\Neos\Testing\TestingFusionAutoIncludeHandler::class)->overrideHandler( + new class ($package, $fusionCode->getRaw(), $this->getObject(\Neos\Neos\Domain\Service\ResourceFusionAutoIncludeHandler::class)) implements \Neos\Neos\Domain\Service\FusionAutoIncludeHandler + { + public function __construct( + private string $package, + private string $fusionCode, + private \Neos\Neos\Domain\Service\ResourceFusionAutoIncludeHandler $defaultHandler, + ) { + } + + public function loadFusionFromPackage( + string $packageKey, + \Neos\Fusion\Core\FusionSourceCodeCollection $sourceCodeCollection + ): \Neos\Fusion\Core\FusionSourceCodeCollection { + if ($packageKey === $this->package) { + return $sourceCodeCollection->union( + \Neos\Fusion\Core\FusionSourceCodeCollection::fromString( + $this->fusionCode + ) + ); + } elseif (in_array($packageKey, ['Neos.Neos', 'Neos.Fusion'])) { + return $this->defaultHandler->loadFusionFromPackage($packageKey, $sourceCodeCollection); + } else { + return $sourceCodeCollection; + } + } + } ); // $fakeFusionService = new class ($original) extends \Neos\Neos\Domain\Service\FusionService // { diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/RoutingTrait.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/RoutingTrait.php index 3eb2dc1a93a..f5232d55a10 100644 --- a/Neos.Neos/Tests/Behavior/Features/Bootstrap/RoutingTrait.php +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/RoutingTrait.php @@ -82,14 +82,15 @@ abstract private function getObject(string $className): object; /** * @Given A site exists for node name :nodeName * @Given A site exists for node name :nodeName and domain :domain + * @Given A site exists for node name :nodeName and domain :domain and package :package */ - public function theSiteExists(string $nodeName, string $domain = null): void + public function theSiteExists(string $nodeName, string $domain = null, string $package = null): void { $siteRepository = $this->getObject(SiteRepository::class); $persistenceManager = $this->getObject(PersistenceManagerInterface::class); $site = new Site($nodeName); - $site->setSiteResourcesPackageKey('Neos.Neos'); + $site->setSiteResourcesPackageKey($package ?: 'Neos.Neos'); $site->setState(Site::STATE_ONLINE); $siteRepository->add($site); diff --git a/Neos.Neos/Tests/Behavior/Features/FrontendNodeController/DefaultFusionRendering.feature b/Neos.Neos/Tests/Behavior/Features/FrontendNodeController/DefaultFusionRendering.feature index cb7689bf7cb..8e2d8dfcd12 100644 --- a/Neos.Neos/Tests/Behavior/Features/FrontendNodeController/DefaultFusionRendering.feature +++ b/Neos.Neos/Tests/Behavior/Features/FrontendNodeController/DefaultFusionRendering.feature @@ -52,7 +52,7 @@ Feature: Test the default Fusion rendering for a request | a1 | a | Neos.Neos:Test.DocumentType | {"uriPathSegment": "a1", "title": "Node a1"} | {"main": "a-tetherton" } | | | a1a1 | a-tetherton | Neos.Neos:Test.ContentType | {"text": "my first text"} | {} | | | a1a2 | a-tetherton | Neos.Neos:Test.ContentType | {"text": "my second text"} | {} | | - And A site exists for node name "a" and domain "http://localhost" + And A site exists for node name "a" and domain "http://localhost" and package "Vendor.Site" And the sites configuration is: """yaml Neos: @@ -67,7 +67,7 @@ Feature: Test the default Fusion rendering for a request """ Scenario: Default output - And the sites Fusion code is: + And the Fusion code for package "Vendor.Site" is: """fusion prototype(Neos.Neos:Test.DocumentType) < prototype(Neos.Neos:Page) { body { From 4d4e98d6a986dcdc0d0ed22828efbfde5c4e3e77 Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Tue, 22 Oct 2024 13:10:14 +0200 Subject: [PATCH 014/214] Make `SiteImportService` a singleton And don't rely on the `ContentRepositoryServiceFactoryInterface` --- ...ContentRepositorySetupProcessorFactory.php | 22 ++++++ .../Factories/EventExportProcessorFactory.php | 29 ++++++++ .../EventStoreImportProcessorFactory.php | 33 +++++++++ .../Classes/Command/SiteCommandController.php | 55 +++++++++++--- .../Domain/Service/SiteImportService.php | 54 ++++++++++---- .../Service/SiteImportServiceFactory.php | 72 ------------------- 6 files changed, 173 insertions(+), 92 deletions(-) create mode 100644 Neos.ContentRepository.Export/src/Factories/ContentRepositorySetupProcessorFactory.php create mode 100644 Neos.ContentRepository.Export/src/Factories/EventExportProcessorFactory.php create mode 100644 Neos.ContentRepository.Export/src/Factories/EventStoreImportProcessorFactory.php delete mode 100644 Neos.Neos/Classes/Domain/Service/SiteImportServiceFactory.php diff --git a/Neos.ContentRepository.Export/src/Factories/ContentRepositorySetupProcessorFactory.php b/Neos.ContentRepository.Export/src/Factories/ContentRepositorySetupProcessorFactory.php new file mode 100644 index 00000000000..076515afaaf --- /dev/null +++ b/Neos.ContentRepository.Export/src/Factories/ContentRepositorySetupProcessorFactory.php @@ -0,0 +1,22 @@ + + */ +final readonly class ContentRepositorySetupProcessorFactory implements ContentRepositoryServiceFactoryInterface +{ + public function build(ContentRepositoryServiceFactoryDependencies $serviceFactoryDependencies): ContentRepositorySetupProcessor + { + return new ContentRepositorySetupProcessor( + $serviceFactoryDependencies->contentRepository, + ); + } +} diff --git a/Neos.ContentRepository.Export/src/Factories/EventExportProcessorFactory.php b/Neos.ContentRepository.Export/src/Factories/EventExportProcessorFactory.php new file mode 100644 index 00000000000..6fe742cec21 --- /dev/null +++ b/Neos.ContentRepository.Export/src/Factories/EventExportProcessorFactory.php @@ -0,0 +1,29 @@ + + */ +final readonly class EventExportProcessorFactory implements ContentRepositoryServiceFactoryInterface +{ + public function __construct( + private ContentStreamId $contentStreamId, + ) { + } + + public function build(ContentRepositoryServiceFactoryDependencies $serviceFactoryDependencies): EventExportProcessor + { + return new EventExportProcessor( + $this->contentStreamId, + $serviceFactoryDependencies->eventStore, + ); + } +} diff --git a/Neos.ContentRepository.Export/src/Factories/EventStoreImportProcessorFactory.php b/Neos.ContentRepository.Export/src/Factories/EventStoreImportProcessorFactory.php new file mode 100644 index 00000000000..94b6aa2f8d4 --- /dev/null +++ b/Neos.ContentRepository.Export/src/Factories/EventStoreImportProcessorFactory.php @@ -0,0 +1,33 @@ + + */ +final readonly class EventStoreImportProcessorFactory implements ContentRepositoryServiceFactoryInterface +{ + public function __construct( + private WorkspaceName $targetWorkspaceName, + private bool $keepEventIds, + ) { + } + + public function build(ContentRepositoryServiceFactoryDependencies $serviceFactoryDependencies): EventStoreImportProcessor + { + return new EventStoreImportProcessor( + $this->targetWorkspaceName, + $this->keepEventIds, + $serviceFactoryDependencies->eventStore, + $serviceFactoryDependencies->eventNormalizer, + $serviceFactoryDependencies->contentRepository, + ); + } +} diff --git a/Neos.Neos/Classes/Command/SiteCommandController.php b/Neos.Neos/Classes/Command/SiteCommandController.php index 137a751488d..713cc2f4da6 100644 --- a/Neos.Neos/Classes/Command/SiteCommandController.php +++ b/Neos.Neos/Classes/Command/SiteCommandController.php @@ -34,6 +34,7 @@ use Neos\Neos\Domain\Service\SiteImportService; use Neos\Neos\Domain\Service\SiteImportServiceFactory; use Neos\Neos\Domain\Service\SiteService; +use Neos\Utility\Files; /** * The Site Command Controller @@ -73,10 +74,10 @@ class SiteCommandController extends CommandController protected $persistenceManager; /** - * @Flow\Inject(lazy=false) - * @var SiteImportServiceFactory + * @Flow\Inject + * @var SiteImportService */ - protected $siteImportServiceFactory; + protected $siteImportService; /** * Create a new site @@ -129,12 +130,46 @@ public function createCommand($name, $packageKey, $nodeType, $nodeName = null, $ ); } - public function importCommand(string $packageKey, string $contentRepository = 'default', bool $verbose = false): void + /** + * Import sites content + * + * This command allows for importing one or more sites or partial content from the file system. The format must + * be identical to that produced by the export command. + * + * If a path is specified, this command expects the corresponding directory to contain the exported files + * + * If a package key is specified, this command expects the export files to be located in the private resources + * directory of the given package (Resources/Private/Content). + * + * @param string|null $packageKey Package key specifying the package containing the sites content + * @param string|null $path relative or absolute path and filename to the export files + * @return void + */ + public function importCommand(string $packageKey = null, string $path = null, string $contentRepository = 'default', bool $verbose = false): void { - $contentRepositoryId = ContentRepositoryId::fromString($contentRepository); - $importService = $this->contentRepositoryRegistry->buildService($contentRepositoryId, $this->siteImportServiceFactory); - assert($importService instanceof SiteImportService); + $exceedingArguments = $this->request->getExceedingArguments(); + if (isset($exceedingArguments[0]) && $packageKey === null && $path === null) { + if (file_exists($exceedingArguments[0])) { + $path = $exceedingArguments[0]; + } elseif ($this->packageManager->isPackageAvailable($exceedingArguments[0])) { + $packageKey = $exceedingArguments[0]; + } + } + if ($packageKey === null && $path === null) { + $this->outputLine('You have to specify either --package-key or --filename'); + $this->quit(1); + } + + // Since this command uses a lot of memory when large sites are imported, we warn the user to watch for + // the confirmation of a successful import. + $this->outputLine('This command can use a lot of memory when importing sites with many resources.'); + $this->outputLine('If the import is successful, you will see a message saying "Import of site ... finished".'); + $this->outputLine('If you do not see this message, the import failed, most likely due to insufficient memory.'); + $this->outputLine('Increase the memory_limit configuration parameter of your php CLI to attempt to fix this.'); + $this->outputLine('Starting import...'); + $this->outputLine('---'); + $contentRepositoryId = ContentRepositoryId::fromString($contentRepository); $onProcessor = function (string $processorLabel) { $this->outputLine('%s...', [$processorLabel]); }; @@ -148,7 +183,11 @@ public function importCommand(string $packageKey, string $contentRepository = 'd Severity::ERROR => sprintf('Error: %s', $message), }); }; - $importService->importFromPackage($packageKey, $onProcessor, $onMessage); + if ($path === null) { + $package = $this->packageManager->getPackage($packageKey); + $path = Files::concatenatePaths([$package->getPackagePath(), 'Resources/Private/Content']); + } + $this->siteImportService->importFromPath($contentRepositoryId, $path, $onProcessor, $onMessage); } /** diff --git a/Neos.Neos/Classes/Domain/Service/SiteImportService.php b/Neos.Neos/Classes/Domain/Service/SiteImportService.php index 39352751821..d04a48ca9dd 100644 --- a/Neos.Neos/Classes/Domain/Service/SiteImportService.php +++ b/Neos.Neos/Classes/Domain/Service/SiteImportService.php @@ -16,19 +16,38 @@ use League\Flysystem\Filesystem; use League\Flysystem\Local\LocalFilesystemAdapter; -use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface; +use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; +use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; +use Neos\ContentRepository\Export\Factories\ContentRepositorySetupProcessorFactory; +use Neos\ContentRepository\Export\Factories\EventStoreImportProcessorFactory; use Neos\ContentRepository\Export\ProcessingContext; use Neos\ContentRepository\Export\ProcessorInterface; -use Neos\ContentRepository\Export\Processors; +use Neos\ContentRepository\Export\Processors\AssetRepositoryImportProcessor; use Neos\ContentRepository\Export\Severity; -use Neos\Flow\Package\PackageManager; -use Neos\Utility\Files; +use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; +use Neos\Flow\Annotations as Flow; +use Neos\Flow\Persistence\Doctrine\Service as DoctrineService; +use Neos\Flow\Persistence\PersistenceManagerInterface; +use Neos\Flow\ResourceManagement\ResourceManager; +use Neos\Flow\ResourceManagement\ResourceRepository; +use Neos\Media\Domain\Repository\AssetRepository; +use Neos\Neos\Domain\Import\DoctrineMigrateProcessor; +use Neos\Neos\Domain\Import\LiveWorkspaceCreationProcessor; +use Neos\Neos\Domain\Import\SiteCreationProcessor; +use Neos\Neos\Domain\Repository\SiteRepository; -final readonly class SiteImportService implements ContentRepositoryServiceInterface +#[Flow\Scope('singleton')] +final readonly class SiteImportService { public function __construct( - private Processors $processors, - private PackageManager $packageManager, + private ContentRepositoryRegistry $contentRepositoryRegistry, + private DoctrineService $doctrineService, + private SiteRepository $siteRepository, + private AssetRepository $assetRepository, + private ResourceRepository $resourceRepository, + private ResourceManager $resourceManager, + private PersistenceManagerInterface $persistenceManager, + private WorkspaceService $workspaceService, ) { } @@ -36,16 +55,27 @@ public function __construct( * @param \Closure(string): void $onProcessor Callback that is invoked for each {@see ProcessorInterface} that is processed * @param \Closure(Severity, string): void $onMessage Callback that is invoked whenever a {@see ProcessorInterface} dispatches a message */ - public function importFromPackage(string $packageKey, \Closure $onProcessor, \Closure $onMessage): void + public function importFromPath(ContentRepositoryId $contentRepositoryId, string $path, \Closure $onProcessor, \Closure $onMessage): void { - $package = $this->packageManager->getPackage($packageKey); - $path = Files::concatenatePaths([$package->getPackagePath(), 'Resources/Private/Content']); if (!is_dir($path)) { - throw new \InvalidArgumentException(sprintf('No contents for package "%s" at path "%s"', $packageKey, $path), 1728912269); + throw new \InvalidArgumentException(sprintf('Path "%s" is not a directory', $path), 1729593802); } $filesystem = new Filesystem(new LocalFilesystemAdapter($path)); $context = new ProcessingContext($filesystem, $onMessage); - foreach ($this->processors as $processorLabel => $processor) { + $contentRepository = $this->contentRepositoryRegistry->get($contentRepositoryId); + + // TODO make configurable (?) + /** @var array $processors */ + $processors = [ + 'Run doctrine migrations' => new DoctrineMigrateProcessor($this->doctrineService), + 'Setup content repository' => $this->contentRepositoryRegistry->buildService($contentRepositoryId, new ContentRepositorySetupProcessorFactory()), + // TODO Check if target content stream is empty, otherwise => nice error "prune..." + 'Create Neos sites' => new SiteCreationProcessor($this->siteRepository), + 'Create Live workspace' => new LiveWorkspaceCreationProcessor($contentRepository, $this->workspaceService), + 'Import events' => $this->contentRepositoryRegistry->buildService($contentRepositoryId, new EventStoreImportProcessorFactory(WorkspaceName::forLive(), keepEventIds: true)), + 'Import assets' => new AssetRepositoryImportProcessor($this->assetRepository, $this->resourceRepository, $this->resourceManager, $this->persistenceManager), + ]; + foreach ($processors as $processorLabel => $processor) { ($onProcessor)($processorLabel); $processor->run($context); } diff --git a/Neos.Neos/Classes/Domain/Service/SiteImportServiceFactory.php b/Neos.Neos/Classes/Domain/Service/SiteImportServiceFactory.php deleted file mode 100644 index 10873e40d3b..00000000000 --- a/Neos.Neos/Classes/Domain/Service/SiteImportServiceFactory.php +++ /dev/null @@ -1,72 +0,0 @@ - - */ -#[Flow\Scope('singleton')] -final readonly class SiteImportServiceFactory implements ContentRepositoryServiceFactoryInterface -{ - public function __construct( - private PackageManager $packageManager, - private DoctrineService $doctrineService, - private SiteRepository $siteRepository, - private AssetRepository $assetRepository, - private ResourceRepository $resourceRepository, - private ResourceManager $resourceManager, - private PersistenceManagerInterface $persistenceManager, - private WorkspaceService $workspaceService, - ) { - } - - public function build(ContentRepositoryServiceFactoryDependencies $serviceFactoryDependencies): SiteImportService - { - // TODO: make configurable(?) - $processors = Processors::fromArray([ - 'Run doctrine migrations' => new DoctrineMigrateProcessor($this->doctrineService), - 'Setup content repository' => new ContentRepositorySetupProcessor($serviceFactoryDependencies->contentRepository), - // TODO Check if target content stream is empty, otherwise => nice error "prune..." - 'Create Neos sites' => new SiteCreationProcessor($this->siteRepository), - 'Create Live workspace' => new LiveWorkspaceCreationProcessor($serviceFactoryDependencies->contentRepository, $this->workspaceService), - 'Import events' => new EventStoreImportProcessor(WorkspaceName::forLive(), true, $serviceFactoryDependencies->eventStore, $serviceFactoryDependencies->eventNormalizer, $serviceFactoryDependencies->contentRepository), - 'Import assets' => new AssetRepositoryImportProcessor($this->assetRepository, $this->resourceRepository, $this->resourceManager, $this->persistenceManager), - ]); - return new SiteImportService( - $processors, - $this->packageManager, - ); - } -} From 7b3ce80e9c56de85c18a558b68770d528123b506 Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Tue, 22 Oct 2024 13:17:20 +0200 Subject: [PATCH 015/214] Fix `LegacyMigrationService` initialization --- .../Classes/Command/CrCommandController.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/CrCommandController.php b/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/CrCommandController.php index 337a3aa66ce..342bad83647 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/CrCommandController.php +++ b/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/CrCommandController.php @@ -124,8 +124,6 @@ public function migrateLegacyDataCommand(bool $verbose = false, string $config = $projectionService->resetAllProjections(); $this->outputLine('Truncated events'); - $liveContentStreamId = ContentStreamId::create(); - $legacyMigrationService = $this->contentRepositoryRegistry->buildService( $contentRepositoryId, new LegacyMigrationServiceFactory( @@ -137,7 +135,6 @@ public function migrateLegacyDataCommand(bool $verbose = false, string $config = $this->resourceRepository, $this->resourceManager, $this->propertyMapper, - $liveContentStreamId ) ); assert($legacyMigrationService instanceof LegacyMigrationService); From c8b40bd1c60eeb9df70de2d8411e43e36592fdee Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Tue, 22 Oct 2024 14:13:44 +0200 Subject: [PATCH 016/214] Re-add `Neos.ContentRepository.Export` behat tests and centralize common behat features into Trait --- .../Bootstrap/CrImportExportTrait.php | 281 ++++++++++++++++++ .../Features/Bootstrap/FeatureContext.php | 78 +++++ .../Features/EventExportProcessor.feature | 43 +++ .../EventStoreImportProcessor.feature | 84 ++++++ .../Tests/Behavior/behat.yml.dist | 0 .../Behavior/Bootstrap/FeatureContext.php | 222 +------------- .../Tests/Behavior/Features/Errors.feature | 12 +- 7 files changed, 507 insertions(+), 213 deletions(-) create mode 100644 Neos.ContentRepository.Export/Tests/Behavior/Features/Bootstrap/CrImportExportTrait.php create mode 100644 Neos.ContentRepository.Export/Tests/Behavior/Features/Bootstrap/FeatureContext.php create mode 100644 Neos.ContentRepository.Export/Tests/Behavior/Features/EventExportProcessor.feature create mode 100644 Neos.ContentRepository.Export/Tests/Behavior/Features/EventStoreImportProcessor.feature create mode 100644 Neos.ContentRepository.Export/Tests/Behavior/behat.yml.dist diff --git a/Neos.ContentRepository.Export/Tests/Behavior/Features/Bootstrap/CrImportExportTrait.php b/Neos.ContentRepository.Export/Tests/Behavior/Features/Bootstrap/CrImportExportTrait.php new file mode 100644 index 00000000000..8f06d03cf64 --- /dev/null +++ b/Neos.ContentRepository.Export/Tests/Behavior/Features/Bootstrap/CrImportExportTrait.php @@ -0,0 +1,281 @@ + */ + private array $crImportExportTrait_loggedErrors = []; + + /** @var array */ + private array $crImportExportTrait_loggedWarnings = []; + + private function setupCrImportExportTrait(): void + { + $this->crImportExportTrait_filesystem = new Filesystem(new InMemoryFilesystemAdapter()); + } + + /** + * @AfterScenario + */ + public function failIfLastMigrationHasErrors(): void + { + if ($this->crImportExportTrait_lastMigrationException !== null) { + throw new \RuntimeException(sprintf('The last migration run led to an exception: %s', $this->crImportExportTrait_lastMigrationException->getMessage())); + } + if ($this->crImportExportTrait_loggedErrors !== []) { + throw new \RuntimeException(sprintf('The last migration run logged %d error%s', count($this->crImportExportTrait_loggedErrors), count($this->crImportExportTrait_loggedErrors) === 1 ? '' : 's')); + } + } + + private function runCrImportExportProcessors(ProcessorInterface ...$processors): void + { + $processingContext = new ProcessingContext($this->crImportExportTrait_filesystem, function (Severity $severity, string $message) { + if ($severity === Severity::ERROR) { + $this->crImportExportTrait_loggedErrors[] = $message; + } elseif ($severity === Severity::WARNING) { + $this->crImportExportTrait_loggedWarnings[] = $message; + } + }); + foreach ($processors as $processor) { + assert($processor instanceof ProcessorInterface); + try { + $processor->run($processingContext); + } catch (\Throwable $e) { + $this->crImportExportTrait_lastMigrationException = $e; + break; + } + } + } + + /** + * @When /^the events are exported$/ + */ + public function theEventsAreExported(): void + { + $eventExporter = $this->getContentRepositoryService(new EventExportProcessorFactory($this->currentContentRepository->findWorkspaceByName(WorkspaceName::forLive())->currentContentStreamId)); + assert($eventExporter instanceof EventExportProcessor); + $this->runCrImportExportProcessors($eventExporter); + } + + /** + * @When /^I import the events\.jsonl(?: into workspace "([^"]*)")?$/ + */ + public function iImportTheEventsJsonl(?string $workspace = null): void + { + $workspaceName = $workspace !== null ? WorkspaceName::fromString($workspace) : $this->currentWorkspaceName; + $eventImporter = $this->getContentRepositoryService(new EventStoreImportProcessorFactory($workspaceName, true)); + assert($eventImporter instanceof EventStoreImportProcessor); + $this->runCrImportExportProcessors($eventImporter); + } + + /** + * @Given /^using the following events\.jsonl:$/ + */ + public function usingTheFollowingEventsJsonl(PyStringNode $string): void + { + $this->crImportExportTrait_filesystem->write('events.jsonl', $string->getRaw()); + } + + /** + * @Then I expect the following jsonl: + */ + public function iExpectTheFollowingJsonL(PyStringNode $string): void + { + if (!$this->crImportExportTrait_filesystem->has('events.jsonl')) { + Assert::fail('No events were exported'); + } + + $jsonL = $this->crImportExportTrait_filesystem->read('events.jsonl'); + + $exportedEvents = ExportedEvents::fromJsonl($jsonL); + $eventsWithoutRandomIds = []; + + foreach ($exportedEvents as $exportedEvent) { + // we have to remove the event id in \Neos\ContentRepository\Core\Feature\Common\NodeAggregateEventPublisher::enrichWithCommand + // and the initiatingTimestamp to make the events diff able + $eventsWithoutRandomIds[] = $exportedEvent + ->withIdentifier('random-event-uuid') + ->processMetadata(function (array $metadata) { + $metadata['initiatingTimestamp'] = 'random-time'; + return $metadata; + }); + } + + Assert::assertSame($string->getRaw(), ExportedEvents::fromIterable($eventsWithoutRandomIds)->toJsonl()); + } + + /** + * @Then I expect the following events to be exported + */ + public function iExpectTheFollowingEventsToBeExported(TableNode $table): void + { + + if (!$this->crImportExportTrait_filesystem->has('events.jsonl')) { + Assert::fail('No events were exported'); + } + $eventsJson = $this->crImportExportTrait_filesystem->read('events.jsonl'); + $exportedEvents = iterator_to_array(ExportedEvents::fromJsonl($eventsJson)); + + $expectedEvents = $table->getHash(); + foreach ($exportedEvents as $exportedEvent) { + $expectedEventRow = array_shift($expectedEvents); + if ($expectedEventRow === null) { + Assert::assertCount(count($table->getHash()), $exportedEvents, 'Expected number of events does not match actual number'); + } + if (!empty($expectedEventRow['Type'])) { + Assert::assertSame($expectedEventRow['Type'], $exportedEvent->type, 'Event: ' . $exportedEvent->toJson()); + } + try { + $expectedEventPayload = json_decode($expectedEventRow['Payload'], true, 512, JSON_THROW_ON_ERROR); + } catch (\JsonException $e) { + throw new \RuntimeException(sprintf('Failed to decode expected JSON: %s', $expectedEventRow['Payload']), 1655811083); + } + $actualEventPayload = $exportedEvent->payload; + foreach (array_keys($actualEventPayload) as $key) { + if (!array_key_exists($key, $expectedEventPayload)) { + unset($actualEventPayload[$key]); + } + } + Assert::assertEquals($expectedEventPayload, $actualEventPayload, 'Actual event: ' . $exportedEvent->toJson()); + } + Assert::assertCount(count($table->getHash()), $exportedEvents, 'Expected number of events does not match actual number'); + } + + /** + * @Then I expect the following errors to be logged + */ + public function iExpectTheFollowingErrorsToBeLogged(TableNode $table): void + { + Assert::assertSame($table->getColumn(0), $this->crImportExportTrait_loggedErrors, 'Expected logged errors do not match'); + $this->crImportExportTrait_loggedErrors = []; + } + + /** + * @Then I expect the following warnings to be logged + */ + public function iExpectTheFollowingWarningsToBeLogged(TableNode $table): void + { + Assert::assertSame($table->getColumn(0), $this->crImportExportTrait_loggedWarnings, 'Expected logged warnings do not match'); + $this->crImportExportTrait_loggedWarnings = []; + } + + /** + * @Then I expect a migration exception + * @Then I expect a migration exception with the message + */ + public function iExpectAMigrationExceptionWithTheMessage(PyStringNode $expectedMessage = null): void + { + Assert::assertNotNull($this->crImportExportTrait_lastMigrationException, 'Expected the previous migration to lead to an exception, but no exception was thrown'); + if ($expectedMessage !== null) { + Assert::assertSame($expectedMessage->getRaw(), $this->crImportExportTrait_lastMigrationException->getMessage()); + } + $this->crImportExportTrait_lastMigrationException = null; + } + + /** + * @Given the following ImageVariants exist + */ + public function theFollowingImageVariantsExist(TableNode $imageVariants): void + { + foreach ($imageVariants->getHash() as $variantData) { + try { + $variantData['imageAdjustments'] = json_decode($variantData['imageAdjustments'], true, 512, JSON_THROW_ON_ERROR); + } catch (\JsonException $e) { + throw new \RuntimeException(sprintf('Failed to JSON decode imageAdjustments for variant "%s"', $variantData['identifier']), 1659530081, $e); + } + $variantData['width'] = (int)$variantData['width']; + $variantData['height'] = (int)$variantData['height']; + $mockImageVariant = SerializedImageVariant::fromArray($variantData); + $this->mockAssets[$mockImageVariant->identifier] = $mockImageVariant; + } + } + + /** + * @Then /^I expect the following (Assets|ImageVariants) to be exported:$/ + */ + public function iExpectTheFollowingAssetsOrImageVariantsToBeExported(string $type, PyStringNode $expectedAssets): void + { + $actualAssets = []; + if (!$this->crImportExportTrait_filesystem->directoryExists($type)) { + Assert::fail(sprintf('No %1$s have been exported (Directory "/%1$s" does not exist)', $type)); + } + /** @var FileAttributes $file */ + foreach ($this->crImportExportTrait_filesystem->listContents($type) as $file) { + $actualAssets[] = json_decode($this->crImportExportTrait_filesystem->read($file->path()), true, 512, JSON_THROW_ON_ERROR); + } + Assert::assertJsonStringEqualsJsonString($expectedAssets->getRaw(), json_encode($actualAssets, JSON_THROW_ON_ERROR)); + } + + + /** + * @Then /^I expect no (Assets|ImageVariants) to be exported$/ + */ + public function iExpectNoAssetsToBeExported(string $type): void + { + Assert::assertFalse($this->crImportExportTrait_filesystem->directoryExists($type)); + } + + /** + * @Then I expect the following PersistentResources to be exported: + */ + public function iExpectTheFollowingPersistentResourcesToBeExported(TableNode $expectedResources): void + { + $actualResources = []; + if (!$this->crImportExportTrait_filesystem->directoryExists('Resources')) { + Assert::fail('No PersistentResources have been exported (Directory "/Resources" does not exist)'); + } + /** @var FileAttributes $file */ + foreach ($this->crImportExportTrait_filesystem->listContents('Resources') as $file) { + $actualResources[] = ['Filename' => basename($file->path()), 'Contents' => $this->crImportExportTrait_filesystem->read($file->path())]; + } + Assert::assertSame($expectedResources->getHash(), $actualResources); + } + + /** + * @Then /^I expect no PersistentResources to be exported$/ + */ + public function iExpectNoPersistentResourcesToBeExported(): void + { + Assert::assertFalse($this->crImportExportTrait_filesystem->directoryExists('Resources')); + } +} diff --git a/Neos.ContentRepository.Export/Tests/Behavior/Features/Bootstrap/FeatureContext.php b/Neos.ContentRepository.Export/Tests/Behavior/Features/Bootstrap/FeatureContext.php new file mode 100644 index 00000000000..01310c7beff --- /dev/null +++ b/Neos.ContentRepository.Export/Tests/Behavior/Features/Bootstrap/FeatureContext.php @@ -0,0 +1,78 @@ +contentRepositoryRegistry = $this->getObject(ContentRepositoryRegistry::class); + + $this->setupCrImportExportTrait(); + } + + /** + * @BeforeScenario + */ + public function resetContentRepositoryComponents(BeforeScenarioScope $scope): void + { + GherkinTableNodeBasedContentDimensionSourceFactory::reset(); + GherkinPyStringNodeBasedNodeTypeManagerFactory::reset(); + } + + protected function getContentRepositoryService( + ContentRepositoryServiceFactoryInterface $factory + ): ContentRepositoryServiceInterface { + return $this->contentRepositoryRegistry->buildService( + $this->currentContentRepository->id, + $factory + ); + } + + protected function createContentRepository( + ContentRepositoryId $contentRepositoryId + ): ContentRepository { + $this->contentRepositoryRegistry->resetFactoryInstance($contentRepositoryId); + $contentRepository = $this->contentRepositoryRegistry->get($contentRepositoryId); + GherkinTableNodeBasedContentDimensionSourceFactory::reset(); + GherkinPyStringNodeBasedNodeTypeManagerFactory::reset(); + + return $contentRepository; + } +} diff --git a/Neos.ContentRepository.Export/Tests/Behavior/Features/EventExportProcessor.feature b/Neos.ContentRepository.Export/Tests/Behavior/Features/EventExportProcessor.feature new file mode 100644 index 00000000000..9c3f28cad09 --- /dev/null +++ b/Neos.ContentRepository.Export/Tests/Behavior/Features/EventExportProcessor.feature @@ -0,0 +1,43 @@ +@contentrepository +Feature: As a user of the CR I want to export the event stream using the EventExportProcessor + + Background: + Given using the following content dimensions: + | Identifier | Values | Generalizations | + | language | de, gsw, fr | gsw->de | + And using the following node types: + """yaml + 'Neos.ContentRepository.Testing:Document': [] + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + And the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | newContentStreamId | "cs-identifier" | + And I am in workspace "live" + And the command CreateRootNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "lady-eleonode-rootford" | + | nodeTypeName | "Neos.ContentRepository:Root" | + And the event NodeAggregateWithNodeWasCreated was published with payload: + | Key | Value | + | workspaceName | "live" | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | nodeTypeName | "Neos.ContentRepository.Testing:Document" | + | originDimensionSpacePoint | {"language":"de"} | + | coveredDimensionSpacePoints | [{"language":"de"},{"language":"gsw"},{"language":"fr"}] | + | parentNodeAggregateId | "lady-eleonode-rootford" | + | nodeName | "child-document" | + | nodeAggregateClassification | "regular" | + + Scenario: Export the event stream + Then I expect exactly 3 events to be published on stream with prefix "ContentStream:cs-identifier" + When the events are exported + Then I expect the following jsonl: + """ + {"identifier":"random-event-uuid","type":"RootNodeAggregateWithNodeWasCreated","payload":{"workspaceName":"live","contentStreamId":"cs-identifier","nodeAggregateId":"lady-eleonode-rootford","nodeTypeName":"Neos.ContentRepository:Root","coveredDimensionSpacePoints":[{"language":"de"},{"language":"gsw"},{"language":"fr"}],"nodeAggregateClassification":"root"},"metadata":{"commandClass":"Neos\\ContentRepository\\Core\\Feature\\RootNodeCreation\\Command\\CreateRootNodeAggregateWithNode","commandPayload":{"workspaceName":"live","nodeAggregateId":"lady-eleonode-rootford","nodeTypeName":"Neos.ContentRepository:Root","tetheredDescendantNodeAggregateIds":[]},"initiatingUserId":"system","initiatingTimestamp":"random-time"}} + {"identifier":"random-event-uuid","type":"NodeAggregateWithNodeWasCreated","payload":{"workspaceName":"live","contentStreamId":"cs-identifier","nodeAggregateId":"nody-mc-nodeface","nodeTypeName":"Neos.ContentRepository.Testing:Document","originDimensionSpacePoint":{"language":"de"},"succeedingSiblingsForCoverage":[{"dimensionSpacePoint":{"language":"de"},"nodeAggregateId":null},{"dimensionSpacePoint":{"language":"gsw"},"nodeAggregateId":null},{"dimensionSpacePoint":{"language":"fr"},"nodeAggregateId":null}],"parentNodeAggregateId":"lady-eleonode-rootford","nodeName":"child-document","initialPropertyValues":[],"nodeAggregateClassification":"regular"},"metadata":{"initiatingTimestamp":"random-time"}} + + """ diff --git a/Neos.ContentRepository.Export/Tests/Behavior/Features/EventStoreImportProcessor.feature b/Neos.ContentRepository.Export/Tests/Behavior/Features/EventStoreImportProcessor.feature new file mode 100644 index 00000000000..443ae5ff474 --- /dev/null +++ b/Neos.ContentRepository.Export/Tests/Behavior/Features/EventStoreImportProcessor.feature @@ -0,0 +1,84 @@ +@contentrepository +Feature: As a user of the CR I want to import events using the EventStoreImportProcessor + + Background: + Given using no content dimensions + And using the following node types: + """yaml + Vendor.Site:HomePage': + superTypes: + Neos.Neos:Site: true + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + And the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | newContentStreamId | "cs-identifier" | + And I am in workspace "live" + + Scenario: Import the event stream into a specific content stream + Given using the following events.jsonl: + """ + {"identifier":"9f64c281-e5b0-48d9-900b-288a8faf92a9","type":"RootNodeAggregateWithNodeWasCreated","payload":{"workspaceName":"workspace-name","contentStreamId":"cs-imported-identifier","nodeAggregateId":"acme-site-sites","nodeTypeName":"Neos.Neos:Sites","coveredDimensionSpacePoints":[[]],"nodeAggregateClassification":"root"},"metadata":[]} + {"identifier":"1640ebbf-7ffe-4526-b0f4-7575cefabfab","type":"NodeAggregateWithNodeWasCreated","payload":{"workspaceName":"workspace-name","contentStreamId":"cs-imported-identifier","nodeAggregateId":"acme-site","nodeTypeName":"Vendor.Site:HomePage","originDimensionSpacePoint":[],"succeedingSiblingsForCoverage":[{"dimensionSpacePoint":[],"nodeAggregateId":null}],"parentNodeAggregateId":"acme-site-sites","nodeName":"acme-site","initialPropertyValues":{"title":{"value":"My Site","type":"string"},"uriPathSegment":{"value":"my-site","type":"string"}},"nodeAggregateClassification":"regular"},"metadata":[]} + """ + And I import the events.jsonl into workspace "live" + Then I expect exactly 3 events to be published on stream with prefix "ContentStream:cs-identifier" + And event at index 0 is of type "ContentStreamWasCreated" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + And event at index 1 is of type "RootNodeAggregateWithNodeWasCreated" with payload: + | Key | Expected | + | workspaceName | "live" | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "acme-site-sites" | + | nodeTypeName | "Neos.Neos:Sites" | + And event at index 2 is of type "NodeAggregateWithNodeWasCreated" with payload: + | Key | Expected | + | workspaceName | "live" | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "acme-site" | + | nodeTypeName | "Vendor.Site:HomePage" | + + Scenario: Import the event stream + Given using the following events.jsonl: + """ + {"identifier":"9f64c281-e5b0-48d9-900b-288a8faf92a9","type":"RootNodeAggregateWithNodeWasCreated","payload":{"workspaceName":"workspace-name","contentStreamId":"cs-imported-identifier","nodeAggregateId":"acme-site-sites","nodeTypeName":"Neos.Neos:Sites","coveredDimensionSpacePoints":[[]],"nodeAggregateClassification":"root"},"metadata":[]} + {"identifier":"1640ebbf-7ffe-4526-b0f4-7575cefabfab","type":"NodeAggregateWithNodeWasCreated","payload":{"workspaceName":"workspace-name","contentStreamId":"cs-imported-identifier","nodeAggregateId":"acme-site","nodeTypeName":"Vendor.Site:HomePage","originDimensionSpacePoint":[],"succeedingSiblingsForCoverage":[{"dimensionSpacePoint":[],"nodeAggregateId":null}],"parentNodeAggregateId":"acme-site-sites","nodeName":"acme-site","initialPropertyValues":{"title":{"value":"My Site","type":"string"},"uriPathSegment":{"value":"my-site","type":"string"}},"nodeAggregateClassification":"regular"},"metadata":[]} + """ + And I import the events.jsonl + Then I expect exactly 3 events to be published on stream with prefix "ContentStream:cs-identifier" + And event at index 0 is of type "ContentStreamWasCreated" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + And event at index 1 is of type "RootNodeAggregateWithNodeWasCreated" with payload: + | Key | Expected | + | workspaceName | "live" | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "acme-site-sites" | + | nodeTypeName | "Neos.Neos:Sites" | + And event at index 2 is of type "NodeAggregateWithNodeWasCreated" with payload: + | Key | Expected | + | workspaceName | "live" | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "acme-site" | + | nodeTypeName | "Vendor.Site:HomePage" | + + Scenario: Import faulty event stream with explicit "ContentStreamWasCreated" does not duplicate content-stream + see issue https://github.com/neos/neos-development-collection/issues/4298 + + Given using the following events.jsonl: + """ + {"identifier":"5f2da12d-7037-4524-acb0-d52037342c77","type":"ContentStreamWasCreated","payload":{"workspaceName":"workspace-name","contentStreamId":"cs-imported-identifier"},"metadata":[]} + {"identifier":"9f64c281-e5b0-48d9-900b-288a8faf92a9","type":"RootNodeAggregateWithNodeWasCreated","payload":{"workspaceName":"workspace-name","contentStreamId":"cs-imported-identifier","nodeAggregateId":"acme-site-sites","nodeTypeName":"Neos.Neos:Sites","coveredDimensionSpacePoints":[[]],"nodeAggregateClassification":"root"},"metadata":[]} + {"identifier":"1640ebbf-7ffe-4526-b0f4-7575cefabfab","type":"NodeAggregateWithNodeWasCreated","payload":{"workspaceName":"workspace-name","contentStreamId":"cs-imported-identifier","nodeAggregateId":"acme-site","nodeTypeName":"Vendor.Site:HomePage","originDimensionSpacePoint":[],"coveredDimensionSpacePoints":[[]],"parentNodeAggregateId":"acme-site-sites","nodeName":"acme-site","initialPropertyValues":{"title":{"value":"My Site","type":"string"},"uriPathSegment":{"value":"my-site","type":"string"}},"nodeAggregateClassification":"regular","succeedingNodeAggregateId":null},"metadata":[]} + """ + And I import the events.jsonl + + And I expect a migration exception with the message + """ + Failed to read events. ContentStreamWasCreated is not expected in imported event stream. + """ + + Then I expect exactly 0 events to be published on stream with prefix "ContentStream:cs-imported-identifier" diff --git a/Neos.ContentRepository.Export/Tests/Behavior/behat.yml.dist b/Neos.ContentRepository.Export/Tests/Behavior/behat.yml.dist new file mode 100644 index 00000000000..e69de29bb2d diff --git a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Bootstrap/FeatureContext.php b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Bootstrap/FeatureContext.php index 0e10464c0fc..7ec432e2230 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Bootstrap/FeatureContext.php +++ b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Bootstrap/FeatureContext.php @@ -1,6 +1,9 @@ */ private array $mockAssets = []; - private InMemoryFilesystemAdapter $mockFilesystemAdapter; - private Filesystem $mockFilesystem; - - private \Throwable|null $lastMigrationException = null; - - /** - * @var array - */ - private array $loggedErrors = []; - - /** - * @var array - */ - private array $loggedWarnings = []; - private ContentRepository $contentRepository; protected ContentRepositoryRegistry $contentRepositoryRegistry; @@ -78,21 +68,7 @@ public function __construct() self::bootstrapFlow(); $this->contentRepositoryRegistry = $this->getObject(ContentRepositoryRegistry::class); - $this->mockFilesystemAdapter = new InMemoryFilesystemAdapter(); - $this->mockFilesystem = new Filesystem($this->mockFilesystemAdapter); - } - - /** - * @AfterScenario - */ - public function failIfLastMigrationHasErrors(): void - { - if ($this->lastMigrationException !== null) { - throw new \RuntimeException(sprintf('The last migration run led to an exception: %s', $this->lastMigrationException->getMessage())); - } - if ($this->loggedErrors !== []) { - throw new \RuntimeException(sprintf('The last migration run logged %d error%s', count($this->loggedErrors), count($this->loggedErrors) === 1 ? '' : 's')); - } + $this->setupCrImportExportTrait(); } /** @@ -149,86 +125,7 @@ public function build(ContentRepositoryServiceFactoryDependencies $serviceFactor if ($contentStream !== null) { $migration->setContentStreamId(ContentStreamId::fromString($contentStream)); } - $processingContext = new ProcessingContext($this->mockFilesystem, function (Severity $severity, string $message) { - if ($severity === Severity::ERROR) { - $this->loggedErrors[] = $message; - } elseif ($severity === Severity::WARNING) { - $this->loggedWarnings[] = $message; - } - }); - try { - $migration->run($processingContext); - } catch (\Throwable $e) { - $this->lastMigrationException = $e; - } - } - - /** - * @Then I expect the following events to be exported - */ - public function iExpectTheFollowingEventsToBeExported(TableNode $table): void - { - - if (!$this->mockFilesystem->has('events.jsonl')) { - Assert::fail('No events were exported'); - } - $eventsJson = $this->mockFilesystem->read('events.jsonl'); - $exportedEvents = iterator_to_array(ExportedEvents::fromJsonl($eventsJson)); - - $expectedEvents = $table->getHash(); - foreach ($exportedEvents as $exportedEvent) { - $expectedEventRow = array_shift($expectedEvents); - if ($expectedEventRow === null) { - Assert::assertCount(count($table->getHash()), $exportedEvents, 'Expected number of events does not match actual number'); - } - if (!empty($expectedEventRow['Type'])) { - Assert::assertSame($expectedEventRow['Type'], $exportedEvent->type, 'Event: ' . $exportedEvent->toJson()); - } - try { - $expectedEventPayload = json_decode($expectedEventRow['Payload'], true, 512, JSON_THROW_ON_ERROR); - } catch (JsonException $e) { - throw new \RuntimeException(sprintf('Failed to decode expected JSON: %s', $expectedEventRow['Payload']), 1655811083); - } - $actualEventPayload = $exportedEvent->payload; - foreach (array_keys($actualEventPayload) as $key) { - if (!array_key_exists($key, $expectedEventPayload)) { - unset($actualEventPayload[$key]); - } - } - Assert::assertEquals($expectedEventPayload, $actualEventPayload, 'Actual event: ' . $exportedEvent->toJson()); - } - Assert::assertCount(count($table->getHash()), $exportedEvents, 'Expected number of events does not match actual number'); - } - - /** - * @Then I expect the following errors to be logged - */ - public function iExpectTheFollowingErrorsToBeLogged(TableNode $table): void - { - Assert::assertSame($table->getColumn(0), $this->loggedErrors, 'Expected logged errors do not match'); - $this->loggedErrors = []; - } - - /** - * @Then I expect the following warnings to be logged - */ - public function iExpectTheFollowingWarningsToBeLogged(TableNode $table): void - { - Assert::assertSame($table->getColumn(0), $this->loggedWarnings, 'Expected logged warnings do not match'); - $this->loggedWarnings = []; - } - - /** - * @Then I expect a MigrationError - * @Then I expect a MigrationError with the message - */ - public function iExpectAMigrationErrorWithTheMessage(PyStringNode $expectedMessage = null): void - { - Assert::assertNotNull($this->lastMigrationException, 'Expected the previous migration to lead to an exception, but no exception was thrown'); - if ($expectedMessage !== null) { - Assert::assertSame($expectedMessage->getRaw(), $this->lastMigrationException->getMessage()); - } - $this->lastMigrationException = null; + $this->runCrImportExportProcessors($migration); } /** @@ -269,24 +166,6 @@ public function theFollowingAssetsExist(TableNode $images): void } } - /** - * @Given the following ImageVariants exist - */ - public function theFollowingImageVariantsExist(TableNode $imageVariants): void - { - foreach ($imageVariants->getHash() as $variantData) { - try { - $variantData['imageAdjustments'] = json_decode($variantData['imageAdjustments'], true, 512, JSON_THROW_ON_ERROR); - } catch (JsonException $e) { - throw new \RuntimeException(sprintf('Failed to JSON decode imageAdjustments for variant "%s"', $variantData['identifier']), 1659530081, $e); - } - $variantData['width'] = (int)$variantData['width']; - $variantData['height'] = (int)$variantData['height']; - $mockImageVariant = SerializedImageVariant::fromArray($variantData); - $this->mockAssets[$mockImageVariant->identifier] = $mockImageVariant; - } - } - /** * @When I run the asset migration */ @@ -298,7 +177,9 @@ public function iRunTheAssetMigration(): void /** * @param array $mockResources */ - public function __construct(private array $mockResources) {} + public function __construct(private array $mockResources) + { + } public function getStreamBySha1(string $sha1) { @@ -315,7 +196,9 @@ public function getStreamBySha1(string $sha1) /** * @param array $mockAssets */ - public function __construct(private array $mockAssets) {} + public function __construct(private array $mockAssets) + { + } public function findAssetById(string $assetId): SerializedAsset|SerializedImageVariant { @@ -326,88 +209,13 @@ public function findAssetById(string $assetId): SerializedAsset|SerializedImageV } }; - $this->mockFilesystemAdapter->deleteEverything(); - $assetExporter = new AssetExporter($this->mockFilesystem, $mockAssetLoader, $mockResourceLoader); + $assetExporter = new AssetExporter($this->crImportExportTrait_filesystem, $mockAssetLoader, $mockResourceLoader); $migration = new NodeDataToAssetsProcessor($nodeTypeManager, $assetExporter, $this->nodeDataRows); - $processingContext = new ProcessingContext($this->mockFilesystem, function (Severity $severity, string $message) { - if ($severity === Severity::ERROR) { - $this->loggedErrors[] = $message; - } elseif ($severity === Severity::WARNING) { - $this->loggedWarnings[] = $message; - } - }); - try { - $migration->run($processingContext); - } catch (\Throwable $e) { - $this->lastMigrationException = $e; - } + $this->runCrImportExportProcessors($migration); } - /** - * @Then /^I expect the following (Assets|ImageVariants) to be exported:$/ - */ - public function iExpectTheFollowingToBeExported(string $type, PyStringNode $expectedAssets): void - { - $actualAssets = []; - if (!$this->mockFilesystem->directoryExists($type)) { - Assert::fail(sprintf('No %1$s have been exported (Directory "/%1$s" does not exist)', $type)); - } - /** @var FileAttributes $file */ - foreach ($this->mockFilesystem->listContents($type) as $file) { - $actualAssets[] = json_decode($this->mockFilesystem->read($file->path()), true, 512, JSON_THROW_ON_ERROR); - } - Assert::assertJsonStringEqualsJsonString($expectedAssets->getRaw(), json_encode($actualAssets, JSON_THROW_ON_ERROR)); - } - - /** - * @Then /^I expect no (Assets|ImageVariants) to be exported$/ - */ - public function iExpectNoAssetsToBeExported(string $type): void - { - Assert::assertFalse($this->mockFilesystem->directoryExists($type)); - } - - /** - * @Then I expect the following PersistentResources to be exported: - */ - public function iExpectTheFollowingPersistentResourcesToBeExported(TableNode $expectedResources): void - { - $actualResources = []; - if (!$this->mockFilesystem->directoryExists('Resources')) { - Assert::fail('No PersistentResources have been exported (Directory "/Resources" does not exist)'); - } - /** @var FileAttributes $file */ - foreach ($this->mockFilesystem->listContents('Resources') as $file) { - $actualResources[] = ['Filename' => basename($file->path()), 'Contents' => $this->mockFilesystem->read($file->path())]; - } - Assert::assertSame($expectedResources->getHash(), $actualResources); - } - - /** - * @Then /^I expect no PersistentResources to be exported$/ - */ - public function iExpectNoPersistentResourcesToBeExported(): void - { - Assert::assertFalse($this->mockFilesystem->directoryExists('Resources')); - } - - /** ---------------------------------- */ - /** - * @param TableNode $table - * @return array - * @throws JsonException - */ - private function parseJsonTable(TableNode $table): array - { - return array_map(static function (array $row) { - return array_map(static function (string $jsonValue) { - return json_decode($jsonValue, true, 512, JSON_THROW_ON_ERROR); - }, $row); - }, $table->getHash()); - } - protected function getContentRepositoryService( ContentRepositoryServiceFactoryInterface $factory ): ContentRepositoryServiceInterface { diff --git a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/Errors.feature b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/Errors.feature index 49b2b021c08..c41e915c647 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/Errors.feature +++ b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/Errors.feature @@ -34,7 +34,7 @@ Feature: Exceptional cases during migrations | site-node-id | /sites/test-site | Some.Package:Homepage | {"language": ["de"]} | | site-node-id | /sites/test-site | Some.Package:SomeOtherHomepage | {"language": ["en"]} | And I run the event migration - Then I expect a MigrationError with the message + Then I expect a migration exception with the message """ Node aggregate with id "site-node-id" has a type of "Some.Package:SomeOtherHomepage" in content dimension [{"language":"en"}]. I was visited previously for content dimension [{"language":"de"}] with the type "Some.Package:Homepage". Node variants must not have different types """ @@ -94,7 +94,7 @@ Feature: Exceptional cases during migrations | sites | /sites | | | a | /sites/a | not json | And I run the event migration - Then I expect a MigrationError + Then I expect a migration exception Scenario: Invalid node properties (no JSON) When I have the following node data rows: @@ -102,7 +102,7 @@ Feature: Exceptional cases during migrations | sites | /sites | | | | a | /sites/a | not json | Some.Package:Homepage | And I run the event migration - Then I expect a MigrationError with the message + Then I expect a migration exception with the message """ Failed to decode properties "not json" of node "a" (type: "Some.Package:Homepage"): Could not convert database value "not json" to Doctrine Type flow_json_array """ @@ -118,7 +118,7 @@ Feature: Exceptional cases during migrations | site-node-id | /sites/test-site | Some.Package:Homepage | {"language": ["ch"]} | | site-node-id | /sites/test-site | Some.Package:Homepage | {"language": ["ch"]} | And I run the event migration - Then I expect a MigrationError with the message + Then I expect a migration exception with the message """ Node "site-node-id" with dimension space point "{"language":"ch"}" was already visited before """ @@ -133,7 +133,7 @@ Feature: Exceptional cases during migrations | site-node-id | /sites/test-site | Some.Package:Homepage | {"language": ["de"]} | | site-node-id | /sites/test-site | Some.Package:Homepage | {"language": ["de"]} | And I run the event migration - Then I expect a MigrationError with the message + Then I expect a migration exception with the message """ Node "site-node-id" for dimension {"language":"de"} was already created previously """ @@ -144,7 +144,7 @@ Feature: Exceptional cases during migrations | sites-node-id | /sites | unstructured | | site-node-id | /sites/test-site | unstructured | And I run the event migration - Then I expect a MigrationError with the message + Then I expect a migration exception with the message """ The site node "site-node-id" (type: "unstructured") must be of type "Neos.Neos:Site" """ From 191ce88a0733b988cff2fd6e6bbcb941ed7945b0 Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Tue, 22 Oct 2024 17:47:40 +0200 Subject: [PATCH 017/214] Rename `Factories` folder to `Factory --- .../Tests/Behavior/Features/Bootstrap/CrImportExportTrait.php | 4 ++-- .../ContentRepositorySetupProcessorFactory.php | 2 +- .../{Factories => Factory}/EventExportProcessorFactory.php | 2 +- .../EventStoreImportProcessorFactory.php | 2 +- Neos.Neos/Classes/Domain/Service/SiteImportService.php | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) rename Neos.ContentRepository.Export/src/{Factories => Factory}/ContentRepositorySetupProcessorFactory.php (93%) rename Neos.ContentRepository.Export/src/{Factories => Factory}/EventExportProcessorFactory.php (94%) rename Neos.ContentRepository.Export/src/{Factories => Factory}/EventStoreImportProcessorFactory.php (95%) diff --git a/Neos.ContentRepository.Export/Tests/Behavior/Features/Bootstrap/CrImportExportTrait.php b/Neos.ContentRepository.Export/Tests/Behavior/Features/Bootstrap/CrImportExportTrait.php index 8f06d03cf64..8fed952b2bd 100644 --- a/Neos.ContentRepository.Export/Tests/Behavior/Features/Bootstrap/CrImportExportTrait.php +++ b/Neos.ContentRepository.Export/Tests/Behavior/Features/Bootstrap/CrImportExportTrait.php @@ -22,8 +22,8 @@ use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepository\Export\Asset\ValueObject\SerializedImageVariant; use Neos\ContentRepository\Export\Event\ValueObject\ExportedEvents; -use Neos\ContentRepository\Export\Factories\EventExportProcessorFactory; -use Neos\ContentRepository\Export\Factories\EventStoreImportProcessorFactory; +use Neos\ContentRepository\Export\Factory\EventExportProcessorFactory; +use Neos\ContentRepository\Export\Factory\EventStoreImportProcessorFactory; use Neos\ContentRepository\Export\ProcessingContext; use Neos\ContentRepository\Export\ProcessorInterface; use Neos\ContentRepository\Export\Processors\EventExportProcessor; diff --git a/Neos.ContentRepository.Export/src/Factories/ContentRepositorySetupProcessorFactory.php b/Neos.ContentRepository.Export/src/Factory/ContentRepositorySetupProcessorFactory.php similarity index 93% rename from Neos.ContentRepository.Export/src/Factories/ContentRepositorySetupProcessorFactory.php rename to Neos.ContentRepository.Export/src/Factory/ContentRepositorySetupProcessorFactory.php index 076515afaaf..945ef993da7 100644 --- a/Neos.ContentRepository.Export/src/Factories/ContentRepositorySetupProcessorFactory.php +++ b/Neos.ContentRepository.Export/src/Factory/ContentRepositorySetupProcessorFactory.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Neos\ContentRepository\Export\Factories; +namespace Neos\ContentRepository\Export\Factory; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceFactoryDependencies; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceFactoryInterface; diff --git a/Neos.ContentRepository.Export/src/Factories/EventExportProcessorFactory.php b/Neos.ContentRepository.Export/src/Factory/EventExportProcessorFactory.php similarity index 94% rename from Neos.ContentRepository.Export/src/Factories/EventExportProcessorFactory.php rename to Neos.ContentRepository.Export/src/Factory/EventExportProcessorFactory.php index 6fe742cec21..636a3a5a1be 100644 --- a/Neos.ContentRepository.Export/src/Factories/EventExportProcessorFactory.php +++ b/Neos.ContentRepository.Export/src/Factory/EventExportProcessorFactory.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Neos\ContentRepository\Export\Factories; +namespace Neos\ContentRepository\Export\Factory; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceFactoryDependencies; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceFactoryInterface; diff --git a/Neos.ContentRepository.Export/src/Factories/EventStoreImportProcessorFactory.php b/Neos.ContentRepository.Export/src/Factory/EventStoreImportProcessorFactory.php similarity index 95% rename from Neos.ContentRepository.Export/src/Factories/EventStoreImportProcessorFactory.php rename to Neos.ContentRepository.Export/src/Factory/EventStoreImportProcessorFactory.php index 94b6aa2f8d4..459a746c1e6 100644 --- a/Neos.ContentRepository.Export/src/Factories/EventStoreImportProcessorFactory.php +++ b/Neos.ContentRepository.Export/src/Factory/EventStoreImportProcessorFactory.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Neos\ContentRepository\Export\Factories; +namespace Neos\ContentRepository\Export\Factory; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceFactoryDependencies; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceFactoryInterface; diff --git a/Neos.Neos/Classes/Domain/Service/SiteImportService.php b/Neos.Neos/Classes/Domain/Service/SiteImportService.php index d04a48ca9dd..07107821f2c 100644 --- a/Neos.Neos/Classes/Domain/Service/SiteImportService.php +++ b/Neos.Neos/Classes/Domain/Service/SiteImportService.php @@ -18,8 +18,8 @@ use League\Flysystem\Local\LocalFilesystemAdapter; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; -use Neos\ContentRepository\Export\Factories\ContentRepositorySetupProcessorFactory; -use Neos\ContentRepository\Export\Factories\EventStoreImportProcessorFactory; +use Neos\ContentRepository\Export\Factory\ContentRepositorySetupProcessorFactory; +use Neos\ContentRepository\Export\Factory\EventStoreImportProcessorFactory; use Neos\ContentRepository\Export\ProcessingContext; use Neos\ContentRepository\Export\ProcessorInterface; use Neos\ContentRepository\Export\Processors\AssetRepositoryImportProcessor; From aa8c6114080564396199ba2b1aa139972b9d8f2b Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Tue, 22 Oct 2024 18:14:27 +0200 Subject: [PATCH 018/214] Remove unused namespace imports --- .../Tests/Behavior/Bootstrap/FeatureContext.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Bootstrap/FeatureContext.php b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Bootstrap/FeatureContext.php index 7ec432e2230..7a506466d0c 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Bootstrap/FeatureContext.php +++ b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Bootstrap/FeatureContext.php @@ -5,11 +5,7 @@ require_once(__DIR__ . '/../../../../Neos.ContentRepository.Export/Tests/Behavior/Features/Bootstrap/CrImportExportTrait.php'); use Behat\Behat\Context\Context; -use Behat\Gherkin\Node\PyStringNode; use Behat\Gherkin\Node\TableNode; -use League\Flysystem\FileAttributes; -use League\Flysystem\Filesystem; -use League\Flysystem\InMemory\InMemoryFilesystemAdapter; use Neos\Behat\FlowBootstrapTrait; use Neos\ContentGraph\DoctrineDbalAdapter\Tests\Behavior\Features\Bootstrap\CrImportExportTrait; use Neos\ContentRepository\BehavioralTests\TestSuite\Behavior\CRBehavioralTestsSubjectProvider; @@ -30,16 +26,12 @@ use Neos\ContentRepository\Export\Asset\ValueObject\SerializedAsset; use Neos\ContentRepository\Export\Asset\ValueObject\SerializedImageVariant; use Neos\ContentRepository\Export\Asset\ValueObject\SerializedResource; -use Neos\ContentRepository\Export\Event\ValueObject\ExportedEvents; -use Neos\ContentRepository\Export\ProcessingContext; -use Neos\ContentRepository\Export\Severity; use Neos\ContentRepository\LegacyNodeMigration\NodeDataToAssetsProcessor; use Neos\ContentRepository\LegacyNodeMigration\NodeDataToEventsProcessor; use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\CRTestSuiteTrait; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Property\PropertyMapper; use Neos\Flow\ResourceManagement\PersistentResource; -use PHPUnit\Framework\Assert; use PHPUnit\Framework\MockObject\Generator as MockGenerator; /** From b79e0bae53d2c0e66841ca596025358d0e204d64 Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Tue, 22 Oct 2024 18:34:59 +0200 Subject: [PATCH 019/214] Move auth related classes to `Feature/Security` and add dedicated `AccessDenied` exception --- .../Behavior/FakeAuthProviderFactory.php | 2 +- .../Classes/ContentRepository.php | 13 ++++--- .../Factory/ContentRepositoryFactory.php | 2 +- .../Factory/ProjectionFactoryDependencies.php | 2 +- .../Security}/AuthProviderInterface.php | 4 ++- .../Security/Dto}/Privilege.php | 2 +- .../Auth => Feature/Security/Dto}/UserId.php | 2 +- .../Security/Exception/AccessDenied.php | 34 +++++++++++++++++++ .../Security}/StaticAuthProvider.php | 4 ++- .../Bootstrap/CRTestSuiteRuntimeVariables.php | 7 ++-- .../Bootstrap/Helpers/FakeAuthProvider.php | 6 ++-- .../Classes/ContentRepositoryRegistry.php | 4 +-- .../AuthProviderFactoryInterface.php | 2 +- .../StaticAuthProviderFactory.php | 6 ++-- .../Controller/Frontend/NodeController.php | 6 +++- .../ContentRepositoryAuthProvider.php | 6 ++-- 16 files changed, 73 insertions(+), 29 deletions(-) rename Neos.ContentRepository.Core/Classes/{SharedModel/Auth => Feature/Security}/AuthProviderInterface.php (78%) rename Neos.ContentRepository.Core/Classes/{SharedModel/Auth => Feature/Security/Dto}/Privilege.php (92%) rename Neos.ContentRepository.Core/Classes/{SharedModel/Auth => Feature/Security/Dto}/UserId.php (96%) create mode 100644 Neos.ContentRepository.Core/Classes/Feature/Security/Exception/AccessDenied.php rename Neos.ContentRepository.Core/Classes/{SharedModel/Auth => Feature/Security}/StaticAuthProvider.php (86%) diff --git a/Neos.ContentRepository.BehavioralTests/Classes/TestSuite/Behavior/FakeAuthProviderFactory.php b/Neos.ContentRepository.BehavioralTests/Classes/TestSuite/Behavior/FakeAuthProviderFactory.php index df59e7f80c8..f018a5ac958 100644 --- a/Neos.ContentRepository.BehavioralTests/Classes/TestSuite/Behavior/FakeAuthProviderFactory.php +++ b/Neos.ContentRepository.BehavioralTests/Classes/TestSuite/Behavior/FakeAuthProviderFactory.php @@ -4,8 +4,8 @@ namespace Neos\ContentRepository\BehavioralTests\TestSuite\Behavior; +use Neos\ContentRepository\Core\Feature\Security\AuthProviderInterface; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; -use Neos\ContentRepository\Core\SharedModel\Auth\AuthProviderInterface; use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Helpers\FakeAuthProvider; use Neos\ContentRepositoryRegistry\Factory\AuthProvider\AuthProviderFactoryInterface; diff --git a/Neos.ContentRepository.Core/Classes/ContentRepository.php b/Neos.ContentRepository.Core/Classes/ContentRepository.php index bbc21fa9c81..a73d7a42aa3 100644 --- a/Neos.ContentRepository.Core/Classes/ContentRepository.php +++ b/Neos.ContentRepository.Core/Classes/ContentRepository.php @@ -26,6 +26,9 @@ use Neos\ContentRepository\Core\EventStore\Events; use Neos\ContentRepository\Core\EventStore\EventsToPublish; use Neos\ContentRepository\Core\Factory\ContentRepositoryFactory; +use Neos\ContentRepository\Core\Feature\Security\AuthProviderInterface; +use Neos\ContentRepository\Core\Feature\Security\Dto\UserId; +use Neos\ContentRepository\Core\Feature\Security\Exception\AccessDenied; use Neos\ContentRepository\Core\NodeType\NodeTypeManager; use Neos\ContentRepository\Core\Projection\CatchUp; use Neos\ContentRepository\Core\Projection\CatchUpOptions; @@ -36,8 +39,6 @@ use Neos\ContentRepository\Core\Projection\ProjectionStateInterface; use Neos\ContentRepository\Core\Projection\ProjectionStatuses; use Neos\ContentRepository\Core\Projection\WithMarkStaleInterface; -use Neos\ContentRepository\Core\SharedModel\Auth\AuthProviderInterface; -use Neos\ContentRepository\Core\SharedModel\Auth\UserId; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryStatus; use Neos\ContentRepository\Core\SharedModel\Exception\WorkspaceDoesNotExist; @@ -97,12 +98,13 @@ public function __construct( * The only API to send commands (mutation intentions) to the system. * * @param CommandInterface $command + * @throws AccessDenied */ public function handle(CommandInterface $command): void { $privilege = $this->authProvider->getCommandPrivilege($command); if (!$privilege->granted) { - throw new \RuntimeException(sprintf('Command "%s" was denied: %s', $command::class, $privilege->reason), 1729086686); + throw AccessDenied::becauseCommandIsNotGranted($command, $privilege->reason); } // the commands only calculate which events they want to have published, but do not do the // publishing themselves @@ -253,19 +255,20 @@ public function resetProjectionState(string $projectionClassName): void /** * @throws WorkspaceDoesNotExist if the workspace does not exist + * @throws AccessDenied if no read access is granted to the workspace ({@see AuthProviderInterface}) */ public function getContentGraph(WorkspaceName $workspaceName): ContentGraphInterface { $privilege = $this->authProvider->getReadNodesFromWorkspacePrivilege($workspaceName); if (!$privilege->granted) { - // TODO more specific exception - throw new \RuntimeException(sprintf('Read access denied for workspace "%s": %s', $workspaceName->value, $privilege->reason ?? ''), 1729014760); + throw AccessDenied::becauseWorkspaceCantBeRead($workspaceName, $privilege->reason); } return $this->getContentRepositoryReadModel()->getContentGraphByWorkspaceName($workspaceName); } /** * @throws WorkspaceDoesNotExist if the workspace does not exist + * @throws AccessDenied if no read access is granted to the workspace ({@see AuthProviderInterface}) */ public function getContentSubgraph(WorkspaceName $workspaceName, DimensionSpacePoint $dimensionSpacePoint): ContentSubgraphInterface { diff --git a/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php b/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php index 333b7e0a472..a942fe9d155 100644 --- a/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php +++ b/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php @@ -21,6 +21,7 @@ use Neos\ContentRepository\Core\DimensionSpace\InterDimensionalVariationGraph; use Neos\ContentRepository\Core\EventStore\EventNormalizer; use Neos\ContentRepository\Core\EventStore\EventPersister; +use Neos\ContentRepository\Core\Feature\Security\AuthProviderInterface; use Neos\ContentRepository\Core\Feature\ContentStreamCommandHandler; use Neos\ContentRepository\Core\Feature\DimensionSpaceAdjustment\DimensionSpaceCommandHandler; use Neos\ContentRepository\Core\Feature\NodeAggregateCommandHandler; @@ -30,7 +31,6 @@ use Neos\ContentRepository\Core\NodeType\NodeTypeManager; use Neos\ContentRepository\Core\Projection\ProjectionsAndCatchUpHooks; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; -use Neos\ContentRepository\Core\SharedModel\Auth\AuthProviderInterface; use Neos\EventStore\EventStoreInterface; use Psr\Clock\ClockInterface; use Symfony\Component\Serializer\Serializer; diff --git a/Neos.ContentRepository.Core/Classes/Factory/ProjectionFactoryDependencies.php b/Neos.ContentRepository.Core/Classes/Factory/ProjectionFactoryDependencies.php index 36b614ba623..0b78e572cc4 100644 --- a/Neos.ContentRepository.Core/Classes/Factory/ProjectionFactoryDependencies.php +++ b/Neos.ContentRepository.Core/Classes/Factory/ProjectionFactoryDependencies.php @@ -18,9 +18,9 @@ use Neos\ContentRepository\Core\DimensionSpace\ContentDimensionZookeeper; use Neos\ContentRepository\Core\DimensionSpace\InterDimensionalVariationGraph; use Neos\ContentRepository\Core\EventStore\EventNormalizer; +use Neos\ContentRepository\Core\Feature\Security\AuthProviderInterface; use Neos\ContentRepository\Core\Infrastructure\Property\PropertyConverter; use Neos\ContentRepository\Core\NodeType\NodeTypeManager; -use Neos\ContentRepository\Core\SharedModel\Auth\AuthProviderInterface; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\EventStore\EventStoreInterface; diff --git a/Neos.ContentRepository.Core/Classes/SharedModel/Auth/AuthProviderInterface.php b/Neos.ContentRepository.Core/Classes/Feature/Security/AuthProviderInterface.php similarity index 78% rename from Neos.ContentRepository.Core/Classes/SharedModel/Auth/AuthProviderInterface.php rename to Neos.ContentRepository.Core/Classes/Feature/Security/AuthProviderInterface.php index 84890012d9d..2d2bca17bfc 100644 --- a/Neos.ContentRepository.Core/Classes/SharedModel/Auth/AuthProviderInterface.php +++ b/Neos.ContentRepository.Core/Classes/Feature/Security/AuthProviderInterface.php @@ -2,9 +2,11 @@ declare(strict_types=1); -namespace Neos\ContentRepository\Core\SharedModel\Auth; +namespace Neos\ContentRepository\Core\Feature\Security; use Neos\ContentRepository\Core\CommandHandler\CommandInterface; +use Neos\ContentRepository\Core\Feature\Security\Dto\Privilege; +use Neos\ContentRepository\Core\Feature\Security\Dto\UserId; use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; diff --git a/Neos.ContentRepository.Core/Classes/SharedModel/Auth/Privilege.php b/Neos.ContentRepository.Core/Classes/Feature/Security/Dto/Privilege.php similarity index 92% rename from Neos.ContentRepository.Core/Classes/SharedModel/Auth/Privilege.php rename to Neos.ContentRepository.Core/Classes/Feature/Security/Dto/Privilege.php index cfe8798273b..0fcef1552fe 100644 --- a/Neos.ContentRepository.Core/Classes/SharedModel/Auth/Privilege.php +++ b/Neos.ContentRepository.Core/Classes/Feature/Security/Dto/Privilege.php @@ -12,7 +12,7 @@ declare(strict_types=1); -namespace Neos\ContentRepository\Core\SharedModel\Auth; +namespace Neos\ContentRepository\Core\Feature\Security\Dto; /** * A privilege that is returned by the {@see AuthProviderInterface} diff --git a/Neos.ContentRepository.Core/Classes/SharedModel/Auth/UserId.php b/Neos.ContentRepository.Core/Classes/Feature/Security/Dto/UserId.php similarity index 96% rename from Neos.ContentRepository.Core/Classes/SharedModel/Auth/UserId.php rename to Neos.ContentRepository.Core/Classes/Feature/Security/Dto/UserId.php index 80228a031d0..b43e9b5feb1 100644 --- a/Neos.ContentRepository.Core/Classes/SharedModel/Auth/UserId.php +++ b/Neos.ContentRepository.Core/Classes/Feature/Security/Dto/UserId.php @@ -12,7 +12,7 @@ declare(strict_types=1); -namespace Neos\ContentRepository\Core\SharedModel\Auth; +namespace Neos\ContentRepository\Core\Feature\Security\Dto; use Neos\ContentRepository\Core\SharedModel\Id\UuidFactory; diff --git a/Neos.ContentRepository.Core/Classes/Feature/Security/Exception/AccessDenied.php b/Neos.ContentRepository.Core/Classes/Feature/Security/Exception/AccessDenied.php new file mode 100644 index 00000000000..21528122e84 --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/Feature/Security/Exception/AccessDenied.php @@ -0,0 +1,34 @@ +value, $reason), 1729014760); + } +} diff --git a/Neos.ContentRepository.Core/Classes/SharedModel/Auth/StaticAuthProvider.php b/Neos.ContentRepository.Core/Classes/Feature/Security/StaticAuthProvider.php similarity index 86% rename from Neos.ContentRepository.Core/Classes/SharedModel/Auth/StaticAuthProvider.php rename to Neos.ContentRepository.Core/Classes/Feature/Security/StaticAuthProvider.php index a44a4e61db1..5c5b72e1223 100644 --- a/Neos.ContentRepository.Core/Classes/SharedModel/Auth/StaticAuthProvider.php +++ b/Neos.ContentRepository.Core/Classes/Feature/Security/StaticAuthProvider.php @@ -2,9 +2,11 @@ declare(strict_types=1); -namespace Neos\ContentRepository\Core\SharedModel\Auth; +namespace Neos\ContentRepository\Core\Feature\Security; use Neos\ContentRepository\Core\CommandHandler\CommandInterface; +use Neos\ContentRepository\Core\Feature\Security\Dto\Privilege; +use Neos\ContentRepository\Core\Feature\Security\Dto\UserId; use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteRuntimeVariables.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteRuntimeVariables.php index 66b1e76d28d..15706af6806 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteRuntimeVariables.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteRuntimeVariables.php @@ -14,9 +14,10 @@ namespace Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap; -use Neos\ContentRepository\Core\ContentRepositoryReadModel; use Neos\ContentRepository\Core\ContentRepository; +use Neos\ContentRepository\Core\ContentRepositoryReadModel; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; +use Neos\ContentRepository\Core\Feature\Security\Dto\UserId; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentSubgraphInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\Projection\ContentGraph\NodeAggregate; @@ -24,11 +25,9 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; -use Neos\ContentRepository\Core\SharedModel\Auth\UserId; -use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; -use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Helpers\FakeClock; use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Helpers\FakeAuthProvider; +use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Helpers\FakeClock; /** * The node creation trait for behavioral tests diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Helpers/FakeAuthProvider.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Helpers/FakeAuthProvider.php index c2cb6b8a84d..ac7def3d8d0 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Helpers/FakeAuthProvider.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Helpers/FakeAuthProvider.php @@ -5,10 +5,10 @@ namespace Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Helpers; use Neos\ContentRepository\Core\CommandHandler\CommandInterface; +use Neos\ContentRepository\Core\Feature\Security\AuthProviderInterface; +use Neos\ContentRepository\Core\Feature\Security\Dto\Privilege; +use Neos\ContentRepository\Core\Feature\Security\Dto\UserId; use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; -use Neos\ContentRepository\Core\SharedModel\Auth\Privilege; -use Neos\ContentRepository\Core\SharedModel\Auth\UserId; -use Neos\ContentRepository\Core\SharedModel\Auth\AuthProviderInterface; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; final class FakeAuthProvider implements AuthProviderInterface diff --git a/Neos.ContentRepositoryRegistry/Classes/ContentRepositoryRegistry.php b/Neos.ContentRepositoryRegistry/Classes/ContentRepositoryRegistry.php index 3e901aa404e..95e5c1887ab 100644 --- a/Neos.ContentRepositoryRegistry/Classes/ContentRepositoryRegistry.php +++ b/Neos.ContentRepositoryRegistry/Classes/ContentRepositoryRegistry.php @@ -10,6 +10,7 @@ use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceFactoryInterface; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface; use Neos\ContentRepository\Core\Factory\ProjectionsAndCatchUpHooksFactory; +use Neos\ContentRepository\Core\Feature\Security\AuthProviderInterface; use Neos\ContentRepository\Core\NodeType\NodeTypeManager; use Neos\ContentRepository\Core\Projection\CatchUpHookFactoryInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentSubgraphInterface; @@ -17,14 +18,13 @@ use Neos\ContentRepository\Core\Projection\ProjectionFactoryInterface; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryIds; -use Neos\ContentRepository\Core\SharedModel\Auth\AuthProviderInterface; use Neos\ContentRepositoryRegistry\Exception\ContentRepositoryNotFoundException; use Neos\ContentRepositoryRegistry\Exception\InvalidConfigurationException; +use Neos\ContentRepositoryRegistry\Factory\AuthProvider\AuthProviderFactoryInterface; use Neos\ContentRepositoryRegistry\Factory\Clock\ClockFactoryInterface; use Neos\ContentRepositoryRegistry\Factory\ContentDimensionSource\ContentDimensionSourceFactoryInterface; use Neos\ContentRepositoryRegistry\Factory\EventStore\EventStoreFactoryInterface; use Neos\ContentRepositoryRegistry\Factory\NodeTypeManager\NodeTypeManagerFactoryInterface; -use Neos\ContentRepositoryRegistry\Factory\AuthProvider\AuthProviderFactoryInterface; use Neos\ContentRepositoryRegistry\SubgraphCachingInMemory\ContentSubgraphWithRuntimeCaches; use Neos\ContentRepositoryRegistry\SubgraphCachingInMemory\SubgraphCachePool; use Neos\EventStore\EventStoreInterface; diff --git a/Neos.ContentRepositoryRegistry/Classes/Factory/AuthProvider/AuthProviderFactoryInterface.php b/Neos.ContentRepositoryRegistry/Classes/Factory/AuthProvider/AuthProviderFactoryInterface.php index 9aebd57e688..809efdf65db 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Factory/AuthProvider/AuthProviderFactoryInterface.php +++ b/Neos.ContentRepositoryRegistry/Classes/Factory/AuthProvider/AuthProviderFactoryInterface.php @@ -4,8 +4,8 @@ namespace Neos\ContentRepositoryRegistry\Factory\AuthProvider; +use Neos\ContentRepository\Core\Feature\Security\AuthProviderInterface; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; -use Neos\ContentRepository\Core\SharedModel\Auth\AuthProviderInterface; /** * @api diff --git a/Neos.ContentRepositoryRegistry/Classes/Factory/AuthProvider/StaticAuthProviderFactory.php b/Neos.ContentRepositoryRegistry/Classes/Factory/AuthProvider/StaticAuthProviderFactory.php index 4cc8b9a7ebd..870f2e5ace2 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Factory/AuthProvider/StaticAuthProviderFactory.php +++ b/Neos.ContentRepositoryRegistry/Classes/Factory/AuthProvider/StaticAuthProviderFactory.php @@ -2,10 +2,10 @@ declare(strict_types=1); namespace Neos\ContentRepositoryRegistry\Factory\AuthProvider; +use Neos\ContentRepository\Core\Feature\Security\AuthProviderInterface; +use Neos\ContentRepository\Core\Feature\Security\Dto\UserId; +use Neos\ContentRepository\Core\Feature\Security\StaticAuthProvider; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; -use Neos\ContentRepository\Core\SharedModel\Auth\StaticAuthProvider; -use Neos\ContentRepository\Core\SharedModel\Auth\UserId; -use Neos\ContentRepository\Core\SharedModel\Auth\AuthProviderInterface; /** * @api diff --git a/Neos.Neos/Classes/Controller/Frontend/NodeController.php b/Neos.Neos/Classes/Controller/Frontend/NodeController.php index 9d5756d5fba..53d98b82c94 100644 --- a/Neos.Neos/Classes/Controller/Frontend/NodeController.php +++ b/Neos.Neos/Classes/Controller/Frontend/NodeController.php @@ -197,8 +197,12 @@ public function showAction(string $node): void $contentRepository = $this->contentRepositoryRegistry->get($nodeAddress->contentRepositoryId); $uncachedSubgraph = $contentRepository->getContentSubgraph($nodeAddress->workspaceName, $nodeAddress->dimensionSpacePoint); - // todo document + + // By default, the visibility constraints only contain the SubtreeTags the authenticated user has _no_ access to + // Neos backend users have access to the "disabled" SubtreeTag so that they can see/edit disabled nodes. + // In this showAction (= "frontend") we have to explicitly remove those disabled nodes, even if the user was authenticated $uncachedSubgraph = $uncachedSubgraph->withVisibilityConstraints($uncachedSubgraph->getVisibilityConstraints()->withAddedSubtreeTag(SubtreeTag::disabled())); + $subgraph = new ContentSubgraphWithRuntimeCaches($uncachedSubgraph, $this->subgraphCachePool); $nodeInstance = $subgraph->findNodeById($nodeAddress->aggregateId); diff --git a/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php index 6179994e815..293ff100e32 100644 --- a/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php +++ b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php @@ -6,6 +6,9 @@ use Neos\ContentRepository\Core\CommandHandler\CommandInterface; use Neos\ContentRepository\Core\Feature\Common\RebasableToOtherWorkspaceInterface; +use Neos\ContentRepository\Core\Feature\Security\AuthProviderInterface; +use Neos\ContentRepository\Core\Feature\Security\Dto\Privilege; +use Neos\ContentRepository\Core\Feature\Security\Dto\UserId; use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Command\CreateRootWorkspace; use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Command\CreateWorkspace; use Neos\ContentRepository\Core\Feature\WorkspaceModification\Command\ChangeBaseWorkspace; @@ -16,9 +19,6 @@ use Neos\ContentRepository\Core\Feature\WorkspacePublication\Command\PublishWorkspace; use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Command\RebaseWorkspace; use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; -use Neos\ContentRepository\Core\SharedModel\Auth\AuthProviderInterface; -use Neos\ContentRepository\Core\SharedModel\Auth\Privilege; -use Neos\ContentRepository\Core\SharedModel\Auth\UserId; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\Flow\Security\Context as SecurityContext; From d58fb9809f9f6446b1bc1a209a21b20ff258a90b Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Tue, 22 Oct 2024 19:27:24 +0200 Subject: [PATCH 020/214] `VisibilityConstraints` not only for Neos users --- .../src/Domain/Repository/ContentSubgraph.php | 15 --------------- .../Domain/Repository/ContentSubhypergraph.php | 15 --------------- .../ContentGraph/ContentSubgraphInterface.php | 2 -- .../ContentSubgraphWithRuntimeCaches.php | 5 ----- .../Controller/Frontend/NodeController.php | 12 ++++++++---- .../ContentRepositoryAuthorizationService.php | 14 +------------- .../ContentRepositoryAuthProvider.php | 6 +----- 7 files changed, 10 insertions(+), 59 deletions(-) diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php index 72b74631867..810a095069f 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php @@ -122,21 +122,6 @@ public function getVisibilityConstraints(): VisibilityConstraints return $this->visibilityConstraints; } - public function withVisibilityConstraints(VisibilityConstraints $newVisibilityConstraints): self - { - return new self( - $this->contentRepositoryId, - $this->workspaceName, - $this->contentStreamId, - $this->dimensionSpacePoint, - $newVisibilityConstraints, - $this->dbal, - $this->nodeFactory, - $this->nodeTypeManager, - $this->nodeQueryBuilder->tableNames, - ); - } - public function findChildNodes(NodeAggregateId $parentNodeAggregateId, FindChildNodesFilter $filter): Nodes { $queryBuilder = $this->buildChildNodesQuery($parentNodeAggregateId, $filter); diff --git a/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/ContentSubhypergraph.php b/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/ContentSubhypergraph.php index 994a880dae7..8b312166d2a 100644 --- a/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/ContentSubhypergraph.php +++ b/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/ContentSubhypergraph.php @@ -106,21 +106,6 @@ public function getVisibilityConstraints(): VisibilityConstraints return $this->visibilityConstraints; } - public function withVisibilityConstraints(VisibilityConstraints $newVisibilityConstraints): self - { - return new self( - $this->contentRepositoryId, - $this->contentStreamId, - $this->workspaceName, - $this->dimensionSpacePoint, - $newVisibilityConstraints, - $this->dbal, - $this->nodeFactory, - $this->nodeTypeManager, - $this->tableNamePrefix, - ); - } - public function findNodeById(NodeAggregateId $nodeAggregateId): ?Node { $query = HypergraphQuery::create($this->contentStreamId, $this->tableNamePrefix); diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentSubgraphInterface.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentSubgraphInterface.php index d2df7ce2dd7..c47c0771932 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentSubgraphInterface.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentSubgraphInterface.php @@ -57,8 +57,6 @@ public function getDimensionSpacePoint(): DimensionSpacePoint; public function getVisibilityConstraints(): VisibilityConstraints; - public function withVisibilityConstraints(VisibilityConstraints $newVisibilityConstraints): self; - /** * Find a single node by its aggregate id * diff --git a/Neos.ContentRepositoryRegistry/Classes/SubgraphCachingInMemory/ContentSubgraphWithRuntimeCaches.php b/Neos.ContentRepositoryRegistry/Classes/SubgraphCachingInMemory/ContentSubgraphWithRuntimeCaches.php index f9f88c2e164..c26bcef38d1 100644 --- a/Neos.ContentRepositoryRegistry/Classes/SubgraphCachingInMemory/ContentSubgraphWithRuntimeCaches.php +++ b/Neos.ContentRepositoryRegistry/Classes/SubgraphCachingInMemory/ContentSubgraphWithRuntimeCaches.php @@ -77,11 +77,6 @@ public function getVisibilityConstraints(): VisibilityConstraints return $this->wrappedContentSubgraph->getVisibilityConstraints(); } - public function withVisibilityConstraints(VisibilityConstraints $newVisibilityConstraints): self - { - return new self($this->wrappedContentSubgraph->withVisibilityConstraints($newVisibilityConstraints), $this->subgraphCachePool); - } - public function findChildNodes(NodeAggregateId $parentNodeAggregateId, FindChildNodesFilter $filter): Nodes { if (!self::isFilterEmpty($filter)) { diff --git a/Neos.Neos/Classes/Controller/Frontend/NodeController.php b/Neos.Neos/Classes/Controller/Frontend/NodeController.php index 53d98b82c94..e96a0ea4fd7 100644 --- a/Neos.Neos/Classes/Controller/Frontend/NodeController.php +++ b/Neos.Neos/Classes/Controller/Frontend/NodeController.php @@ -36,6 +36,7 @@ use Neos\Flow\Session\SessionInterface; use Neos\Flow\Utility\Now; use Neos\Neos\Domain\Model\RenderingMode; +use Neos\Neos\Domain\Model\User; use Neos\Neos\Domain\Service\NodeTypeNameFactory; use Neos\Neos\Domain\Service\RenderingModeService; use Neos\Neos\FrontendRouting\Exception\InvalidShortcutException; @@ -43,6 +44,7 @@ use Neos\Neos\FrontendRouting\NodeShortcutResolver; use Neos\Neos\FrontendRouting\NodeUriBuilderFactory; use Neos\Neos\FrontendRouting\SiteDetection\SiteDetectionResult; +use Neos\Neos\Security\Authorization\ContentRepositoryAuthorizationService; use Neos\Neos\Utility\NodeTypeWithFallbackProvider; use Neos\Neos\View\FusionView; @@ -111,6 +113,9 @@ class NodeController extends ActionController #[Flow\Inject] protected NodeUriBuilderFactory $nodeUriBuilderFactory; + #[Flow\Inject] + protected ContentRepositoryAuthorizationService $contentRepositoryAuthorizationService; + /** * @param string $node * @throws NodeNotFoundException @@ -189,19 +194,18 @@ public function previewAction(string $node): void public function showAction(string $node): void { $nodeAddress = NodeAddress::fromJsonString($node); - unset($node); if (!$nodeAddress->workspaceName->isLive()) { throw new NodeNotFoundException('The requested node isn\'t accessible to the current user', 1430218623); } $contentRepository = $this->contentRepositoryRegistry->get($nodeAddress->contentRepositoryId); - $uncachedSubgraph = $contentRepository->getContentSubgraph($nodeAddress->workspaceName, $nodeAddress->dimensionSpacePoint); - // By default, the visibility constraints only contain the SubtreeTags the authenticated user has _no_ access to // Neos backend users have access to the "disabled" SubtreeTag so that they can see/edit disabled nodes. // In this showAction (= "frontend") we have to explicitly remove those disabled nodes, even if the user was authenticated - $uncachedSubgraph = $uncachedSubgraph->withVisibilityConstraints($uncachedSubgraph->getVisibilityConstraints()->withAddedSubtreeTag(SubtreeTag::disabled())); + $visibilityConstraints = $this->contentRepositoryAuthorizationService->getVisibilityConstraintsForRoles($contentRepository->id, $this->securityContext->getRoles()); + $visibilityConstraints = $visibilityConstraints->withAddedSubtreeTag(SubtreeTag::disabled()); + $uncachedSubgraph = $contentRepository->getContentGraph($nodeAddress->workspaceName)->getSubgraph($nodeAddress->dimensionSpacePoint, $visibilityConstraints); $subgraph = new ContentSubgraphWithRuntimeCaches($uncachedSubgraph, $this->subgraphCachePool); diff --git a/Neos.Neos/Classes/Security/Authorization/ContentRepositoryAuthorizationService.php b/Neos.Neos/Classes/Security/Authorization/ContentRepositoryAuthorizationService.php index 3d056cae5a3..f37a03a33b3 100644 --- a/Neos.Neos/Classes/Security/Authorization/ContentRepositoryAuthorizationService.php +++ b/Neos.Neos/Classes/Security/Authorization/ContentRepositoryAuthorizationService.php @@ -85,22 +85,10 @@ public function getWorkspacePermissionsForUser(ContentRepositoryId $contentRepos ); } - public function getVisibilityConstraintsForAnonymousUser(ContentRepositoryId $contentRepositoryId): VisibilityConstraints - { - $roles = array_map($this->policyService->getRole(...), [self::FLOW_ROLE_EVERYBODY, self::FLOW_ROLE_ANONYMOUS]); - return $this->visibilityConstraintsForRoles($contentRepositoryId, $roles); - } - - public function getVisibilityConstraintsForUser(ContentRepositoryId $contentRepositoryId, User $user): VisibilityConstraints - { - $userRoles = $this->rolesForUser($user); - return $this->visibilityConstraintsForRoles($contentRepositoryId, $userRoles); - } - /** * @param array $roles */ - private function visibilityConstraintsForRoles(ContentRepositoryId $contentRepositoryId, array $roles): VisibilityConstraints + public function getVisibilityConstraintsForRoles(ContentRepositoryId $contentRepositoryId, array $roles): VisibilityConstraints { $restrictedSubtreeTags = []; /** @var SubtreeTagPrivilege $privilege */ diff --git a/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php index 293ff100e32..bd8b020c877 100644 --- a/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php +++ b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php @@ -56,11 +56,7 @@ public function getVisibilityConstraints(WorkspaceName $workspaceName): Visibili if ($this->securityContext->areAuthorizationChecksDisabled()) { return VisibilityConstraints::default(); } - $user = $this->userService->getCurrentUser(); - if ($user === null) { - return $this->authorizationService->getVisibilityConstraintsForAnonymousUser($this->contentRepositoryId); - } - return $this->authorizationService->getVisibilityConstraintsForUser($this->contentRepositoryId, $user); + return $this->authorizationService->getVisibilityConstraintsForRoles($this->contentRepositoryId, $this->securityContext->getRoles()); } public function getReadNodesFromWorkspacePrivilege(WorkspaceName $workspaceName): Privilege From cab0bd6bca905000b35b9e16d80eefcda1d8c5c6 Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Tue, 22 Oct 2024 20:05:35 +0200 Subject: [PATCH 021/214] Tweaks --- .../Classes/ContentRepository.php | 1 - .../Domain/Service/WorkspaceService.php | 20 +++++++++---------- .../Controller/WorkspaceController.php | 10 ++++++++++ 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/ContentRepository.php b/Neos.ContentRepository.Core/Classes/ContentRepository.php index b2ccf9f21ee..e3eb1b81dec 100644 --- a/Neos.ContentRepository.Core/Classes/ContentRepository.php +++ b/Neos.ContentRepository.Core/Classes/ContentRepository.php @@ -112,7 +112,6 @@ public function handle(CommandInterface $command): void // publishing themselves $eventsToPublish = $this->commandBus->handle($command, $this->commandHandlingDependencies); - // TODO meaningful exception message $initiatingUserId = $this->authProvider->getAuthenticatedUserId() ?? UserId::forSystemUser(); $initiatingTimestamp = $this->clock->now()->format(\DateTimeInterface::ATOM); diff --git a/Neos.Neos/Classes/Domain/Service/WorkspaceService.php b/Neos.Neos/Classes/Domain/Service/WorkspaceService.php index 37d3b09e1ad..5d31865c781 100644 --- a/Neos.Neos/Classes/Domain/Service/WorkspaceService.php +++ b/Neos.Neos/Classes/Domain/Service/WorkspaceService.php @@ -27,7 +27,6 @@ use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Annotations as Flow; -use Neos\Flow\Security\Context as SecurityContext; use Neos\Neos\Domain\Model\User; use Neos\Neos\Domain\Model\UserId; use Neos\Neos\Domain\Model\WorkspaceClassification; @@ -45,20 +44,17 @@ /** * Central authority to interact with Content Repository Workspaces within Neos * - * TODO evaluate permissions for workspace changes - * * @api */ #[Flow\Scope('singleton')] -final class WorkspaceService +final readonly class WorkspaceService { private const TABLE_NAME_WORKSPACE_METADATA = 'neos_neos_workspace_metadata'; private const TABLE_NAME_WORKSPACE_ROLE = 'neos_neos_workspace_role'; public function __construct( - private readonly ContentRepositoryRegistry $contentRepositoryRegistry, - private readonly Connection $dbal, - private readonly SecurityContext $securityContext, + private ContentRepositoryRegistry $contentRepositoryRegistry, + private Connection $dbal, ) { } @@ -83,10 +79,11 @@ public function getWorkspaceMetadata(ContentRepositoryId $contentRepositoryId, W /** * Update/set title metadata for the specified workspace + * + * NOTE: The workspace privileges are not evaluated for this interaction, this should be done in the calling side if needed */ public function setWorkspaceTitle(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, WorkspaceTitle $newWorkspaceTitle): void { - // TODO check workspace permissions $this->updateWorkspaceMetadata($contentRepositoryId, $workspaceName, [ 'title' => $newWorkspaceTitle->value, ]); @@ -94,10 +91,11 @@ public function setWorkspaceTitle(ContentRepositoryId $contentRepositoryId, Work /** * Update/set description metadata for the specified workspace + * + * NOTE: The workspace privileges are not evaluated for this interaction, this should be done in the calling side if needed */ public function setWorkspaceDescription(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, WorkspaceDescription $newWorkspaceDescription): void { - // TODO check workspace permissions $this->updateWorkspaceMetadata($contentRepositoryId, $workspaceName, [ 'description' => $newWorkspaceDescription->value, ]); @@ -178,14 +176,14 @@ public function createPersonalWorkspaceForUserIfMissing(ContentRepositoryId $con return; } $workspaceName = $this->getUniqueWorkspaceName($contentRepositoryId, $user->getLabel()); - $this->securityContext->withoutAuthorizationChecks(fn () => $this->createPersonalWorkspace( + $this->createPersonalWorkspace( $contentRepositoryId, $workspaceName, WorkspaceTitle::fromString($user->getLabel()), WorkspaceDescription::empty(), WorkspaceName::forLive(), $user->getId(), - )); + ); } /** diff --git a/Neos.Workspace.Ui/Classes/Controller/WorkspaceController.php b/Neos.Workspace.Ui/Classes/Controller/WorkspaceController.php index 494b6900c8e..ea63307c93a 100644 --- a/Neos.Workspace.Ui/Classes/Controller/WorkspaceController.php +++ b/Neos.Workspace.Ui/Classes/Controller/WorkspaceController.php @@ -43,6 +43,7 @@ use Neos\Flow\Package\PackageManager; use Neos\Flow\Property\PropertyMapper; use Neos\Flow\Security\Context; +use Neos\Flow\Security\Exception\AccessDeniedException; use Neos\Media\Domain\Model\AssetInterface; use Neos\Media\Domain\Model\ImageInterface; use Neos\Neos\Controller\Module\AbstractModuleController; @@ -288,6 +289,15 @@ public function updateAction( $contentRepositoryId = SiteDetectionResult::fromRequest($this->request->getHttpRequest())->contentRepositoryId; $contentRepository = $this->contentRepositoryRegistry->get($contentRepositoryId); + $user = $this->userService->getCurrentUser(); + if ($user === null) { + throw new AccessDeniedException('No user is authenticated', 1729620262); + } + $workspacePermissions = $this->contentRepositoryAuthorizationService->getWorkspacePermissionsForUser($contentRepository->id, $workspaceName, $user); + if (!$workspacePermissions->manage) { + throw new AccessDeniedException(sprintf('The authenticated user does not have manage permissions for workspace "%s"', $workspaceName->value), 1729620297); + } + if ($title->value === '') { $title = WorkspaceTitle::fromString($workspaceName->value); } From 630bc09a2e0316c456e133119120cec10e1c35b2 Mon Sep 17 00:00:00 2001 From: Martin Ficzel Date: Wed, 23 Oct 2024 10:50:58 +0200 Subject: [PATCH 022/214] Verify tha that Live workspace contains no events prior to importing --- .../Import/LiveWorkspaceIsEmptyProcessor.php | 53 +++++++++++++++++++ .../LiveWorkspaceIsEmptyProcessorFactory.php | 27 ++++++++++ .../Domain/Service/SiteImportService.php | 3 +- 3 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 Neos.Neos/Classes/Domain/Import/LiveWorkspaceIsEmptyProcessor.php create mode 100644 Neos.Neos/Classes/Domain/Import/LiveWorkspaceIsEmptyProcessorFactory.php diff --git a/Neos.Neos/Classes/Domain/Import/LiveWorkspaceIsEmptyProcessor.php b/Neos.Neos/Classes/Domain/Import/LiveWorkspaceIsEmptyProcessor.php new file mode 100644 index 00000000000..82f6a967c07 --- /dev/null +++ b/Neos.Neos/Classes/Domain/Import/LiveWorkspaceIsEmptyProcessor.php @@ -0,0 +1,53 @@ +dispatch(Severity::NOTICE, 'Ensures empty live workspace'); + + if ($this->workspaceHasEvents(WorkspaceName::forLive())) { + throw new \RuntimeException( 'Live workspace already contains events please run "cr:prune" before importing.'); + } + } + + private function workspaceHasEvents(WorkspaceName $workspaceName): bool + { + $workspaceStreamName = WorkspaceEventStreamName::fromWorkspaceName($workspaceName)->getEventStreamName(); + $eventStream = $this->eventStore->load($workspaceStreamName); + foreach ($eventStream as $event) { + return true; + } + return false; + } +} diff --git a/Neos.Neos/Classes/Domain/Import/LiveWorkspaceIsEmptyProcessorFactory.php b/Neos.Neos/Classes/Domain/Import/LiveWorkspaceIsEmptyProcessorFactory.php new file mode 100644 index 00000000000..792b0495cac --- /dev/null +++ b/Neos.Neos/Classes/Domain/Import/LiveWorkspaceIsEmptyProcessorFactory.php @@ -0,0 +1,27 @@ +eventStore); + } +} diff --git a/Neos.Neos/Classes/Domain/Service/SiteImportService.php b/Neos.Neos/Classes/Domain/Service/SiteImportService.php index 07107821f2c..50a1a044238 100644 --- a/Neos.Neos/Classes/Domain/Service/SiteImportService.php +++ b/Neos.Neos/Classes/Domain/Service/SiteImportService.php @@ -33,6 +33,7 @@ use Neos\Media\Domain\Repository\AssetRepository; use Neos\Neos\Domain\Import\DoctrineMigrateProcessor; use Neos\Neos\Domain\Import\LiveWorkspaceCreationProcessor; +use Neos\Neos\Domain\Import\LiveWorkspaceIsEmptyProcessorFactory; use Neos\Neos\Domain\Import\SiteCreationProcessor; use Neos\Neos\Domain\Repository\SiteRepository; @@ -69,7 +70,7 @@ public function importFromPath(ContentRepositoryId $contentRepositoryId, string $processors = [ 'Run doctrine migrations' => new DoctrineMigrateProcessor($this->doctrineService), 'Setup content repository' => $this->contentRepositoryRegistry->buildService($contentRepositoryId, new ContentRepositorySetupProcessorFactory()), - // TODO Check if target content stream is empty, otherwise => nice error "prune..." + 'Verify Live workspace does not exist yet' => $this->contentRepositoryRegistry->buildService($contentRepositoryId, new LiveWorkspaceIsEmptyProcessorFactory()), 'Create Neos sites' => new SiteCreationProcessor($this->siteRepository), 'Create Live workspace' => new LiveWorkspaceCreationProcessor($contentRepository, $this->workspaceService), 'Import events' => $this->contentRepositoryRegistry->buildService($contentRepositoryId, new EventStoreImportProcessorFactory(WorkspaceName::forLive(), keepEventIds: true)), From 221096b5afc90b221c266289ac71c22c61901b04 Mon Sep 17 00:00:00 2001 From: Martin Ficzel Date: Wed, 23 Oct 2024 10:51:21 +0200 Subject: [PATCH 023/214] Ensure sites are persisted during import --- Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php b/Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php index e4d6c0c6d92..401a8fd4810 100644 --- a/Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php +++ b/Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php @@ -20,6 +20,7 @@ use Neos\ContentRepository\Export\ProcessingContext; use Neos\ContentRepository\Export\ProcessorInterface; use Neos\ContentRepository\Export\Severity; +use Neos\Flow\Persistence\Doctrine\PersistenceManager; use Neos\Neos\Domain\Model\Site; use Neos\Neos\Domain\Repository\SiteRepository; @@ -32,6 +33,7 @@ { public function __construct( private SiteRepository $siteRepository, + private PersistenceManager $persistenceManager ) { } @@ -47,6 +49,7 @@ public function run(ProcessingContext $context): void } else { $sites = self::extractSitesFromEventStream($context); } + $persistAllIsRequired = false; /** @var SiteShape $site */ foreach ($sites as $site) { $context->dispatch(Severity::NOTICE, "Creating site \"{$site['name']}\""); @@ -56,16 +59,19 @@ public function run(ProcessingContext $context): void $context->dispatch(Severity::NOTICE, "Site for node name \"{$siteNodeName->value}\" already exists, skipping"); continue; } - // TODO use node aggregate identifier instead of node name $siteInstance = new Site($siteNodeName->value); $siteInstance->setSiteResourcesPackageKey($site['packageKey']); $siteInstance->setState(($site['inactive'] ?? false) ? Site::STATE_OFFLINE : Site::STATE_ONLINE); $siteInstance->setName($site['name']); $this->siteRepository->add($siteInstance); + $persistAllIsRequired = true; // TODO add domains? } + if ($persistAllIsRequired) { + $this->persistenceManager->persistAll(); + } } /** From 7ef315c4ac0a99fa38b1563edd9a3c3bbfc841cd Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Wed, 23 Oct 2024 11:31:53 +0200 Subject: [PATCH 024/214] Fix doc comments for `workspace:assignRole` command --- Neos.Neos/Classes/Command/WorkspaceCommandController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Neos.Neos/Classes/Command/WorkspaceCommandController.php b/Neos.Neos/Classes/Command/WorkspaceCommandController.php index d4d9f2688ef..de14714430f 100644 --- a/Neos.Neos/Classes/Command/WorkspaceCommandController.php +++ b/Neos.Neos/Classes/Command/WorkspaceCommandController.php @@ -269,7 +269,7 @@ public function setDescriptionCommand(string $workspace, string $newDescription, * * @param string $workspace Name of the workspace, for example "some-workspace" * @param string $subject The user/group that should be assigned. By default, this is expected to be a Flow role identifier (e.g. 'Neos.Neos:AbstractEditor') – if $type is 'user', this is the username (aka account identifier) of a Neos user - * @param string $role Role to assign, either 'collaborator' or 'manager' – a collaborator can read and write from/to the workspace. A manager can _on top_ change the workspace metadata & roles itself + * @param string $role Role to assign, either 'viewer', 'collaborator' or 'manager' – a viewer can only read from the workspace, a collaborator can read and write from/to the workspace. A manager can _on top_ change the workspace metadata & roles itself * @param string $contentRepository Identifier of the content repository. (Default: 'default') * @param string $type Type of role, either 'group' (default) or 'user' – if 'group', $subject is expected to be a Flow role identifier, otherwise the username (aka account identifier) of a Neos user * @throws StopCommandException From 35ff7db46de4ecabbcdfffc6ef3f0eb4fa7261bb Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Wed, 23 Oct 2024 11:45:55 +0200 Subject: [PATCH 025/214] Determine privileges based on the (authenticated) `Account` ..instead of the Neos `User`. Reasoning: - A Neos `User` can have multiple accounts assigned - It makes sense to evaluate all authenticated roles e.g. for frontend authentication) --- .../Classes/Controller/UsageController.php | 18 +++- .../Controller/Frontend/NodeController.php | 7 +- .../Domain/Service/WorkspaceService.php | 4 +- .../ContentRepositoryAuthorizationService.php | 93 ++++++++++++++----- .../ContentRepositoryAuthProvider.php | 18 ++-- .../Controller/WorkspaceController.php | 33 +++---- 6 files changed, 118 insertions(+), 55 deletions(-) diff --git a/Neos.Media.Browser/Classes/Controller/UsageController.php b/Neos.Media.Browser/Classes/Controller/UsageController.php index 5d11138f12c..a62d36b6298 100644 --- a/Neos.Media.Browser/Classes/Controller/UsageController.php +++ b/Neos.Media.Browser/Classes/Controller/UsageController.php @@ -19,6 +19,7 @@ use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Annotations as Flow; use Neos\Flow\Mvc\Controller\ActionController; +use Neos\Flow\Security\Context as SecurityContext; use Neos\Media\Domain\Model\AssetInterface; use Neos\Media\Domain\Service\AssetService; use Neos\Neos\Domain\Repository\SiteRepository; @@ -66,6 +67,12 @@ class UsageController extends ActionController */ protected $workspaceService; + /** + * @Flow\Inject + * @var SecurityContext + */ + protected $securityContext; + /** * @Flow\Inject * @var ContentRepositoryAuthorizationService @@ -110,11 +117,12 @@ public function relatedNodesAction(AssetInterface $asset) ); $nodeType = $nodeAggregate ? $contentRepository->getNodeTypeManager()->getNodeType($nodeAggregate->nodeTypeName) : null; - $workspacePermissions = $this->contentRepositoryAuthorizationService->getWorkspacePermissionsForUser( - $currentContentRepositoryId, - $usage->getWorkspaceName(), - $currentUser - ); + $authenticatedAccount = $this->securityContext->getAccount(); + if ($authenticatedAccount !== null) { + $workspacePermissions = $this->contentRepositoryAuthorizationService->getWorkspacePermissionsForAccount($currentContentRepositoryId, $usage->getWorkspaceName(), $authenticatedAccount); + } else { + $workspacePermissions = $this->contentRepositoryAuthorizationService->getWorkspacePermissionsForAnonymousUser($currentContentRepositoryId, $usage->getWorkspaceName()); + } $workspace = $contentRepository->findWorkspaceByName($usage->getWorkspaceName()); diff --git a/Neos.Neos/Classes/Controller/Frontend/NodeController.php b/Neos.Neos/Classes/Controller/Frontend/NodeController.php index e96a0ea4fd7..db146184bdb 100644 --- a/Neos.Neos/Classes/Controller/Frontend/NodeController.php +++ b/Neos.Neos/Classes/Controller/Frontend/NodeController.php @@ -203,7 +203,12 @@ public function showAction(string $node): void // By default, the visibility constraints only contain the SubtreeTags the authenticated user has _no_ access to // Neos backend users have access to the "disabled" SubtreeTag so that they can see/edit disabled nodes. // In this showAction (= "frontend") we have to explicitly remove those disabled nodes, even if the user was authenticated - $visibilityConstraints = $this->contentRepositoryAuthorizationService->getVisibilityConstraintsForRoles($contentRepository->id, $this->securityContext->getRoles()); + $authenticatedAccount = $this->securityContext->getAccount(); + if ($authenticatedAccount !== null) { + $visibilityConstraints = $this->contentRepositoryAuthorizationService->getVisibilityConstraintsForAccount($contentRepository->id, $authenticatedAccount); + } else { + $visibilityConstraints = $this->contentRepositoryAuthorizationService->getVisibilityConstraintsForAnonymousUser($contentRepository->id); + } $visibilityConstraints = $visibilityConstraints->withAddedSubtreeTag(SubtreeTag::disabled()); $uncachedSubgraph = $contentRepository->getContentGraph($nodeAddress->workspaceName)->getSubgraph($nodeAddress->dimensionSpacePoint, $visibilityConstraints); diff --git a/Neos.Neos/Classes/Domain/Service/WorkspaceService.php b/Neos.Neos/Classes/Domain/Service/WorkspaceService.php index 5d31865c781..6cc3ecfe4ac 100644 --- a/Neos.Neos/Classes/Domain/Service/WorkspaceService.php +++ b/Neos.Neos/Classes/Domain/Service/WorkspaceService.php @@ -236,7 +236,7 @@ public function unassignWorkspaceRole(ContentRepositoryId $contentRepositoryId, /** * Get all role assignments for the specified workspace * - * NOTE: This should never be used to evaluate permissions, instead {@see ContentRepositoryAuthorizationService::getWorkspacePermissionsForUser()} should be used! + * NOTE: This should never be used to evaluate permissions, instead {@see ContentRepositoryAuthorizationService::getWorkspacePermissionsForAccount()} and {@see ContentRepositoryAuthorizationService::getWorkspacePermissionsForAnonymousUser()} should be used! */ public function getWorkspaceRoleAssignments(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName): WorkspaceRoleAssignments { @@ -272,7 +272,7 @@ public function getWorkspaceRoleAssignments(ContentRepositoryId $contentReposito /** * Get the role with the most privileges for the specified {@see WorkspaceRoleSubjects} on workspace $workspaceName * - * NOTE: This should never be used to evaluate permissions, instead {@see ContentRepositoryAuthorizationService::getWorkspacePermissionsForUser()} should be used! + * NOTE: This should never be used to evaluate permissions, instead {@see ContentRepositoryAuthorizationService::getWorkspacePermissionsForAccount()} and {@see ContentRepositoryAuthorizationService::getWorkspacePermissionsForAnonymousUser()} should be used! */ public function getMostPrivilegedWorkspaceRoleForSubjects(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, WorkspaceRoleSubjects $subjects): ?WorkspaceRole { diff --git a/Neos.Neos/Classes/Security/Authorization/ContentRepositoryAuthorizationService.php b/Neos.Neos/Classes/Security/Authorization/ContentRepositoryAuthorizationService.php index f37a03a33b3..6bb13616c79 100644 --- a/Neos.Neos/Classes/Security/Authorization/ContentRepositoryAuthorizationService.php +++ b/Neos.Neos/Classes/Security/Authorization/ContentRepositoryAuthorizationService.php @@ -9,8 +9,8 @@ use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\Flow\Annotations as Flow; +use Neos\Flow\Security\Account; use Neos\Flow\Security\Authorization\PrivilegeManagerInterface; -use Neos\Flow\Security\Exception\NoSuchRoleException; use Neos\Flow\Security\Policy\PolicyService; use Neos\Flow\Security\Policy\Role; use Neos\Neos\Domain\Model\User; @@ -18,10 +18,10 @@ use Neos\Neos\Domain\Model\WorkspaceRole; use Neos\Neos\Domain\Model\WorkspaceRoleSubject; use Neos\Neos\Domain\Model\WorkspaceRoleSubjects; -use Neos\Neos\Domain\Service\UserService; use Neos\Neos\Domain\Service\WorkspaceService; use Neos\Neos\Security\Authorization\Privilege\SubtreeTagPrivilege; use Neos\Neos\Security\Authorization\Privilege\SubtreeTagPrivilegeSubject; +use Neos\Party\Domain\Service\PartyService; /** * @api @@ -31,65 +31,77 @@ { private const FLOW_ROLE_EVERYBODY = 'Neos.Flow:Everybody'; private const FLOW_ROLE_ANONYMOUS = 'Neos.Flow:Anonymous'; - private const FLOW_ROLE_ADMINISTRATOR = 'Neos.Neos:Administrator'; + private const FLOW_ROLE_AUTHENTICATED_USER = 'Neos.Flow:AuthenticatedUser'; + private const FLOW_ROLE_NEOS_ADMINISTRATOR = 'Neos.Neos:Administrator'; public function __construct( - private UserService $userService, + private PartyService $partyService, private WorkspaceService $workspaceService, private PolicyService $policyService, private PrivilegeManagerInterface $privilegeManager, ) { } + /** + * Determines the {@see WorkspacePermissions} an anonymous user has for the specified workspace (aka "public access") + */ public function getWorkspacePermissionsForAnonymousUser(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName): WorkspacePermissions { $subjects = [WorkspaceRoleSubject::createForGroup(self::FLOW_ROLE_EVERYBODY), WorkspaceRoleSubject::createForGroup(self::FLOW_ROLE_ANONYMOUS)]; $userWorkspaceRole = $this->workspaceService->getMostPrivilegedWorkspaceRoleForSubjects($contentRepositoryId, $workspaceName, WorkspaceRoleSubjects::fromArray($subjects)); if ($userWorkspaceRole === null) { - return WorkspacePermissions::none("Anonymous user has no explicit role for workspace '{$workspaceName->value}'"); + return WorkspacePermissions::none(sprintf('Anonymous user has no explicit role for workspace "%s"', $workspaceName->value)); } return WorkspacePermissions::create( read: $userWorkspaceRole->isAtLeast(WorkspaceRole::VIEWER), write: $userWorkspaceRole->isAtLeast(WorkspaceRole::COLLABORATOR), manage: $userWorkspaceRole->isAtLeast(WorkspaceRole::MANAGER), - reason: "Anonymous user has role '{$userWorkspaceRole->value}' for workspace '{$workspaceName->value}'", + reason: sprintf('Anonymous user has role "%s" for workspace "%s"', $userWorkspaceRole->value, $workspaceName->value), ); } /** - * Determines the permission the given user has for the specified workspace {@see WorkspacePermissions} + * Determines the {@see WorkspacePermissions} the given user has for the specified workspace */ - public function getWorkspacePermissionsForUser(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, User $user): WorkspacePermissions + public function getWorkspacePermissionsForAccount(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, Account $account): WorkspacePermissions { $workspaceMetadata = $this->workspaceService->getWorkspaceMetadata($contentRepositoryId, $workspaceName); - if ($workspaceMetadata->ownerUserId !== null && $workspaceMetadata->ownerUserId->equals($user->getId())) { - return WorkspacePermissions::all("User '{$user->getLabel()}' (id: {$user->getId()->value} is the owner of workspace '{$workspaceName->value}'"); + $neosUser = $this->neosUserFromAccount($account); + if ($workspaceMetadata->ownerUserId !== null && $neosUser !== null && $neosUser->getId()->equals($workspaceMetadata->ownerUserId)) { + return WorkspacePermissions::all(sprintf('User "%s" (id: %s is the owner of workspace "%s"', $neosUser->getLabel(), $neosUser->getId()->value, $workspaceName->value)); } - $userRoles = $this->rolesForUser($user); - $userIsAdministrator = array_key_exists(self::FLOW_ROLE_ADMINISTRATOR, $userRoles); + $userRoles = $this->expandAccountRoles($account); + $userIsAdministrator = array_key_exists(self::FLOW_ROLE_NEOS_ADMINISTRATOR, $userRoles); $subjects = array_map(WorkspaceRoleSubject::createForGroup(...), array_keys($userRoles)); - $subjects[] = WorkspaceRoleSubject::createForUser($user->getId()); + + if ($neosUser !== null) { + $subjects[] = WorkspaceRoleSubject::createForUser($neosUser->getId()); + } $userWorkspaceRole = $this->workspaceService->getMostPrivilegedWorkspaceRoleForSubjects($contentRepositoryId, $workspaceName, WorkspaceRoleSubjects::fromArray($subjects)); if ($userWorkspaceRole === null) { if ($userIsAdministrator) { - return WorkspacePermissions::manage("User '{$user->getLabel()}' (id: '{$user->getId()->value}') has no explicit role for workspace '{$workspaceName->value}' but is an Administrator"); + return WorkspacePermissions::manage(sprintf('Account "%s" is a Neos Administrator without explicit role for workspace "%s"', $account->getAccountIdentifier(), $workspaceName->value)); } - return WorkspacePermissions::none("User '{$user->getLabel()}' (id: '{$user->getId()->value}') has no explicit role for workspace '{$workspaceName->value}' and is no Administrator"); + return WorkspacePermissions::none(sprintf('Account "%s" is no Neos Administrator and has no explicit role for workspace "%s"', $account->getAccountIdentifier(), $workspaceName->value)); } return WorkspacePermissions::create( read: $userWorkspaceRole->isAtLeast(WorkspaceRole::VIEWER), write: $userWorkspaceRole->isAtLeast(WorkspaceRole::COLLABORATOR), manage: $userIsAdministrator || $userWorkspaceRole->isAtLeast(WorkspaceRole::MANAGER), - reason: "User '{$user->getLabel()}' (id: '{$user->getId()->value}') has role '{$userWorkspaceRole->value}' for workspace '{$workspaceName->value}'" . ($userIsAdministrator ? ' and is an Administrator' : ' and is no Administrator'), + reason: sprintf('Account "%s" is %s Neos Administrator and has role "%s" for workspace "%s"', $account->getAccountIdentifier(), $userIsAdministrator ? 'a' : 'no', $userWorkspaceRole->value, $workspaceName->value), ); } /** - * @param array $roles + * Determines the default {@see VisibilityConstraints} for an anonymous user (aka "public access") */ - public function getVisibilityConstraintsForRoles(ContentRepositoryId $contentRepositoryId, array $roles): VisibilityConstraints + public function getVisibilityConstraintsForAnonymousUser(ContentRepositoryId $contentRepositoryId): VisibilityConstraints { + $roles = [ + self::FLOW_ROLE_EVERYBODY => $this->policyService->getRole(self::FLOW_ROLE_EVERYBODY), + self::FLOW_ROLE_ANONYMOUS => $this->policyService->getRole(self::FLOW_ROLE_ANONYMOUS), + ]; $restrictedSubtreeTags = []; /** @var SubtreeTagPrivilege $privilege */ foreach ($this->policyService->getAllPrivilegesByType(SubtreeTagPrivilege::class) as $privilege) { @@ -100,16 +112,49 @@ public function getVisibilityConstraintsForRoles(ContentRepositoryId $contentRep return new VisibilityConstraints(SubtreeTags::fromArray($restrictedSubtreeTags)); } + /** + * Determines the default {@see VisibilityConstraints} for the specified account + */ + public function getVisibilityConstraintsForAccount(ContentRepositoryId $contentRepositoryId, Account $account): VisibilityConstraints + { + $roles = $this->expandAccountRoles($account); + $restrictedSubtreeTags = []; + /** @var SubtreeTagPrivilege $privilege */ + foreach ($this->policyService->getAllPrivilegesByType(SubtreeTagPrivilege::class) as $privilege) { + if (!$this->privilegeManager->isGrantedForRoles($roles, SubtreeTagPrivilege::class, new SubtreeTagPrivilegeSubject($privilege->getSubtreeTag(), $contentRepositoryId))) { + $restrictedSubtreeTags[] = $privilege->getSubtreeTag(); + } + } + return new VisibilityConstraints(SubtreeTags::fromArray($restrictedSubtreeTags)); + } + + // ------------------------------ + /** * @return array */ - private function rolesForUser(User $user): array + private function expandAccountRoles(Account $account): array { - try { - $userRoles = $this->userService->getAllRoles($user); - } catch (NoSuchRoleException $e) { - throw new \RuntimeException("Failed to determine roles for user '{$user->getLabel()}' (id: '{$user->getId()->value}'), check your package dependencies: {$e->getMessage()}", 1727084881, $e); + $roles = [ + self::FLOW_ROLE_EVERYBODY => $this->policyService->getRole(self::FLOW_ROLE_EVERYBODY), + self::FLOW_ROLE_AUTHENTICATED_USER => $this->policyService->getRole(self::FLOW_ROLE_AUTHENTICATED_USER), + ]; + foreach ($account->getRoles() as $currentRole) { + if (!array_key_exists($currentRole->getIdentifier(), $roles)) { + $roles[$currentRole->getIdentifier()] = $currentRole; + } + foreach ($currentRole->getAllParentRoles() as $currentParentRole) { + if (!array_key_exists($currentParentRole->getIdentifier(), $roles)) { + $roles[$currentParentRole->getIdentifier()] = $currentParentRole; + } + } } - return $userRoles; + return $roles; + } + + private function neosUserFromAccount(Account $account): ?User + { + $user = $this->partyService->getAssignedPartyOfAccount($account); + return $user instanceof User ? $user : null; } } diff --git a/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php index bd8b020c877..60f73360eb2 100644 --- a/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php +++ b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php @@ -56,7 +56,11 @@ public function getVisibilityConstraints(WorkspaceName $workspaceName): Visibili if ($this->securityContext->areAuthorizationChecksDisabled()) { return VisibilityConstraints::default(); } - return $this->authorizationService->getVisibilityConstraintsForRoles($this->contentRepositoryId, $this->securityContext->getRoles()); + $authenticatedAccount = $this->securityContext->getAccount(); + if ($authenticatedAccount) { + return $this->authorizationService->getVisibilityConstraintsForAccount($this->contentRepositoryId, $authenticatedAccount); + } + return $this->authorizationService->getVisibilityConstraintsForAnonymousUser($this->contentRepositoryId); } public function getReadNodesFromWorkspacePrivilege(WorkspaceName $workspaceName): Privilege @@ -64,11 +68,11 @@ public function getReadNodesFromWorkspacePrivilege(WorkspaceName $workspaceName) if ($this->securityContext->areAuthorizationChecksDisabled()) { return Privilege::granted('Authorization checks are disabled'); } - $user = $this->userService->getCurrentUser(); - if ($user === null) { + $authenticatedAccount = $this->securityContext->getAccount(); + if ($authenticatedAccount === null) { $workspacePermissions = $this->authorizationService->getWorkspacePermissionsForAnonymousUser($this->contentRepositoryId, $workspaceName); } else { - $workspacePermissions = $this->authorizationService->getWorkspacePermissionsForUser($this->contentRepositoryId, $workspaceName, $user); + $workspacePermissions = $this->authorizationService->getWorkspacePermissionsForAccount($this->contentRepositoryId, $workspaceName, $authenticatedAccount); } return $workspacePermissions->read ? Privilege::granted($workspacePermissions->getReason()) : Privilege::denied($workspacePermissions->getReason()); } @@ -124,10 +128,10 @@ private function requireWorkspacePermission(WorkspaceName $workspaceName, string private function getWorkspacePermissionsForCurrentUser(WorkspaceName $workspaceName): WorkspacePermissions { - $user = $this->userService->getCurrentUser(); - if ($user === null) { + $authenticatedAccount = $this->securityContext->getAccount(); + if ($authenticatedAccount === null) { return $this->authorizationService->getWorkspacePermissionsForAnonymousUser($this->contentRepositoryId, $workspaceName); } - return $this->authorizationService->getWorkspacePermissionsForUser($this->contentRepositoryId, $workspaceName, $user); + return $this->authorizationService->getWorkspacePermissionsForAccount($this->contentRepositoryId, $workspaceName, $authenticatedAccount); } } diff --git a/Neos.Workspace.Ui/Classes/Controller/WorkspaceController.php b/Neos.Workspace.Ui/Classes/Controller/WorkspaceController.php index ea63307c93a..e3a2276cc9a 100644 --- a/Neos.Workspace.Ui/Classes/Controller/WorkspaceController.php +++ b/Neos.Workspace.Ui/Classes/Controller/WorkspaceController.php @@ -113,9 +113,9 @@ class WorkspaceController extends AbstractModuleController */ public function indexAction(): void { - $currentUser = $this->userService->getCurrentUser(); - if ($currentUser === null) { - throw new \RuntimeException('No user authenticated', 1718308216); + $authenticatedAccount = $this->securityContext->getAccount(); + if ($authenticatedAccount === null) { + throw new AccessDeniedException('No user authenticated', 1718308216); } $contentRepositoryIds = $this->contentRepositoryRegistry->getContentRepositoryIds(); @@ -140,7 +140,7 @@ public function indexAction(): void $allWorkspaces = $contentRepository->findWorkspaces(); foreach ($allWorkspaces as $workspace) { $workspaceMetadata = $this->workspaceService->getWorkspaceMetadata($contentRepositoryId, $workspace->workspaceName); - $permissions = $this->contentRepositoryAuthorizationService->getWorkspacePermissionsForUser($contentRepositoryId, $workspace->workspaceName, $currentUser); + $permissions = $this->contentRepositoryAuthorizationService->getWorkspacePermissionsForAccount($contentRepositoryId, $workspace->workspaceName, $authenticatedAccount); if (!$permissions->read) { continue; } @@ -160,9 +160,9 @@ classification: $workspaceMetadata->classification->name, public function showAction(WorkspaceName $workspace): void { - $currentUser = $this->userService->getCurrentUser(); - if ($currentUser === null) { - throw new \RuntimeException('No user authenticated', 1720371024); + $authenticatedAccount = $this->securityContext->getAccount(); + if ($authenticatedAccount === null) { + throw new AccessDeniedException('No user authenticated', 1720371024); } $contentRepositoryId = SiteDetectionResult::fromRequest($this->request->getHttpRequest())->contentRepositoryId; $contentRepository = $this->contentRepositoryRegistry->get($contentRepositoryId); @@ -179,7 +179,7 @@ public function showAction(WorkspaceName $workspace): void $baseWorkspace = $contentRepository->findWorkspaceByName($workspaceObj->baseWorkspaceName); assert($baseWorkspace !== null); $baseWorkspaceMetadata = $this->workspaceService->getWorkspaceMetadata($contentRepositoryId, $baseWorkspace->workspaceName); - $baseWorkspacePermissions = $this->contentRepositoryAuthorizationService->getWorkspacePermissionsForUser($contentRepositoryId, $baseWorkspace->workspaceName, $currentUser); + $baseWorkspacePermissions = $this->contentRepositoryAuthorizationService->getWorkspacePermissionsForAccount($contentRepositoryId, $baseWorkspace->workspaceName, $authenticatedAccount); } $this->view->assignMultiple([ 'selectedWorkspace' => $workspaceObj, @@ -208,7 +208,7 @@ public function createAction( ): void { $currentUser = $this->userService->getCurrentUser(); if ($currentUser === null) { - throw new \RuntimeException('No user authenticated', 1718303756); + throw new AccessDeniedException('No user authenticated', 1718303756); } $workspaceName = $this->workspaceService->getUniqueWorkspaceName($contentRepositoryId, $title->value); try { @@ -289,11 +289,11 @@ public function updateAction( $contentRepositoryId = SiteDetectionResult::fromRequest($this->request->getHttpRequest())->contentRepositoryId; $contentRepository = $this->contentRepositoryRegistry->get($contentRepositoryId); - $user = $this->userService->getCurrentUser(); - if ($user === null) { + $authenticatedAccount = $this->securityContext->getAccount(); + if ($authenticatedAccount === null) { throw new AccessDeniedException('No user is authenticated', 1729620262); } - $workspacePermissions = $this->contentRepositoryAuthorizationService->getWorkspacePermissionsForUser($contentRepository->id, $workspaceName, $user); + $workspacePermissions = $this->contentRepositoryAuthorizationService->getWorkspacePermissionsForAccount($contentRepository->id, $workspaceName, $authenticatedAccount); if (!$workspacePermissions->manage) { throw new AccessDeniedException(sprintf('The authenticated user does not have manage permissions for workspace "%s"', $workspaceName->value), 1729620297); } @@ -1023,7 +1023,7 @@ protected function prepareBaseWorkspaceOptions( ContentRepository $contentRepository, WorkspaceName $excludedWorkspace = null, ): array { - $user = $this->userService->getCurrentUser(); + $authenticatedAccount = $this->securityContext->getAccount(); $baseWorkspaceOptions = []; $workspaces = $contentRepository->findWorkspaces(); foreach ($workspaces as $workspace) { @@ -1039,10 +1039,11 @@ protected function prepareBaseWorkspaceOptions( if (!in_array($workspaceMetadata->classification, [WorkspaceClassification::SHARED, WorkspaceClassification::ROOT], true)) { continue; } - if ($user === null) { - continue; + if ($authenticatedAccount !== null) { + $permissions = $this->contentRepositoryAuthorizationService->getWorkspacePermissionsForAccount($contentRepository->id, $workspace->workspaceName, $authenticatedAccount); + } else { + $permissions = $this->contentRepositoryAuthorizationService->getWorkspacePermissionsForAnonymousUser($contentRepository->id, $workspace->workspaceName); } - $permissions = $this->contentRepositoryAuthorizationService->getWorkspacePermissionsForUser($contentRepository->id, $workspace->workspaceName, $user); if (!$permissions->manage) { continue; } From 918a052176899ec8ea3e3f621f41c1e1b28c04c1 Mon Sep 17 00:00:00 2001 From: Martin Ficzel Date: Wed, 23 Oct 2024 12:28:50 +0200 Subject: [PATCH 026/214] Add site:export command again --- .../Classes/Command/SiteCommandController.php | 65 ++++++++++++- .../Domain/Export/SiteExportProcessor.php | 67 +++++++++++++ .../Domain/Import/SiteCreationProcessor.php | 18 +++- .../Domain/Service/SiteExportService.php | 94 +++++++++++++++++++ 4 files changed, 239 insertions(+), 5 deletions(-) create mode 100644 Neos.Neos/Classes/Domain/Export/SiteExportProcessor.php create mode 100644 Neos.Neos/Classes/Domain/Service/SiteExportService.php diff --git a/Neos.Neos/Classes/Command/SiteCommandController.php b/Neos.Neos/Classes/Command/SiteCommandController.php index 713cc2f4da6..ba2dc2602cc 100644 --- a/Neos.Neos/Classes/Command/SiteCommandController.php +++ b/Neos.Neos/Classes/Command/SiteCommandController.php @@ -31,6 +31,7 @@ use Neos\Neos\Domain\Model\Site; use Neos\Neos\Domain\Repository\SiteRepository; use Neos\Neos\Domain\Service\NodeTypeNameFactory; +use Neos\Neos\Domain\Service\SiteExportService; use Neos\Neos\Domain\Service\SiteImportService; use Neos\Neos\Domain\Service\SiteImportServiceFactory; use Neos\Neos\Domain\Service\SiteService; @@ -79,6 +80,12 @@ class SiteCommandController extends CommandController */ protected $siteImportService; + /** + * @Flow\Inject + * @var SiteExportService + */ + protected $siteExportService; + /** * Create a new site * @@ -131,11 +138,13 @@ public function createCommand($name, $packageKey, $nodeType, $nodeName = null, $ } /** - * Import sites content + * Import sites * - * This command allows for importing one or more sites or partial content from the file system. The format must + * This command allows importing sites from the given path/packahe. The format must * be identical to that produced by the export command. * + * !!! At the moment the live workspace has to be empty prior to importing. This will be improved in future. !!! + * * If a path is specified, this command expects the corresponding directory to contain the exported files * * If a package key is specified, this command expects the export files to be located in the private resources @@ -190,6 +199,58 @@ public function importCommand(string $packageKey = null, string $path = null, st $this->siteImportService->importFromPath($contentRepositoryId, $path, $onProcessor, $onMessage); } + /** + * Export sites + * + * This command allows to export all current sites. + * + * !!! At the moment always all sites are exported. This will be improved in future!!! + * + * If a path is specified, this command expects the corresponding directory to contain the exported files + * + * If a package key is specified, this command expects the export files to be located in the private resources + * directory of the given package (Resources/Private/Content). + * + * @param string|null $packageKey Package key specifying the package containing the sites content + * @param string|null $path relative or absolute path and filename to the export files + * @return void + */ + public function exportCommand(string $packageKey = null, string $path = null, string $contentRepository = 'default', bool $verbose = false): void + { + $exceedingArguments = $this->request->getExceedingArguments(); + if (isset($exceedingArguments[0]) && $packageKey === null && $path === null) { + if (file_exists($exceedingArguments[0])) { + $path = $exceedingArguments[0]; + } elseif ($this->packageManager->isPackageAvailable($exceedingArguments[0])) { + $packageKey = $exceedingArguments[0]; + } + } + if ($packageKey === null && $path === null) { + $this->outputLine('You have to specify either --package-key or --filename'); + $this->quit(1); + } + + $contentRepositoryId = ContentRepositoryId::fromString($contentRepository); + $onProcessor = function (string $processorLabel) { + $this->outputLine('%s...', [$processorLabel]); + }; + $onMessage = function (Severity $severity, string $message) use ($verbose) { + if (!$verbose && $severity === Severity::NOTICE) { + return; + } + $this->outputLine(match ($severity) { + Severity::NOTICE => $message, + Severity::WARNING => sprintf('Warning: %s', $message), + Severity::ERROR => sprintf('Error: %s', $message), + }); + }; + if ($path === null) { + $package = $this->packageManager->getPackage($packageKey); + $path = Files::concatenatePaths([$package->getPackagePath(), 'Resources/Private/Content']); + } + $this->siteExportService->exportToPath($contentRepositoryId, $path, $onProcessor, $onMessage); + } + /** * Remove site with content and related data (with globbing) * diff --git a/Neos.Neos/Classes/Domain/Export/SiteExportProcessor.php b/Neos.Neos/Classes/Domain/Export/SiteExportProcessor.php new file mode 100644 index 00000000000..c79bd416604 --- /dev/null +++ b/Neos.Neos/Classes/Domain/Export/SiteExportProcessor.php @@ -0,0 +1,67 @@ + [ + "name" => $site->getName(), + "nodeName" => $site->getNodeName()->value, + "siteResourcesPackageKey" => $site->getSiteResourcesPackageKey(), + "online" => $site->isOnline(), + "domains" => array_map( + fn(Domain $domain) => [ + 'hostname' => $domain->getHostname(), + 'scheme' => $domain->getScheme(), + 'port' => $domain->getPort(), + 'active' => $domain->getActive(), + 'primary' => $domain === $site->getPrimaryDomain(), + ], + $site->getDomains()->toArray() + ) + ], + $this->siteRepository->findAll()->toArray() + ); + + $context->files->write( + 'sites.json', + json_encode($sites, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) + ); + } +} diff --git a/Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php b/Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php index 401a8fd4810..f783f3ab9e9 100644 --- a/Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php +++ b/Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php @@ -21,6 +21,7 @@ use Neos\ContentRepository\Export\ProcessorInterface; use Neos\ContentRepository\Export\Severity; use Neos\Flow\Persistence\Doctrine\PersistenceManager; +use Neos\Neos\Domain\Model\Domain; use Neos\Neos\Domain\Model\Site; use Neos\Neos\Domain\Repository\SiteRepository; @@ -61,13 +62,24 @@ public function run(ProcessingContext $context): void } // TODO use node aggregate identifier instead of node name $siteInstance = new Site($siteNodeName->value); - $siteInstance->setSiteResourcesPackageKey($site['packageKey']); + $siteInstance->setSiteResourcesPackageKey($site['siteResourcesPackageKey']); $siteInstance->setState(($site['inactive'] ?? false) ? Site::STATE_OFFLINE : Site::STATE_ONLINE); $siteInstance->setName($site['name']); + + foreach ($site['domains'] ?? [] as $domain) { + $domainInstance = new Domain(); + $domainInstance->setSite($siteInstance); + $domainInstance->setHostname($domain['hostname']); + $domainInstance->setPort($domain['port']); + $domainInstance->setScheme($domain['scheme']); + $domainInstance->setActive($domain['active'] ?? false); + if ($domain['primary']) { + $siteInstance->setPrimaryDomain($domainInstance); + } + } + $this->siteRepository->add($siteInstance); $persistAllIsRequired = true; - - // TODO add domains? } if ($persistAllIsRequired) { $this->persistenceManager->persistAll(); diff --git a/Neos.Neos/Classes/Domain/Service/SiteExportService.php b/Neos.Neos/Classes/Domain/Service/SiteExportService.php new file mode 100644 index 00000000000..97b7a1bf064 --- /dev/null +++ b/Neos.Neos/Classes/Domain/Service/SiteExportService.php @@ -0,0 +1,94 @@ +contentRepositoryRegistry->get($contentRepositoryId); + + $liveWorkspace = $contentRepository->findWorkspaceByName(WorkspaceName::forLive()); + if ($liveWorkspace === null) { + throw new \RuntimeException('Failed to find live workspace', 1716652280); + } + + // TODO make configurable (?) + /** @var array $processors */ + $processors = [ + 'Exporting events' => $this->contentRepositoryRegistry->buildService( + $contentRepositoryId, + new EventExportProcessorFactory( + $liveWorkspace->currentContentStreamId + ) + ), + 'Exporting assets' => new AssetExportProcessor( + $contentRepositoryId, + $this->assetRepository, + $liveWorkspace, + $this->assetUsageService + ), + 'Export sites' => new SiteExportProcessor($this->siteRepository), + ]; + foreach ($processors as $processorLabel => $processor) { + ($onProcessor)($processorLabel); + $processor->run($context); + } + } +} From a37387f5bff85c7ed9ff1c6e576e3fad9c249880 Mon Sep 17 00:00:00 2001 From: Martin Ficzel Date: Wed, 23 Oct 2024 12:29:12 +0200 Subject: [PATCH 027/214] Avoid exporting of contentStreamId --- .../src/Event/ValueObject/ExportedEvent.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Neos.ContentRepository.Export/src/Event/ValueObject/ExportedEvent.php b/Neos.ContentRepository.Export/src/Event/ValueObject/ExportedEvent.php index 8c50097a96e..5392aa14ac6 100644 --- a/Neos.ContentRepository.Export/src/Event/ValueObject/ExportedEvent.php +++ b/Neos.ContentRepository.Export/src/Event/ValueObject/ExportedEvent.php @@ -24,10 +24,13 @@ public function __construct( public static function fromRawEvent(Event $event): self { + $payload = \json_decode($event->data->value, true, 512, JSON_THROW_ON_ERROR); + // unset content stream id as this is overwritten during import + unset($payload['contentStreamId']); return new self( $event->id->value, $event->type->value, - \json_decode($event->data->value, true, 512, JSON_THROW_ON_ERROR), + $payload, $event->metadata?->value ?? [], ); } From 65a0ab5bd6f5722cf22a107e0cfce770142a039f Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Wed, 23 Oct 2024 12:45:35 +0200 Subject: [PATCH 028/214] Adjust behat tests to account based ACL --- .../Features/Bootstrap/WorkspaceServiceTrait.php | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/WorkspaceServiceTrait.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/WorkspaceServiceTrait.php index 8106b291e13..416b6d4a8d3 100644 --- a/Neos.Neos/Tests/Behavior/Features/Bootstrap/WorkspaceServiceTrait.php +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/WorkspaceServiceTrait.php @@ -213,10 +213,13 @@ public function theWorkspaceShouldHaveTheFollowingRoleAssignments($workspaceName public function theNeosUserShouldHaveThePermissionsForWorkspace(string $username, string $expectedPermissions, string $workspaceName): void { $user = $this->getObject(UserService::class)->getUser($username); - $permissions = $this->getObject(ContentRepositoryAuthorizationService::class)->getWorkspacePermissionsForUser( + Assert::assertNotNull($user); + $account = $user->getAccounts()->first(); + Assert::assertNotNull($account); + $permissions = $this->getObject(ContentRepositoryAuthorizationService::class)->getWorkspacePermissionsForAccount( $this->currentContentRepository->id, WorkspaceName::fromString($workspaceName), - $user, + $account, ); Assert::assertSame($expectedPermissions, implode(',', array_keys(array_filter(get_object_vars($permissions))))); } @@ -227,10 +230,13 @@ public function theNeosUserShouldHaveThePermissionsForWorkspace(string $username public function theNeosUserShouldHaveNoPermissionsForWorkspace(string $username, string $workspaceName): void { $user = $this->getObject(UserService::class)->getUser($username); - $permissions = $this->getObject(ContentRepositoryAuthorizationService::class)->getWorkspacePermissionsForUser( + Assert::assertNotNull($user); + $account = $user->getAccounts()->first(); + Assert::assertNotNull($account); + $permissions = $this->getObject(ContentRepositoryAuthorizationService::class)->getWorkspacePermissionsForAccount( $this->currentContentRepository->id, WorkspaceName::fromString($workspaceName), - $user, + $account, ); Assert::assertFalse($permissions->read); Assert::assertFalse($permissions->write); From 4478619be5e3874306ab7ad7e7962104ceb19564 Mon Sep 17 00:00:00 2001 From: Martin Ficzel Date: Wed, 23 Oct 2024 13:27:58 +0200 Subject: [PATCH 029/214] TASK: Make linter happy --- .../Domain/Export/SiteExportProcessor.php | 23 +++++++++++++------ .../Import/LiveWorkspaceIsEmptyProcessor.php | 3 ++- .../LiveWorkspaceIsEmptyProcessorFactory.php | 4 ++++ .../Domain/Import/SiteCreationProcessor.php | 17 +++++++------- .../Domain/Service/SiteExportService.php | 5 ---- .../Domain/Service/SiteImportService.php | 2 +- 6 files changed, 32 insertions(+), 22 deletions(-) diff --git a/Neos.Neos/Classes/Domain/Export/SiteExportProcessor.php b/Neos.Neos/Classes/Domain/Export/SiteExportProcessor.php index c79bd416604..6fb159c1914 100644 --- a/Neos.Neos/Classes/Domain/Export/SiteExportProcessor.php +++ b/Neos.Neos/Classes/Domain/Export/SiteExportProcessor.php @@ -28,7 +28,9 @@ /** * Export processor exports Neos {@see Site} instances as json * - * @phpstan-type SiteShape array{name:string, packageKey:string, nodeName?: string, inactive?:bool} + * @phpstan-type DomainShape array{hostname: string, scheme?: ?string, port?: ?int, active?: ?bool, primary?: ?bool } + * @phpstan-type SiteShape array{name:string, siteResourcesPackageKey:string, nodeName?: string, online?:bool, domains?: ?DomainShape[] } + * */ final readonly class SiteExportProcessor implements ProcessorInterface { @@ -39,7 +41,19 @@ public function __construct( public function run(ProcessingContext $context): void { - $sites = array_map( + $sites = $this->getSiteData(); + $context->files->write( + 'sites.json', + json_encode($sites, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) + ); + } + + /** + * @return SiteShape[] + */ + private function getSiteData(): array + { + return array_map( fn(Site $site) => [ "name" => $site->getName(), "nodeName" => $site->getNodeName()->value, @@ -58,10 +72,5 @@ public function run(ProcessingContext $context): void ], $this->siteRepository->findAll()->toArray() ); - - $context->files->write( - 'sites.json', - json_encode($sites, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) - ); } } diff --git a/Neos.Neos/Classes/Domain/Import/LiveWorkspaceIsEmptyProcessor.php b/Neos.Neos/Classes/Domain/Import/LiveWorkspaceIsEmptyProcessor.php index 82f6a967c07..90c942279ff 100644 --- a/Neos.Neos/Classes/Domain/Import/LiveWorkspaceIsEmptyProcessor.php +++ b/Neos.Neos/Classes/Domain/Import/LiveWorkspaceIsEmptyProcessor.php @@ -37,12 +37,13 @@ public function run(ProcessingContext $context): void $context->dispatch(Severity::NOTICE, 'Ensures empty live workspace'); if ($this->workspaceHasEvents(WorkspaceName::forLive())) { - throw new \RuntimeException( 'Live workspace already contains events please run "cr:prune" before importing.'); + throw new \RuntimeException('Live workspace already contains events please run "cr:prune" before importing.'); } } private function workspaceHasEvents(WorkspaceName $workspaceName): bool { + /** @phpstan-ignore-next-line internal method of the cr is called */ $workspaceStreamName = WorkspaceEventStreamName::fromWorkspaceName($workspaceName)->getEventStreamName(); $eventStream = $this->eventStore->load($workspaceStreamName); foreach ($eventStream as $event) { diff --git a/Neos.Neos/Classes/Domain/Import/LiveWorkspaceIsEmptyProcessorFactory.php b/Neos.Neos/Classes/Domain/Import/LiveWorkspaceIsEmptyProcessorFactory.php index 792b0495cac..d74eaac3215 100644 --- a/Neos.Neos/Classes/Domain/Import/LiveWorkspaceIsEmptyProcessorFactory.php +++ b/Neos.Neos/Classes/Domain/Import/LiveWorkspaceIsEmptyProcessorFactory.php @@ -17,7 +17,11 @@ use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceFactoryDependencies; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceFactoryInterface; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface; +use Neos\ContentRepository\Export\Processors\EventExportProcessor; +/** + * @implements ContentRepositoryServiceFactoryInterface + */ final readonly class LiveWorkspaceIsEmptyProcessorFactory implements ContentRepositoryServiceFactoryInterface { public function build(ContentRepositoryServiceFactoryDependencies $serviceFactoryDependencies): ContentRepositoryServiceInterface diff --git a/Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php b/Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php index f783f3ab9e9..5095e2173d8 100644 --- a/Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php +++ b/Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php @@ -20,7 +20,7 @@ use Neos\ContentRepository\Export\ProcessingContext; use Neos\ContentRepository\Export\ProcessorInterface; use Neos\ContentRepository\Export\Severity; -use Neos\Flow\Persistence\Doctrine\PersistenceManager; +use Neos\Flow\Persistence\PersistenceManagerInterface; use Neos\Neos\Domain\Model\Domain; use Neos\Neos\Domain\Model\Site; use Neos\Neos\Domain\Repository\SiteRepository; @@ -28,13 +28,14 @@ /** * Import processor that creates and persists a Neos {@see Site} instance * - * @phpstan-type SiteShape array{name:string, packageKey:string, nodeName?: string, inactive?:bool} + * @phpstan-type DomainShape array{hostname: string, scheme?: ?string, port?: ?int, active?: ?bool, primary?: ?bool } + * @phpstan-type SiteShape array{name:string, siteResourcesPackageKey:string, nodeName?: string, online?:bool, domains?: ?DomainShape[] } */ final readonly class SiteCreationProcessor implements ProcessorInterface { public function __construct( private SiteRepository $siteRepository, - private PersistenceManager $persistenceManager + private PersistenceManagerInterface $persistenceManager ) { } @@ -63,17 +64,17 @@ public function run(ProcessingContext $context): void // TODO use node aggregate identifier instead of node name $siteInstance = new Site($siteNodeName->value); $siteInstance->setSiteResourcesPackageKey($site['siteResourcesPackageKey']); - $siteInstance->setState(($site['inactive'] ?? false) ? Site::STATE_OFFLINE : Site::STATE_ONLINE); + $siteInstance->setState(($site['online'] ?? false) ? Site::STATE_OFFLINE : Site::STATE_ONLINE); $siteInstance->setName($site['name']); foreach ($site['domains'] ?? [] as $domain) { $domainInstance = new Domain(); $domainInstance->setSite($siteInstance); $domainInstance->setHostname($domain['hostname']); - $domainInstance->setPort($domain['port']); - $domainInstance->setScheme($domain['scheme']); + $domainInstance->setPort($domain['port'] ?? null); + $domainInstance->setScheme($domain['scheme'] ?? null); $domainInstance->setActive($domain['active'] ?? false); - if ($domain['primary']) { + if ($domain['primary'] ?? false) { $siteInstance->setPrimaryDomain($domainInstance); } } @@ -102,7 +103,7 @@ private static function extractSitesFromEventStream(ProcessingContext $context): } if ($event->type === 'NodeAggregateWithNodeWasCreated' && in_array($event->payload['parentNodeAggregateId'], $rootNodeAggregateIds, true)) { $sites[] = [ - 'packageKey' => self::extractPackageKeyFromNodeTypeName($event->payload['nodeTypeName']), + 'siteResourcesPackageKey' => self::extractPackageKeyFromNodeTypeName($event->payload['nodeTypeName']), 'name' => $event->payload['initialPropertyValues']['title']['value'] ?? $event->payload['nodeTypeName'], 'nodeTypeName' => $event->payload['nodeTypeName'], 'nodeName' => $event->payload['nodeName'] ?? null, diff --git a/Neos.Neos/Classes/Domain/Service/SiteExportService.php b/Neos.Neos/Classes/Domain/Service/SiteExportService.php index 97b7a1bf064..7735a131e94 100644 --- a/Neos.Neos/Classes/Domain/Service/SiteExportService.php +++ b/Neos.Neos/Classes/Domain/Service/SiteExportService.php @@ -40,14 +40,9 @@ { public function __construct( private ContentRepositoryRegistry $contentRepositoryRegistry, - private DoctrineService $doctrineService, private SiteRepository $siteRepository, private AssetRepository $assetRepository, private AssetUsageService $assetUsageService, - private ResourceRepository $resourceRepository, - private ResourceManager $resourceManager, - private PersistenceManagerInterface $persistenceManager, - private WorkspaceService $workspaceService, ) { } diff --git a/Neos.Neos/Classes/Domain/Service/SiteImportService.php b/Neos.Neos/Classes/Domain/Service/SiteImportService.php index 50a1a044238..d3a35ac5e05 100644 --- a/Neos.Neos/Classes/Domain/Service/SiteImportService.php +++ b/Neos.Neos/Classes/Domain/Service/SiteImportService.php @@ -71,7 +71,7 @@ public function importFromPath(ContentRepositoryId $contentRepositoryId, string 'Run doctrine migrations' => new DoctrineMigrateProcessor($this->doctrineService), 'Setup content repository' => $this->contentRepositoryRegistry->buildService($contentRepositoryId, new ContentRepositorySetupProcessorFactory()), 'Verify Live workspace does not exist yet' => $this->contentRepositoryRegistry->buildService($contentRepositoryId, new LiveWorkspaceIsEmptyProcessorFactory()), - 'Create Neos sites' => new SiteCreationProcessor($this->siteRepository), + 'Create Neos sites' => new SiteCreationProcessor($this->siteRepository, $this->persistenceManager), 'Create Live workspace' => new LiveWorkspaceCreationProcessor($contentRepository, $this->workspaceService), 'Import events' => $this->contentRepositoryRegistry->buildService($contentRepositoryId, new EventStoreImportProcessorFactory(WorkspaceName::forLive(), keepEventIds: true)), 'Import assets' => new AssetRepositoryImportProcessor($this->assetRepository, $this->resourceRepository, $this->resourceManager, $this->persistenceManager), From 4a54aced2c19c0a08f192bab53eeb899257b909b Mon Sep 17 00:00:00 2001 From: Martin Ficzel Date: Wed, 23 Oct 2024 14:14:53 +0200 Subject: [PATCH 030/214] Add replayall to site:import process --- .../Classes/Service/ProjectionReplayService.php | 9 ++++++++- Neos.Neos/Classes/Domain/Service/SiteImportService.php | 4 ++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionReplayService.php b/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionReplayService.php index 00a946be01a..87753b37e58 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionReplayService.php +++ b/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionReplayService.php @@ -9,6 +9,8 @@ use Neos\ContentRepository\Core\Projection\ProjectionInterface; use Neos\ContentRepository\Core\Projection\Projections; use Neos\ContentRepository\Core\Projection\ProjectionStateInterface; +use Neos\ContentRepository\Export\ProcessingContext; +use Neos\ContentRepository\Export\ProcessorInterface; use Neos\EventStore\EventStoreInterface; use Neos\EventStore\Model\Event\SequenceNumber; use Neos\EventStore\Model\EventStream\VirtualStreamName; @@ -18,7 +20,7 @@ * * @internal this is currently only used by the {@see CrCommandController} */ -final class ProjectionReplayService implements ContentRepositoryServiceInterface +final class ProjectionReplayService implements ProcessorInterface, ContentRepositoryServiceInterface { public function __construct( @@ -28,6 +30,11 @@ public function __construct( ) { } + public function run(ProcessingContext $context): void + { + $this->replayAllProjections(CatchUpOptions::create()); + } + public function replayProjection(string $projectionAliasOrClassName, CatchUpOptions $options): void { $projectionClassName = $this->resolveProjectionClassName($projectionAliasOrClassName); diff --git a/Neos.Neos/Classes/Domain/Service/SiteImportService.php b/Neos.Neos/Classes/Domain/Service/SiteImportService.php index d3a35ac5e05..9eeddb4d148 100644 --- a/Neos.Neos/Classes/Domain/Service/SiteImportService.php +++ b/Neos.Neos/Classes/Domain/Service/SiteImportService.php @@ -25,6 +25,7 @@ use Neos\ContentRepository\Export\Processors\AssetRepositoryImportProcessor; use Neos\ContentRepository\Export\Severity; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; +use Neos\ContentRepositoryRegistry\Service\ProjectionReplayServiceFactory; use Neos\Flow\Annotations as Flow; use Neos\Flow\Persistence\Doctrine\Service as DoctrineService; use Neos\Flow\Persistence\PersistenceManagerInterface; @@ -49,6 +50,7 @@ public function __construct( private ResourceManager $resourceManager, private PersistenceManagerInterface $persistenceManager, private WorkspaceService $workspaceService, + private ProjectionReplayServiceFactory $projectionReplayServiceFactory, ) { } @@ -75,7 +77,9 @@ public function importFromPath(ContentRepositoryId $contentRepositoryId, string 'Create Live workspace' => new LiveWorkspaceCreationProcessor($contentRepository, $this->workspaceService), 'Import events' => $this->contentRepositoryRegistry->buildService($contentRepositoryId, new EventStoreImportProcessorFactory(WorkspaceName::forLive(), keepEventIds: true)), 'Import assets' => new AssetRepositoryImportProcessor($this->assetRepository, $this->resourceRepository, $this->resourceManager, $this->persistenceManager), + 'Replay all projections' => $this->contentRepositoryRegistry->buildService($contentRepositoryId, $this->projectionReplayServiceFactory), ]; + foreach ($processors as $processorLabel => $processor) { ($onProcessor)($processorLabel); $processor->run($context); From 9f68937cf3fca6c0df52bc7aadbbe0ef859ac05a Mon Sep 17 00:00:00 2001 From: Martin Ficzel Date: Wed, 23 Oct 2024 16:39:32 +0200 Subject: [PATCH 031/214] Rename command `site:import` and `site:export` to `site:importAll` and `site:exportAll` to be in line with current behavior --- .../Classes/Command/SiteCommandController.php | 23 +++++---- .../Domain/Export/SiteExportProcessor.php | 51 +++++++++++++++---- .../Export/SiteExportProcessorFactory.php | 43 ++++++++++++++++ .../Domain/Service/SiteExportService.php | 16 +++--- 4 files changed, 104 insertions(+), 29 deletions(-) create mode 100644 Neos.Neos/Classes/Domain/Export/SiteExportProcessorFactory.php diff --git a/Neos.Neos/Classes/Command/SiteCommandController.php b/Neos.Neos/Classes/Command/SiteCommandController.php index ba2dc2602cc..7ed76cf0103 100644 --- a/Neos.Neos/Classes/Command/SiteCommandController.php +++ b/Neos.Neos/Classes/Command/SiteCommandController.php @@ -140,10 +140,10 @@ public function createCommand($name, $packageKey, $nodeType, $nodeName = null, $ /** * Import sites * - * This command allows importing sites from the given path/packahe. The format must - * be identical to that produced by the export command. + * This command allows importing sites from the given path/package. The format must + * be identical to that produced by the exportAll command. * - * !!! At the moment the live workspace has to be empty prior to importing. This will be improved in future. !!! + * !!! The live workspace has to be empty prior to importing. !!! * * If a path is specified, this command expects the corresponding directory to contain the exported files * @@ -154,7 +154,7 @@ public function createCommand($name, $packageKey, $nodeType, $nodeName = null, $ * @param string|null $path relative or absolute path and filename to the export files * @return void */ - public function importCommand(string $packageKey = null, string $path = null, string $contentRepository = 'default', bool $verbose = false): void + public function importAllCommand(string $packageKey = null, string $path = null, string $contentRepository = 'default', bool $verbose = false): void { $exceedingArguments = $this->request->getExceedingArguments(); if (isset($exceedingArguments[0]) && $packageKey === null && $path === null) { @@ -202,20 +202,18 @@ public function importCommand(string $packageKey = null, string $path = null, st /** * Export sites * - * This command allows to export all current sites. + * This command exports all sites of the content repository. + ** + * If a path is specified, this command creates the directory if needed and exports into that. * - * !!! At the moment always all sites are exported. This will be improved in future!!! - * - * If a path is specified, this command expects the corresponding directory to contain the exported files - * - * If a package key is specified, this command expects the export files to be located in the private resources + * If a package key is specified, this command exports to the private resources * directory of the given package (Resources/Private/Content). * * @param string|null $packageKey Package key specifying the package containing the sites content * @param string|null $path relative or absolute path and filename to the export files * @return void */ - public function exportCommand(string $packageKey = null, string $path = null, string $contentRepository = 'default', bool $verbose = false): void + public function exportAllCommand(string $packageKey = null, string $path = null, string $contentRepository = 'default', bool $verbose = false): void { $exceedingArguments = $this->request->getExceedingArguments(); if (isset($exceedingArguments[0]) && $packageKey === null && $path === null) { @@ -248,6 +246,9 @@ public function exportCommand(string $packageKey = null, string $path = null, st $package = $this->packageManager->getPackage($packageKey); $path = Files::concatenatePaths([$package->getPackagePath(), 'Resources/Private/Content']); } + if (file_exists($path) === false) { + Files::createDirectoryRecursively($path); + } $this->siteExportService->exportToPath($contentRepositoryId, $path, $onProcessor, $onMessage); } diff --git a/Neos.Neos/Classes/Domain/Export/SiteExportProcessor.php b/Neos.Neos/Classes/Domain/Export/SiteExportProcessor.php index 6fb159c1914..6f7ac14f0a4 100644 --- a/Neos.Neos/Classes/Domain/Export/SiteExportProcessor.php +++ b/Neos.Neos/Classes/Domain/Export/SiteExportProcessor.php @@ -14,16 +14,15 @@ namespace Neos\Neos\Domain\Export; -use JsonException; -use Neos\ContentRepository\Core\SharedModel\Node\NodeName; -use Neos\ContentRepository\Export\Event\ValueObject\ExportedEvent; +use Neos\ContentRepository\Core\ContentRepository; +use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface; +use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepository\Export\ProcessingContext; use Neos\ContentRepository\Export\ProcessorInterface; -use Neos\ContentRepository\Export\Severity; -use Neos\Flow\Persistence\Doctrine\PersistenceManager; use Neos\Neos\Domain\Model\Domain; use Neos\Neos\Domain\Model\Site; use Neos\Neos\Domain\Repository\SiteRepository; +use Neos\Neos\Domain\Service\NodeTypeNameFactory; /** * Export processor exports Neos {@see Site} instances as json @@ -32,9 +31,11 @@ * @phpstan-type SiteShape array{name:string, siteResourcesPackageKey:string, nodeName?: string, online?:bool, domains?: ?DomainShape[] } * */ -final readonly class SiteExportProcessor implements ProcessorInterface +final readonly class SiteExportProcessor implements ProcessorInterface, ContentRepositoryServiceInterface { public function __construct( + private ContentRepository $contentRepository, + private WorkspaceName $workspaceName, private SiteRepository $siteRepository, ) { } @@ -53,8 +54,9 @@ public function run(ProcessingContext $context): void */ private function getSiteData(): array { - return array_map( - fn(Site $site) => [ + $siteData = []; + foreach ($this->findSites($this->workspaceName) as $site) { + $siteData[] = [ "name" => $site->getName(), "nodeName" => $site->getNodeName()->value, "siteResourcesPackageKey" => $site->getSiteResourcesPackageKey(), @@ -69,8 +71,35 @@ private function getSiteData(): array ], $site->getDomains()->toArray() ) - ], - $this->siteRepository->findAll()->toArray() - ); + ]; + } + + return $siteData; + } + + /** + * @param WorkspaceName $workspaceName + * @return \Traversable + */ + private function findSites(WorkspaceName $workspaceName): \Traversable + { + $contentGraph = $this->contentRepository->getContentGraph($workspaceName); + $sitesNodeAggregate = $contentGraph->findRootNodeAggregateByType(NodeTypeNameFactory::forSites()); + if ($sitesNodeAggregate === null) { + return; + } + + $siteNodeAggregates = $contentGraph->findChildNodeAggregates($sitesNodeAggregate->nodeAggregateId); + foreach ($siteNodeAggregates as $siteNodeAggregate) { + $siteNodeName = $siteNodeAggregate->nodeName?->value; + if ($siteNodeName === null) { + continue; + } + $site = $this->siteRepository->findOneByNodeName($siteNodeName); + if ($site === null) { + continue; + } + yield $site; + } } } diff --git a/Neos.Neos/Classes/Domain/Export/SiteExportProcessorFactory.php b/Neos.Neos/Classes/Domain/Export/SiteExportProcessorFactory.php new file mode 100644 index 00000000000..07832016e15 --- /dev/null +++ b/Neos.Neos/Classes/Domain/Export/SiteExportProcessorFactory.php @@ -0,0 +1,43 @@ + + */ +final readonly class SiteExportProcessorFactory implements ContentRepositoryServiceFactoryInterface +{ + public function __construct( + private WorkspaceName $workspaceName, + private SiteRepository $siteRepository, + ) { + } + + public function build(ContentRepositoryServiceFactoryDependencies $serviceFactoryDependencies): ContentRepositoryServiceInterface + { + return new SiteExportProcessor( + $serviceFactoryDependencies->contentRepository, + $this->workspaceName, + $this->siteRepository, + ); + } +} diff --git a/Neos.Neos/Classes/Domain/Service/SiteExportService.php b/Neos.Neos/Classes/Domain/Service/SiteExportService.php index 7735a131e94..f5b2e21bf21 100644 --- a/Neos.Neos/Classes/Domain/Service/SiteExportService.php +++ b/Neos.Neos/Classes/Domain/Service/SiteExportService.php @@ -25,14 +25,10 @@ use Neos\ContentRepository\Export\Severity; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Annotations as Flow; -use Neos\Flow\Persistence\Doctrine\Service as DoctrineService; -use Neos\Flow\Persistence\PersistenceManagerInterface; -use Neos\Flow\ResourceManagement\ResourceManager; -use Neos\Flow\ResourceManagement\ResourceRepository; use Neos\Media\Domain\Repository\AssetRepository; use Neos\Neos\AssetUsage\AssetUsageService; -use Neos\Neos\Domain\Import\SiteCreationProcessor; use Neos\Neos\Domain\Export\SiteExportProcessor; +use Neos\Neos\Domain\Export\SiteExportProcessorFactory; use Neos\Neos\Domain\Repository\SiteRepository; #[Flow\Scope('singleton')] @@ -40,9 +36,9 @@ { public function __construct( private ContentRepositoryRegistry $contentRepositoryRegistry, - private SiteRepository $siteRepository, private AssetRepository $assetRepository, private AssetUsageService $assetUsageService, + private SiteRepository $siteRepository, ) { } @@ -79,7 +75,13 @@ public function exportToPath(ContentRepositoryId $contentRepositoryId, string $p $liveWorkspace, $this->assetUsageService ), - 'Export sites' => new SiteExportProcessor($this->siteRepository), + 'Export sites' => $this->contentRepositoryRegistry->buildService( + $contentRepositoryId, + new SiteExportProcessorFactory( + $liveWorkspace->workspaceName, + $this->siteRepository, + ) + ), ]; foreach ($processors as $processorLabel => $processor) { ($onProcessor)($processorLabel); From cd550d74bc539343ff33b5381a0e166290367eba Mon Sep 17 00:00:00 2001 From: Martin Ficzel Date: Wed, 23 Oct 2024 17:46:19 +0200 Subject: [PATCH 032/214] Add `site:pruneAll` command that empties the current cr and removes all referenced site records --- .../Classes/Command/CrCommandController.php | 69 ---------- .../Classes/Command/SiteCommandController.php | 118 +++++++++++++++--- .../Domain/Export/SiteExportProcessor.php | 13 +- .../Domain/Import/SiteCreationProcessor.php | 16 +-- .../Domain/Service/SiteExportService.php | 1 - .../Domain/Service/SiteImportService.php | 4 +- 6 files changed, 117 insertions(+), 104 deletions(-) delete mode 100644 Neos.Neos/Classes/Command/CrCommandController.php diff --git a/Neos.Neos/Classes/Command/CrCommandController.php b/Neos.Neos/Classes/Command/CrCommandController.php deleted file mode 100644 index 05bf03e9179..00000000000 --- a/Neos.Neos/Classes/Command/CrCommandController.php +++ /dev/null @@ -1,69 +0,0 @@ -output->askConfirmation(sprintf('> This will prune your content repository "%s". Are you sure to proceed? (y/n) ', $contentRepository), false)) { - $this->outputLine('Abort.'); - return; - } - - $contentRepositoryId = ContentRepositoryId::fromString($contentRepository); - - $contentStreamPruner = $this->contentRepositoryRegistry->buildService( - $contentRepositoryId, - new ContentStreamPrunerFactory() - ); - - $workspaceMaintenanceService = $this->contentRepositoryRegistry->buildService( - $contentRepositoryId, - new WorkspaceMaintenanceServiceFactory() - ); - - $projectionService = $this->contentRepositoryRegistry->buildService( - $contentRepositoryId, - $this->projectionServiceFactory - ); - - // remove the workspace metadata and roles for this cr - $this->workspaceService->pruneRoleAsssignments($contentRepositoryId); - $this->workspaceService->pruneWorkspaceMetadata($contentRepositoryId); - - // reset the events table - $contentStreamPruner->pruneAll(); - $workspaceMaintenanceService->pruneAll(); - - // reset the projections state - $projectionService->resetAllProjections(); - - $this->outputLine('Done.'); - } -} diff --git a/Neos.Neos/Classes/Command/SiteCommandController.php b/Neos.Neos/Classes/Command/SiteCommandController.php index 7ed76cf0103..7450e3fcd84 100644 --- a/Neos.Neos/Classes/Command/SiteCommandController.php +++ b/Neos.Neos/Classes/Command/SiteCommandController.php @@ -14,27 +14,31 @@ namespace Neos\Neos\Command; +use Neos\ContentRepository\Core\ContentRepository; +use Neos\ContentRepository\Core\Service\ContentStreamPrunerFactory; +use Neos\ContentRepository\Core\Service\WorkspaceMaintenanceServiceFactory; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\Exception\NodeNameIsAlreadyCovered; use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeNotFound; -use Neos\ContentRepository\Export\ProcessorEventInterface; +use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepository\Export\Severity; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; +use Neos\ContentRepositoryRegistry\Service\ProjectionReplayServiceFactory; use Neos\Flow\Annotations as Flow; use Neos\Flow\Cli\CommandController; use Neos\Flow\Cli\Exception\StopCommandException; -use Neos\Flow\ObjectManagement\DependencyInjection\DependencyProxy; use Neos\Flow\Package\PackageManager; use Neos\Flow\Persistence\PersistenceManagerInterface; use Neos\Neos\Domain\Exception\SiteNodeNameIsAlreadyInUseByAnotherSite; use Neos\Neos\Domain\Exception\SiteNodeTypeIsInvalid; use Neos\Neos\Domain\Model\Site; +use Neos\Neos\Domain\Repository\DomainRepository; use Neos\Neos\Domain\Repository\SiteRepository; use Neos\Neos\Domain\Service\NodeTypeNameFactory; use Neos\Neos\Domain\Service\SiteExportService; use Neos\Neos\Domain\Service\SiteImportService; -use Neos\Neos\Domain\Service\SiteImportServiceFactory; use Neos\Neos\Domain\Service\SiteService; +use Neos\Neos\Domain\Service\WorkspaceService; use Neos\Utility\Files; /** @@ -50,6 +54,12 @@ class SiteCommandController extends CommandController */ protected $siteRepository; + /** + * @Flow\Inject + * @var DomainRepository + */ + protected $domainRepository; + /** * @Flow\Inject * @var SiteService @@ -86,6 +96,18 @@ class SiteCommandController extends CommandController */ protected $siteExportService; + /** + * @Flow\Inject + * @var WorkspaceService + */ + protected $workspaceService; + + /** + * @Flow\Inject(lazy=false) + * @var ProjectionReplayServiceFactory + */ + protected $projectionServiceFactory; + /** * Create a new site * @@ -253,31 +275,56 @@ public function exportAllCommand(string $packageKey = null, string $path = null, } /** - * Remove site with content and related data (with globbing) + * This will completely prune the data of the specified content repository and remove all site-records. * - * In the future we need some more sophisticated cleanup. - * - * @param string $siteNode Name for site root nodes to clear only content of this sites (globbing is supported) + * @param string $contentRepository Name of the content repository where the data should be pruned from. + * @param bool $force Prune the cr without confirmation. This cannot be reverted! * @return void */ - public function pruneCommand($siteNode) + public function pruneAllCommand(string $contentRepository = 'default', bool $force = false): void { - $sites = $this->findSitesByNodeNamePattern($siteNode); - if (empty($sites)) { - $this->outputLine('No Site found for pattern "%s".', [$siteNode]); - // Help the user a little about what he needs to provide as a parameter here - $this->outputLine('To find out which sites you have, use the site:list command.'); - $this->outputLine('The site:prune command expects the "Node name" from the site list as a parameter.'); - $this->outputLine('If you want to delete all sites, you can run site:prune \'*\'.'); - $this->quit(1); + if (!$force && !$this->output->askConfirmation(sprintf('> This will prune your content repository "%s". Are you sure to proceed? (y/n) ', $contentRepository), false)) { + $this->outputLine('Abort.'); + return; } + $contentRepositoryId = ContentRepositoryId::fromString($contentRepository); + + // find and remove all sites + $sites = $this->findAllSites( + $this->contentRepositoryRegistry->get($contentRepositoryId), + WorkspaceName::forLive() + ); foreach ($sites as $site) { $this->siteService->pruneSite($site); - $this->outputLine( - 'Site with root "%s" matched pattern "%s" and has been removed.', - [$site->getNodeName(), $siteNode] - ); } + + // remove cr data + $contentStreamPruner = $this->contentRepositoryRegistry->buildService( + $contentRepositoryId, + new ContentStreamPrunerFactory() + ); + $workspaceMaintenanceService = $this->contentRepositoryRegistry->buildService( + $contentRepositoryId, + new WorkspaceMaintenanceServiceFactory() + ); + + $projectionService = $this->contentRepositoryRegistry->buildService( + $contentRepositoryId, + $this->projectionServiceFactory + ); + + // remove the workspace metadata and roles for this cr + $this->workspaceService->pruneRoleAsssignments($contentRepositoryId); + $this->workspaceService->pruneWorkspaceMetadata($contentRepositoryId); + + // reset the events table + $contentStreamPruner->pruneAll(); + $workspaceMaintenanceService->pruneAll(); + + // reset the projections state + $projectionService->resetAllProjections(); + + $this->outputLine('Done.'); } /** @@ -362,4 +409,35 @@ function (Site $site) use ($siteNodePattern) { } ); } + + /** + * Find all sites in a cr by finding the children of the sites node + * + * @param ContentRepository $contentRepository + * @param WorkspaceName $workspaceName + * @return Site[] + */ + protected function findAllSites(ContentRepository $contentRepository, WorkspaceName $workspaceName): array + { + $contentGraph = $contentRepository->getContentGraph($workspaceName); + $sitesNodeAggregate = $contentGraph->findRootNodeAggregateByType(NodeTypeNameFactory::forSites()); + if ($sitesNodeAggregate === null) { + return []; + } + + $siteNodeAggregates = $contentGraph->findChildNodeAggregates($sitesNodeAggregate->nodeAggregateId); + $sites = []; + foreach ($siteNodeAggregates as $siteNodeAggregate) { + $siteNodeName = $siteNodeAggregate->nodeName?->value; + if ($siteNodeName === null) { + continue; + } + $site = $this->siteRepository->findOneByNodeName($siteNodeName); + if ($site === null) { + continue; + } + $sites[] = $site; + } + return $sites; + } } diff --git a/Neos.Neos/Classes/Domain/Export/SiteExportProcessor.php b/Neos.Neos/Classes/Domain/Export/SiteExportProcessor.php index 6f7ac14f0a4..643845d8b49 100644 --- a/Neos.Neos/Classes/Domain/Export/SiteExportProcessor.php +++ b/Neos.Neos/Classes/Domain/Export/SiteExportProcessor.php @@ -54,8 +54,9 @@ public function run(ProcessingContext $context): void */ private function getSiteData(): array { + $sites = $this->findSites($this->workspaceName); $siteData = []; - foreach ($this->findSites($this->workspaceName) as $site) { + foreach ($sites as $site) { $siteData[] = [ "name" => $site->getName(), "nodeName" => $site->getNodeName()->value, @@ -79,17 +80,18 @@ private function getSiteData(): array /** * @param WorkspaceName $workspaceName - * @return \Traversable + * @return Site[] */ - private function findSites(WorkspaceName $workspaceName): \Traversable + private function findSites(WorkspaceName $workspaceName): array { $contentGraph = $this->contentRepository->getContentGraph($workspaceName); $sitesNodeAggregate = $contentGraph->findRootNodeAggregateByType(NodeTypeNameFactory::forSites()); if ($sitesNodeAggregate === null) { - return; + return []; } $siteNodeAggregates = $contentGraph->findChildNodeAggregates($sitesNodeAggregate->nodeAggregateId); + $sites = []; foreach ($siteNodeAggregates as $siteNodeAggregate) { $siteNodeName = $siteNodeAggregate->nodeName?->value; if ($siteNodeName === null) { @@ -99,7 +101,8 @@ private function findSites(WorkspaceName $workspaceName): \Traversable if ($site === null) { continue; } - yield $site; + $sites[] = $site; } + return $sites; } } diff --git a/Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php b/Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php index 5095e2173d8..947011aea80 100644 --- a/Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php +++ b/Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php @@ -23,6 +23,7 @@ use Neos\Flow\Persistence\PersistenceManagerInterface; use Neos\Neos\Domain\Model\Domain; use Neos\Neos\Domain\Model\Site; +use Neos\Neos\Domain\Repository\DomainRepository; use Neos\Neos\Domain\Repository\SiteRepository; /** @@ -35,6 +36,7 @@ { public function __construct( private SiteRepository $siteRepository, + private DomainRepository $domainRepository, private PersistenceManagerInterface $persistenceManager ) { } @@ -51,7 +53,7 @@ public function run(ProcessingContext $context): void } else { $sites = self::extractSitesFromEventStream($context); } - $persistAllIsRequired = false; + /** @var SiteShape $site */ foreach ($sites as $site) { $context->dispatch(Severity::NOTICE, "Creating site \"{$site['name']}\""); @@ -66,7 +68,8 @@ public function run(ProcessingContext $context): void $siteInstance->setSiteResourcesPackageKey($site['siteResourcesPackageKey']); $siteInstance->setState(($site['online'] ?? false) ? Site::STATE_OFFLINE : Site::STATE_ONLINE); $siteInstance->setName($site['name']); - + $this->siteRepository->add($siteInstance); + $this->persistenceManager->persistAll(); foreach ($site['domains'] ?? [] as $domain) { $domainInstance = new Domain(); $domainInstance->setSite($siteInstance); @@ -74,16 +77,13 @@ public function run(ProcessingContext $context): void $domainInstance->setPort($domain['port'] ?? null); $domainInstance->setScheme($domain['scheme'] ?? null); $domainInstance->setActive($domain['active'] ?? false); + $this->domainRepository->add($domainInstance); if ($domain['primary'] ?? false) { $siteInstance->setPrimaryDomain($domainInstance); + $this->siteRepository->update($siteInstance); } + $this->persistenceManager->persistAll(); } - - $this->siteRepository->add($siteInstance); - $persistAllIsRequired = true; - } - if ($persistAllIsRequired) { - $this->persistenceManager->persistAll(); } } diff --git a/Neos.Neos/Classes/Domain/Service/SiteExportService.php b/Neos.Neos/Classes/Domain/Service/SiteExportService.php index f5b2e21bf21..87c581edea9 100644 --- a/Neos.Neos/Classes/Domain/Service/SiteExportService.php +++ b/Neos.Neos/Classes/Domain/Service/SiteExportService.php @@ -27,7 +27,6 @@ use Neos\Flow\Annotations as Flow; use Neos\Media\Domain\Repository\AssetRepository; use Neos\Neos\AssetUsage\AssetUsageService; -use Neos\Neos\Domain\Export\SiteExportProcessor; use Neos\Neos\Domain\Export\SiteExportProcessorFactory; use Neos\Neos\Domain\Repository\SiteRepository; diff --git a/Neos.Neos/Classes/Domain/Service/SiteImportService.php b/Neos.Neos/Classes/Domain/Service/SiteImportService.php index 9eeddb4d148..addb86244b1 100644 --- a/Neos.Neos/Classes/Domain/Service/SiteImportService.php +++ b/Neos.Neos/Classes/Domain/Service/SiteImportService.php @@ -36,6 +36,7 @@ use Neos\Neos\Domain\Import\LiveWorkspaceCreationProcessor; use Neos\Neos\Domain\Import\LiveWorkspaceIsEmptyProcessorFactory; use Neos\Neos\Domain\Import\SiteCreationProcessor; +use Neos\Neos\Domain\Repository\DomainRepository; use Neos\Neos\Domain\Repository\SiteRepository; #[Flow\Scope('singleton')] @@ -45,6 +46,7 @@ public function __construct( private ContentRepositoryRegistry $contentRepositoryRegistry, private DoctrineService $doctrineService, private SiteRepository $siteRepository, + private DomainRepository $domainRepository, private AssetRepository $assetRepository, private ResourceRepository $resourceRepository, private ResourceManager $resourceManager, @@ -73,7 +75,7 @@ public function importFromPath(ContentRepositoryId $contentRepositoryId, string 'Run doctrine migrations' => new DoctrineMigrateProcessor($this->doctrineService), 'Setup content repository' => $this->contentRepositoryRegistry->buildService($contentRepositoryId, new ContentRepositorySetupProcessorFactory()), 'Verify Live workspace does not exist yet' => $this->contentRepositoryRegistry->buildService($contentRepositoryId, new LiveWorkspaceIsEmptyProcessorFactory()), - 'Create Neos sites' => new SiteCreationProcessor($this->siteRepository, $this->persistenceManager), + 'Create Neos sites' => new SiteCreationProcessor($this->siteRepository, $this->domainRepository, $this->persistenceManager), 'Create Live workspace' => new LiveWorkspaceCreationProcessor($contentRepository, $this->workspaceService), 'Import events' => $this->contentRepositoryRegistry->buildService($contentRepositoryId, new EventStoreImportProcessorFactory(WorkspaceName::forLive(), keepEventIds: true)), 'Import assets' => new AssetRepositoryImportProcessor($this->assetRepository, $this->resourceRepository, $this->resourceManager, $this->persistenceManager), From e260954dfebe85d675bba448e9178c2fc7563e04 Mon Sep 17 00:00:00 2001 From: Martin Ficzel Date: Wed, 23 Oct 2024 18:25:09 +0200 Subject: [PATCH 033/214] Improve after review - centralize pathDetermination and closure generationIn site command controller - use catch-up instead of replayall after import - remove workspace name from exported events --- .../src/Event/ValueObject/ExportedEvent.php | 2 +- .../Service/ProjectionCatchupService.php | 95 ++++++++++++++ .../ProjectionCatchupServiceFactory.php | 30 +++++ .../Classes/Command/SiteCommandController.php | 121 +++++++++--------- .../Import/LiveWorkspaceIsEmptyProcessor.php | 2 +- .../Domain/Service/SiteImportService.php | 6 +- 6 files changed, 190 insertions(+), 66 deletions(-) create mode 100644 Neos.ContentRepositoryRegistry/Classes/Service/ProjectionCatchupService.php create mode 100644 Neos.ContentRepositoryRegistry/Classes/Service/ProjectionCatchupServiceFactory.php diff --git a/Neos.ContentRepository.Export/src/Event/ValueObject/ExportedEvent.php b/Neos.ContentRepository.Export/src/Event/ValueObject/ExportedEvent.php index 5392aa14ac6..a9ca8474d3a 100644 --- a/Neos.ContentRepository.Export/src/Event/ValueObject/ExportedEvent.php +++ b/Neos.ContentRepository.Export/src/Event/ValueObject/ExportedEvent.php @@ -26,7 +26,7 @@ public static function fromRawEvent(Event $event): self { $payload = \json_decode($event->data->value, true, 512, JSON_THROW_ON_ERROR); // unset content stream id as this is overwritten during import - unset($payload['contentStreamId']); + unset($payload['contentStreamId'], $payload['workspaceName']); return new self( $event->id->value, $event->type->value, diff --git a/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionCatchupService.php b/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionCatchupService.php new file mode 100644 index 00000000000..7112ad85486 --- /dev/null +++ b/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionCatchupService.php @@ -0,0 +1,95 @@ +catchupAllProjections(CatchUpOptions::create()); + } + + public function catchupProjection(string $projectionAliasOrClassName, CatchUpOptions $options): void + { + $projectionClassName = $this->resolveProjectionClassName($projectionAliasOrClassName); + $this->contentRepository->catchUpProjection($projectionClassName, $options); + } + + public function catchupAllProjections(CatchUpOptions $options, ?\Closure $progressCallback = null): void + { + foreach ($this->projectionClassNamesAndAliases() as $classNamesAndAlias) { + if ($progressCallback) { + $progressCallback($classNamesAndAlias['alias']); + } + $this->contentRepository->catchUpProjection($classNamesAndAlias['className'], $options); + } + } + + /** + * @return class-string> + */ + private function resolveProjectionClassName(string $projectionAliasOrClassName): string + { + $lowerCaseProjectionName = strtolower($projectionAliasOrClassName); + $projectionClassNamesAndAliases = $this->projectionClassNamesAndAliases(); + foreach ($projectionClassNamesAndAliases as $classNamesAndAlias) { + if (strtolower($classNamesAndAlias['className']) === $lowerCaseProjectionName || strtolower($classNamesAndAlias['alias']) === $lowerCaseProjectionName) { + return $classNamesAndAlias['className']; + } + } + throw new \InvalidArgumentException(sprintf( + 'The projection "%s" is not registered for this Content Repository. The following projection aliases (or fully qualified class names) can be used: %s', + $projectionAliasOrClassName, + implode('', array_map(static fn (array $classNamesAndAlias) => sprintf(chr(10) . ' * %s (%s)', $classNamesAndAlias['alias'], $classNamesAndAlias['className']), $projectionClassNamesAndAliases)) + ), 1680519624); + } + + /** + * @return array>, alias: string}> + */ + private function projectionClassNamesAndAliases(): array + { + return array_map( + static fn (string $projectionClassName) => [ + 'className' => $projectionClassName, + 'alias' => self::projectionAlias($projectionClassName), + ], + $this->projections->getClassNames() + ); + } + + private static function projectionAlias(string $className): string + { + $alias = lcfirst(substr(strrchr($className, '\\') ?: '\\' . $className, 1)); + if (str_ends_with($alias, 'Projection')) { + $alias = substr($alias, 0, -10); + } + return $alias; + } +} diff --git a/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionCatchupServiceFactory.php b/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionCatchupServiceFactory.php new file mode 100644 index 00000000000..126de8eec64 --- /dev/null +++ b/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionCatchupServiceFactory.php @@ -0,0 +1,30 @@ + + * @internal this is currently only used by the {@see CrCommandController} + */ +#[Flow\Scope("singleton")] +final class ProjectionCatchupServiceFactory implements ContentRepositoryServiceFactoryInterface +{ + + public function build(ContentRepositoryServiceFactoryDependencies $serviceFactoryDependencies): ContentRepositoryServiceInterface + { + return new ProjectionCatchupService( + $serviceFactoryDependencies->projections, + $serviceFactoryDependencies->contentRepository, + $serviceFactoryDependencies->eventStore, + ); + } +} diff --git a/Neos.Neos/Classes/Command/SiteCommandController.php b/Neos.Neos/Classes/Command/SiteCommandController.php index 7450e3fcd84..6a98c12a8b4 100644 --- a/Neos.Neos/Classes/Command/SiteCommandController.php +++ b/Neos.Neos/Classes/Command/SiteCommandController.php @@ -178,19 +178,6 @@ public function createCommand($name, $packageKey, $nodeType, $nodeName = null, $ */ public function importAllCommand(string $packageKey = null, string $path = null, string $contentRepository = 'default', bool $verbose = false): void { - $exceedingArguments = $this->request->getExceedingArguments(); - if (isset($exceedingArguments[0]) && $packageKey === null && $path === null) { - if (file_exists($exceedingArguments[0])) { - $path = $exceedingArguments[0]; - } elseif ($this->packageManager->isPackageAvailable($exceedingArguments[0])) { - $packageKey = $exceedingArguments[0]; - } - } - if ($packageKey === null && $path === null) { - $this->outputLine('You have to specify either --package-key or --filename'); - $this->quit(1); - } - // Since this command uses a lot of memory when large sites are imported, we warn the user to watch for // the confirmation of a successful import. $this->outputLine('This command can use a lot of memory when importing sites with many resources.'); @@ -200,25 +187,16 @@ public function importAllCommand(string $packageKey = null, string $path = null, $this->outputLine('Starting import...'); $this->outputLine('---'); + $path = $this->determineTargetPath($packageKey, $path); + $contentRepositoryId = ContentRepositoryId::fromString($contentRepository); - $onProcessor = function (string $processorLabel) { - $this->outputLine('%s...', [$processorLabel]); - }; - $onMessage = function (Severity $severity, string $message) use ($verbose) { - if (!$verbose && $severity === Severity::NOTICE) { - return; - } - $this->outputLine(match ($severity) { - Severity::NOTICE => $message, - Severity::WARNING => sprintf('Warning: %s', $message), - Severity::ERROR => sprintf('Error: %s', $message), - }); - }; - if ($path === null) { - $package = $this->packageManager->getPackage($packageKey); - $path = Files::concatenatePaths([$package->getPackagePath(), 'Resources/Private/Content']); - } - $this->siteImportService->importFromPath($contentRepositoryId, $path, $onProcessor, $onMessage); + + $this->siteImportService->importFromPath( + $contentRepositoryId, + $path, + $this->createOnProcessorClosure(), + $this->createOnMessageClosure($verbose) + ); } /** @@ -237,41 +215,17 @@ public function importAllCommand(string $packageKey = null, string $path = null, */ public function exportAllCommand(string $packageKey = null, string $path = null, string $contentRepository = 'default', bool $verbose = false): void { - $exceedingArguments = $this->request->getExceedingArguments(); - if (isset($exceedingArguments[0]) && $packageKey === null && $path === null) { - if (file_exists($exceedingArguments[0])) { - $path = $exceedingArguments[0]; - } elseif ($this->packageManager->isPackageAvailable($exceedingArguments[0])) { - $packageKey = $exceedingArguments[0]; - } - } - if ($packageKey === null && $path === null) { - $this->outputLine('You have to specify either --package-key or --filename'); - $this->quit(1); - } - + $path = $this->determineTargetPath($packageKey, $path); $contentRepositoryId = ContentRepositoryId::fromString($contentRepository); - $onProcessor = function (string $processorLabel) { - $this->outputLine('%s...', [$processorLabel]); - }; - $onMessage = function (Severity $severity, string $message) use ($verbose) { - if (!$verbose && $severity === Severity::NOTICE) { - return; - } - $this->outputLine(match ($severity) { - Severity::NOTICE => $message, - Severity::WARNING => sprintf('Warning: %s', $message), - Severity::ERROR => sprintf('Error: %s', $message), - }); - }; - if ($path === null) { - $package = $this->packageManager->getPackage($packageKey); - $path = Files::concatenatePaths([$package->getPackagePath(), 'Resources/Private/Content']); - } if (file_exists($path) === false) { Files::createDirectoryRecursively($path); } - $this->siteExportService->exportToPath($contentRepositoryId, $path, $onProcessor, $onMessage); + $this->siteExportService->exportToPath( + $contentRepositoryId, + $path, + $this->createOnProcessorClosure(), + $this->createOnMessageClosure($verbose) + ); } /** @@ -440,4 +394,47 @@ protected function findAllSites(ContentRepository $contentRepository, WorkspaceN } return $sites; } + + protected function determineTargetPath(?string $packageKey, ?string $path): string + { + $exceedingArguments = $this->request->getExceedingArguments(); + if (isset($exceedingArguments[0]) && $packageKey === null && $path === null) { + if (file_exists($exceedingArguments[0])) { + $path = $exceedingArguments[0]; + } elseif ($this->packageManager->isPackageAvailable($exceedingArguments[0])) { + $packageKey = $exceedingArguments[0]; + } + } + if ($packageKey === null && $path === null) { + $this->outputLine('You have to specify either --package-key or --filename'); + $this->quit(1); + } + if ($path === null) { + $package = $this->packageManager->getPackage($packageKey); + $path = Files::concatenatePaths([$package->getPackagePath(), 'Resources/Private/Content']); + } + return $path; + } + + protected function createOnProcessorClosure(): \Closure + { + $onProcessor = function (string $processorLabel) { + $this->outputLine('%s...', [$processorLabel]); + }; + return $onProcessor; + } + + protected function createOnMessageClosure(bool $verbose): \Closure + { + return function (Severity $severity, string $message) use ($verbose) { + if (!$verbose && $severity === Severity::NOTICE) { + return; + } + $this->outputLine(match ($severity) { + Severity::NOTICE => $message, + Severity::WARNING => sprintf('Warning: %s', $message), + Severity::ERROR => sprintf('Error: %s', $message), + }); + }; + } } diff --git a/Neos.Neos/Classes/Domain/Import/LiveWorkspaceIsEmptyProcessor.php b/Neos.Neos/Classes/Domain/Import/LiveWorkspaceIsEmptyProcessor.php index 90c942279ff..3aee0df7594 100644 --- a/Neos.Neos/Classes/Domain/Import/LiveWorkspaceIsEmptyProcessor.php +++ b/Neos.Neos/Classes/Domain/Import/LiveWorkspaceIsEmptyProcessor.php @@ -37,7 +37,7 @@ public function run(ProcessingContext $context): void $context->dispatch(Severity::NOTICE, 'Ensures empty live workspace'); if ($this->workspaceHasEvents(WorkspaceName::forLive())) { - throw new \RuntimeException('Live workspace already contains events please run "cr:prune" before importing.'); + throw new \RuntimeException('Live workspace already contains events please run "site:pruneAll" before importing.'); } } diff --git a/Neos.Neos/Classes/Domain/Service/SiteImportService.php b/Neos.Neos/Classes/Domain/Service/SiteImportService.php index addb86244b1..57d9e8b7c3d 100644 --- a/Neos.Neos/Classes/Domain/Service/SiteImportService.php +++ b/Neos.Neos/Classes/Domain/Service/SiteImportService.php @@ -25,6 +25,8 @@ use Neos\ContentRepository\Export\Processors\AssetRepositoryImportProcessor; use Neos\ContentRepository\Export\Severity; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; +use Neos\ContentRepositoryRegistry\Service\ProjectionCatchupService; +use Neos\ContentRepositoryRegistry\Service\ProjectionCatchupServiceFactory; use Neos\ContentRepositoryRegistry\Service\ProjectionReplayServiceFactory; use Neos\Flow\Annotations as Flow; use Neos\Flow\Persistence\Doctrine\Service as DoctrineService; @@ -52,7 +54,7 @@ public function __construct( private ResourceManager $resourceManager, private PersistenceManagerInterface $persistenceManager, private WorkspaceService $workspaceService, - private ProjectionReplayServiceFactory $projectionReplayServiceFactory, + private ProjectionCatchupServiceFactory $projectionCatchupServiceFactory, ) { } @@ -79,7 +81,7 @@ public function importFromPath(ContentRepositoryId $contentRepositoryId, string 'Create Live workspace' => new LiveWorkspaceCreationProcessor($contentRepository, $this->workspaceService), 'Import events' => $this->contentRepositoryRegistry->buildService($contentRepositoryId, new EventStoreImportProcessorFactory(WorkspaceName::forLive(), keepEventIds: true)), 'Import assets' => new AssetRepositoryImportProcessor($this->assetRepository, $this->resourceRepository, $this->resourceManager, $this->persistenceManager), - 'Replay all projections' => $this->contentRepositoryRegistry->buildService($contentRepositoryId, $this->projectionReplayServiceFactory), + 'Catchup all projections' => $this->contentRepositoryRegistry->buildService($contentRepositoryId, $this->projectionCatchupServiceFactory), ]; foreach ($processors as $processorLabel => $processor) { From 361c71e4c71d9400dfadaeeedbe580991b5f88cc Mon Sep 17 00:00:00 2001 From: Martin Ficzel Date: Wed, 23 Oct 2024 20:02:24 +0200 Subject: [PATCH 034/214] Extract SitePruningService --- .../Classes/Command/SiteCommandController.php | 87 ++-------------- .../ContentRepositoryPruningProcessor.php | 58 +++++++++++ ...ntentRepositoryPruningProcessorFactory.php | 45 +++++++++ .../RoleAndMetadataPruningProcessor.php | 54 ++++++++++ ...RoleAndMetadataPruningProcessorFactory.php | 47 +++++++++ .../Domain/Pruning/SitePruningProcessor.php | 91 +++++++++++++++++ .../Pruning/SitePruningProcessorFactory.php | 52 ++++++++++ .../Domain/Service/SitePruningService.php | 99 +++++++++++++++++++ 8 files changed, 456 insertions(+), 77 deletions(-) create mode 100644 Neos.Neos/Classes/Domain/Pruning/ContentRepositoryPruningProcessor.php create mode 100644 Neos.Neos/Classes/Domain/Pruning/ContentRepositoryPruningProcessorFactory.php create mode 100644 Neos.Neos/Classes/Domain/Pruning/RoleAndMetadataPruningProcessor.php create mode 100644 Neos.Neos/Classes/Domain/Pruning/RoleAndMetadataPruningProcessorFactory.php create mode 100644 Neos.Neos/Classes/Domain/Pruning/SitePruningProcessor.php create mode 100644 Neos.Neos/Classes/Domain/Pruning/SitePruningProcessorFactory.php create mode 100644 Neos.Neos/Classes/Domain/Service/SitePruningService.php diff --git a/Neos.Neos/Classes/Command/SiteCommandController.php b/Neos.Neos/Classes/Command/SiteCommandController.php index 6a98c12a8b4..7a1de11bc18 100644 --- a/Neos.Neos/Classes/Command/SiteCommandController.php +++ b/Neos.Neos/Classes/Command/SiteCommandController.php @@ -14,16 +14,11 @@ namespace Neos\Neos\Command; -use Neos\ContentRepository\Core\ContentRepository; -use Neos\ContentRepository\Core\Service\ContentStreamPrunerFactory; -use Neos\ContentRepository\Core\Service\WorkspaceMaintenanceServiceFactory; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\Exception\NodeNameIsAlreadyCovered; use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeNotFound; -use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepository\Export\Severity; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; -use Neos\ContentRepositoryRegistry\Service\ProjectionReplayServiceFactory; use Neos\Flow\Annotations as Flow; use Neos\Flow\Cli\CommandController; use Neos\Flow\Cli\Exception\StopCommandException; @@ -37,6 +32,7 @@ use Neos\Neos\Domain\Service\NodeTypeNameFactory; use Neos\Neos\Domain\Service\SiteExportService; use Neos\Neos\Domain\Service\SiteImportService; +use Neos\Neos\Domain\Service\SitePruningService; use Neos\Neos\Domain\Service\SiteService; use Neos\Neos\Domain\Service\WorkspaceService; use Neos\Utility\Files; @@ -98,15 +94,15 @@ class SiteCommandController extends CommandController /** * @Flow\Inject - * @var WorkspaceService + * @var SitePruningService */ - protected $workspaceService; + protected $sitePruningService; /** - * @Flow\Inject(lazy=false) - * @var ProjectionReplayServiceFactory + * @Flow\Inject + * @var WorkspaceService */ - protected $projectionServiceFactory; + protected $workspaceService; /** * Create a new site @@ -231,11 +227,10 @@ public function exportAllCommand(string $packageKey = null, string $path = null, /** * This will completely prune the data of the specified content repository and remove all site-records. * - * @param string $contentRepository Name of the content repository where the data should be pruned from. * @param bool $force Prune the cr without confirmation. This cannot be reverted! * @return void */ - public function pruneAllCommand(string $contentRepository = 'default', bool $force = false): void + public function pruneAllCommand(string $contentRepository = 'default', bool $force = false, bool $verbose = false): void { if (!$force && !$this->output->askConfirmation(sprintf('> This will prune your content repository "%s". Are you sure to proceed? (y/n) ', $contentRepository), false)) { $this->outputLine('Abort.'); @@ -243,42 +238,11 @@ public function pruneAllCommand(string $contentRepository = 'default', bool $for } $contentRepositoryId = ContentRepositoryId::fromString($contentRepository); - // find and remove all sites - $sites = $this->findAllSites( - $this->contentRepositoryRegistry->get($contentRepositoryId), - WorkspaceName::forLive() - ); - foreach ($sites as $site) { - $this->siteService->pruneSite($site); - } - - // remove cr data - $contentStreamPruner = $this->contentRepositoryRegistry->buildService( - $contentRepositoryId, - new ContentStreamPrunerFactory() - ); - $workspaceMaintenanceService = $this->contentRepositoryRegistry->buildService( - $contentRepositoryId, - new WorkspaceMaintenanceServiceFactory() - ); - - $projectionService = $this->contentRepositoryRegistry->buildService( + $this->sitePruningService->pruneAll( $contentRepositoryId, - $this->projectionServiceFactory + $this->createOnProcessorClosure(), + $this->createOnMessageClosure($verbose) ); - - // remove the workspace metadata and roles for this cr - $this->workspaceService->pruneRoleAsssignments($contentRepositoryId); - $this->workspaceService->pruneWorkspaceMetadata($contentRepositoryId); - - // reset the events table - $contentStreamPruner->pruneAll(); - $workspaceMaintenanceService->pruneAll(); - - // reset the projections state - $projectionService->resetAllProjections(); - - $this->outputLine('Done.'); } /** @@ -364,37 +328,6 @@ function (Site $site) use ($siteNodePattern) { ); } - /** - * Find all sites in a cr by finding the children of the sites node - * - * @param ContentRepository $contentRepository - * @param WorkspaceName $workspaceName - * @return Site[] - */ - protected function findAllSites(ContentRepository $contentRepository, WorkspaceName $workspaceName): array - { - $contentGraph = $contentRepository->getContentGraph($workspaceName); - $sitesNodeAggregate = $contentGraph->findRootNodeAggregateByType(NodeTypeNameFactory::forSites()); - if ($sitesNodeAggregate === null) { - return []; - } - - $siteNodeAggregates = $contentGraph->findChildNodeAggregates($sitesNodeAggregate->nodeAggregateId); - $sites = []; - foreach ($siteNodeAggregates as $siteNodeAggregate) { - $siteNodeName = $siteNodeAggregate->nodeName?->value; - if ($siteNodeName === null) { - continue; - } - $site = $this->siteRepository->findOneByNodeName($siteNodeName); - if ($site === null) { - continue; - } - $sites[] = $site; - } - return $sites; - } - protected function determineTargetPath(?string $packageKey, ?string $path): string { $exceedingArguments = $this->request->getExceedingArguments(); diff --git a/Neos.Neos/Classes/Domain/Pruning/ContentRepositoryPruningProcessor.php b/Neos.Neos/Classes/Domain/Pruning/ContentRepositoryPruningProcessor.php new file mode 100644 index 00000000000..be73e71a1d1 --- /dev/null +++ b/Neos.Neos/Classes/Domain/Pruning/ContentRepositoryPruningProcessor.php @@ -0,0 +1,58 @@ +contentRepository->findContentStreams() as $contentStream) { + $streamName = ContentStreamEventStreamName::fromContentStreamId($contentStream->id)->getEventStreamName(); + $this->eventStore->deleteStream($streamName); + } + foreach ($this->contentRepository->findWorkspaces() as $workspace) { + $streamName = WorkspaceEventStreamName::fromWorkspaceName($workspace->workspaceName)->getEventStreamName(); + $this->eventStore->deleteStream($streamName); + } + } +} diff --git a/Neos.Neos/Classes/Domain/Pruning/ContentRepositoryPruningProcessorFactory.php b/Neos.Neos/Classes/Domain/Pruning/ContentRepositoryPruningProcessorFactory.php new file mode 100644 index 00000000000..84b645624f4 --- /dev/null +++ b/Neos.Neos/Classes/Domain/Pruning/ContentRepositoryPruningProcessorFactory.php @@ -0,0 +1,45 @@ + + */ +final readonly class ContentRepositoryPruningProcessorFactory implements ContentRepositoryServiceFactoryInterface +{ + + public function __construct( + ) { + } + + public function build(ContentRepositoryServiceFactoryDependencies $serviceFactoryDependencies): ContentRepositoryServiceInterface + { + return new ContentRepositoryPruningProcessor( + $serviceFactoryDependencies->contentRepository, + $serviceFactoryDependencies->eventStore, + ); + } +} diff --git a/Neos.Neos/Classes/Domain/Pruning/RoleAndMetadataPruningProcessor.php b/Neos.Neos/Classes/Domain/Pruning/RoleAndMetadataPruningProcessor.php new file mode 100644 index 00000000000..c4aa7592586 --- /dev/null +++ b/Neos.Neos/Classes/Domain/Pruning/RoleAndMetadataPruningProcessor.php @@ -0,0 +1,54 @@ +workspaceService->pruneRoleAsssignments($this->contentRepositoryId); + $this->workspaceService->pruneWorkspaceMetadata($this->contentRepositoryId); + } +} diff --git a/Neos.Neos/Classes/Domain/Pruning/RoleAndMetadataPruningProcessorFactory.php b/Neos.Neos/Classes/Domain/Pruning/RoleAndMetadataPruningProcessorFactory.php new file mode 100644 index 00000000000..116821465f6 --- /dev/null +++ b/Neos.Neos/Classes/Domain/Pruning/RoleAndMetadataPruningProcessorFactory.php @@ -0,0 +1,47 @@ + + */ +final readonly class RoleAndMetadataPruningProcessorFactory implements ContentRepositoryServiceFactoryInterface +{ + + public function __construct( + private WorkspaceService $workspaceService, + ) { + } + + public function build(ContentRepositoryServiceFactoryDependencies $serviceFactoryDependencies): ContentRepositoryServiceInterface + { + return new RoleAndMetadataPruningProcessor( + $serviceFactoryDependencies->contentRepositoryId, + $this->workspaceService, + ); + } +} diff --git a/Neos.Neos/Classes/Domain/Pruning/SitePruningProcessor.php b/Neos.Neos/Classes/Domain/Pruning/SitePruningProcessor.php new file mode 100644 index 00000000000..7c272774d91 --- /dev/null +++ b/Neos.Neos/Classes/Domain/Pruning/SitePruningProcessor.php @@ -0,0 +1,91 @@ +findAllSites(); + foreach ($sites as $site) { + $domains = $site->getDomains(); + if ($site->getPrimaryDomain() !== null) { + $site->setPrimaryDomain(null); + $this->siteRepository->update($site); + } + foreach ($domains as $domain) { + $this->domainRepository->remove($domain); + } + $this->persistenceManager->persistAll(); + $this->siteRepository->remove($site); + $this->persistenceManager->persistAll(); + } + } + + /** + * @return Site[] + */ + protected function findAllSites(): array + { + $contentGraph = $this->contentRepository->getContentGraph($this->workspaceName); + $sitesNodeAggregate = $contentGraph->findRootNodeAggregateByType(NodeTypeNameFactory::forSites()); + if ($sitesNodeAggregate === null) { + return []; + } + + $siteNodeAggregates = $contentGraph->findChildNodeAggregates($sitesNodeAggregate->nodeAggregateId); + $sites = []; + foreach ($siteNodeAggregates as $siteNodeAggregate) { + $siteNodeName = $siteNodeAggregate->nodeName?->value; + if ($siteNodeName === null) { + continue; + } + $site = $this->siteRepository->findOneByNodeName($siteNodeName); + if ($site === null) { + continue; + } + $sites[] = $site; + } + return $sites; + } +} diff --git a/Neos.Neos/Classes/Domain/Pruning/SitePruningProcessorFactory.php b/Neos.Neos/Classes/Domain/Pruning/SitePruningProcessorFactory.php new file mode 100644 index 00000000000..76f0e0b55d0 --- /dev/null +++ b/Neos.Neos/Classes/Domain/Pruning/SitePruningProcessorFactory.php @@ -0,0 +1,52 @@ + + */ +final readonly class SitePruningProcessorFactory implements ContentRepositoryServiceFactoryInterface +{ + + public function __construct( + private WorkspaceName $workspaceName, + private SiteRepository $siteRepository, + private DomainRepository $domainRepository, + private PersistenceManagerInterface $persistenceManager, + ) { + } + + public function build(ContentRepositoryServiceFactoryDependencies $serviceFactoryDependencies): ContentRepositoryServiceInterface + { + return new SitePruningProcessor( + $serviceFactoryDependencies->contentRepository, + $this->workspaceName, + $this->siteRepository, + $this->domainRepository, + $this->persistenceManager + ); + } +} diff --git a/Neos.Neos/Classes/Domain/Service/SitePruningService.php b/Neos.Neos/Classes/Domain/Service/SitePruningService.php new file mode 100644 index 00000000000..fc5069768d1 --- /dev/null +++ b/Neos.Neos/Classes/Domain/Service/SitePruningService.php @@ -0,0 +1,99 @@ + $processors */ + $processors = [ + 'Remove site nodes' => $this->contentRepositoryRegistry->buildService( + $contentRepositoryId, + new SitePruningProcessorFactory( + WorkspaceName::forLive(), + $this->siteRepository, + $this->domainRepository, + $this->persistenceManager + ) + ), + 'Prune content repository' => $this->contentRepositoryRegistry->buildService( + $contentRepositoryId, + new ContentRepositoryPruningProcessorFactory() + ), + 'Prune roles and metadata' => $this->contentRepositoryRegistry->buildService( + $contentRepositoryId, + new RoleAndMetadataPruningProcessorFactory( + $this->workspaceService + ) + ), + 'Replay all projections' => $this->contentRepositoryRegistry->buildService($contentRepositoryId, $this->projectionReplayServiceFactory), + ]; + + foreach ($processors as $processorLabel => $processor) { + ($onProcessor)($processorLabel); + $processor->run($context); + } + } +} From a05ad66f6e4c4ae6a04e6353a55f4e9ed5461fa8 Mon Sep 17 00:00:00 2001 From: Martin Ficzel Date: Thu, 24 Oct 2024 11:34:57 +0200 Subject: [PATCH 035/214] Remove ProcessorInterface from ProjectionService and introduce separate ProjectionCatchUpProcessor and ProjectionReplayProcessor. --- .../Classes/Command/CrCommandController.php | 22 ++--- .../composer.json | 1 + .../Classes/Command/CrCommandController.php | 4 +- .../Processors/ProjectionCatchupProcessor.php | 29 ++++++ .../ProjectionCatchupProcessorFactory.php | 36 +++++++ .../Processors/ProjectionReplayProcessor.php | 30 ++++++ .../ProjectionReplayProcessorFactory.php | 34 +++++++ .../Service/ProjectionCatchupService.php | 95 ------------------- .../ProjectionCatchupServiceFactory.php | 30 ------ ...eplayService.php => ProjectionService.php} | 28 ++++-- ...ctory.php => ProjectionServiceFactory.php} | 9 +- .../Classes/Command/SiteCommandController.php | 2 +- .../ContentRepositoryPruningProcessor.php | 13 +-- ...ntentRepositoryPruningProcessorFactory.php | 12 +-- .../RoleAndMetadataPruningProcessor.php | 15 --- ...RoleAndMetadataPruningProcessorFactory.php | 8 -- .../Pruning/SitePruningProcessorFactory.php | 3 +- .../Domain/Service/SiteImportService.php | 26 ++--- .../Domain/Service/SitePruningService.php | 21 ++-- phpstan-baseline.neon | 2 +- 20 files changed, 196 insertions(+), 224 deletions(-) create mode 100644 Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionCatchupProcessor.php create mode 100644 Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionCatchupProcessorFactory.php create mode 100644 Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionReplayProcessor.php create mode 100644 Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionReplayProcessorFactory.php delete mode 100644 Neos.ContentRepositoryRegistry/Classes/Service/ProjectionCatchupService.php delete mode 100644 Neos.ContentRepositoryRegistry/Classes/Service/ProjectionCatchupServiceFactory.php rename Neos.ContentRepositoryRegistry/Classes/Service/{ProjectionReplayService.php => ProjectionService.php} (83%) rename Neos.ContentRepositoryRegistry/Classes/Service/{ProjectionReplayServiceFactory.php => ProjectionServiceFactory.php} (81%) diff --git a/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/CrCommandController.php b/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/CrCommandController.php index 342bad83647..90b0fa78049 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/CrCommandController.php +++ b/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/CrCommandController.php @@ -24,7 +24,7 @@ use Neos\ContentRepository\LegacyNodeMigration\LegacyMigrationServiceFactory; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\ContentRepositoryRegistry\Factory\EventStore\DoctrineEventStoreFactory; -use Neos\ContentRepositoryRegistry\Service\ProjectionReplayServiceFactory; +use Neos\ContentRepositoryRegistry\Service\ProjectionServiceFactory; use Neos\Flow\Cli\CommandController; use Neos\Flow\Persistence\PersistenceManagerInterface; use Neos\Flow\Property\PropertyMapper; @@ -38,16 +38,16 @@ class CrCommandController extends CommandController { public function __construct( - private readonly Connection $connection, - private readonly Environment $environment, + private readonly Connection $connection, + private readonly Environment $environment, private readonly PersistenceManagerInterface $persistenceManager, - private readonly AssetRepository $assetRepository, - private readonly ResourceRepository $resourceRepository, - private readonly ResourceManager $resourceManager, - private readonly PropertyMapper $propertyMapper, - private readonly ContentRepositoryRegistry $contentRepositoryRegistry, - private readonly SiteRepository $siteRepository, - private readonly ProjectionReplayServiceFactory $projectionReplayServiceFactory, + private readonly AssetRepository $assetRepository, + private readonly ResourceRepository $resourceRepository, + private readonly ResourceManager $resourceManager, + private readonly PropertyMapper $propertyMapper, + private readonly ContentRepositoryRegistry $contentRepositoryRegistry, + private readonly SiteRepository $siteRepository, + private readonly ProjectionServiceFactory $projectionServiceFactory, ) { parent::__construct(); } @@ -120,7 +120,7 @@ public function migrateLegacyDataCommand(bool $verbose = false, string $config = } $this->connection->executeStatement('TRUNCATE ' . $connection->quoteIdentifier($eventTableName)); // we also need to reset the projections; in order to ensure the system runs deterministically - $projectionService = $this->contentRepositoryRegistry->buildService($contentRepositoryId, $this->projectionReplayServiceFactory); + $projectionService = $this->contentRepositoryRegistry->buildService($contentRepositoryId, $this->projectionServiceFactory); $projectionService->resetAllProjections(); $this->outputLine('Truncated events'); diff --git a/Neos.ContentRepository.LegacyNodeMigration/composer.json b/Neos.ContentRepository.LegacyNodeMigration/composer.json index acb3ddbb2d1..f88ba50c268 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/composer.json +++ b/Neos.ContentRepository.LegacyNodeMigration/composer.json @@ -12,6 +12,7 @@ ], "require": { "php": ">=8.2", + "neoe/neos": "self.version", "neos/contentrepository-core": "self.version", "neos/contentrepository-export": "self.version", "league/flysystem": "^3" diff --git a/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php b/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php index 459398dfeb0..38149fcbe74 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php +++ b/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php @@ -9,7 +9,7 @@ use Neos\ContentRepository\Core\Service\WorkspaceMaintenanceServiceFactory; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; -use Neos\ContentRepositoryRegistry\Service\ProjectionReplayServiceFactory; +use Neos\ContentRepositoryRegistry\Service\ProjectionServiceFactory; use Neos\EventStore\Model\Event\SequenceNumber; use Neos\EventStore\Model\EventStore\StatusType; use Neos\Flow\Cli\CommandController; @@ -23,7 +23,7 @@ final class CrCommandController extends CommandController public function __construct( private readonly ContentRepositoryRegistry $contentRepositoryRegistry, - private readonly ProjectionReplayServiceFactory $projectionServiceFactory, + private readonly ProjectionServiceFactory $projectionServiceFactory, ) { parent::__construct(); } diff --git a/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionCatchupProcessor.php b/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionCatchupProcessor.php new file mode 100644 index 00000000000..3f837b91571 --- /dev/null +++ b/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionCatchupProcessor.php @@ -0,0 +1,29 @@ +projectionservice->catchupAllProjections(CatchUpOptions::create()); + } +} diff --git a/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionCatchupProcessorFactory.php b/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionCatchupProcessorFactory.php new file mode 100644 index 00000000000..14436d9dc52 --- /dev/null +++ b/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionCatchupProcessorFactory.php @@ -0,0 +1,36 @@ + + * @internal this is currently only used by the {@see SiteImportService} {@see SitePruningService} + */ +#[Flow\Scope("singleton")] +final class ProjectionCatchupProcessorFactory implements ContentRepositoryServiceFactoryInterface +{ + + public function build(ContentRepositoryServiceFactoryDependencies $serviceFactoryDependencies): ContentRepositoryServiceInterface + { + return new ProjectionCatchupProcessor( + new ProjectionService( + $serviceFactoryDependencies->projections, + $serviceFactoryDependencies->contentRepository, + $serviceFactoryDependencies->eventStore, + ) + ); + } +} diff --git a/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionReplayProcessor.php b/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionReplayProcessor.php new file mode 100644 index 00000000000..91a764eef56 --- /dev/null +++ b/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionReplayProcessor.php @@ -0,0 +1,30 @@ +projectionService->replayAllProjections(CatchUpOptions::create()); + } +} diff --git a/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionReplayProcessorFactory.php b/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionReplayProcessorFactory.php new file mode 100644 index 00000000000..4b59f652c5d --- /dev/null +++ b/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionReplayProcessorFactory.php @@ -0,0 +1,34 @@ + + * @internal this is currently only used by the {@see SitePruningService} + */ +#[Flow\Scope("singleton")] +final class ProjectionReplayProcessorFactory implements ContentRepositoryServiceFactoryInterface +{ + public function build(ContentRepositoryServiceFactoryDependencies $serviceFactoryDependencies): ContentRepositoryServiceInterface + { + return new ProjectionReplayProcessor( + new ProjectionService( + $serviceFactoryDependencies->projections, + $serviceFactoryDependencies->contentRepository, + $serviceFactoryDependencies->eventStore, + ) + ); + } +} diff --git a/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionCatchupService.php b/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionCatchupService.php deleted file mode 100644 index 7112ad85486..00000000000 --- a/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionCatchupService.php +++ /dev/null @@ -1,95 +0,0 @@ -catchupAllProjections(CatchUpOptions::create()); - } - - public function catchupProjection(string $projectionAliasOrClassName, CatchUpOptions $options): void - { - $projectionClassName = $this->resolveProjectionClassName($projectionAliasOrClassName); - $this->contentRepository->catchUpProjection($projectionClassName, $options); - } - - public function catchupAllProjections(CatchUpOptions $options, ?\Closure $progressCallback = null): void - { - foreach ($this->projectionClassNamesAndAliases() as $classNamesAndAlias) { - if ($progressCallback) { - $progressCallback($classNamesAndAlias['alias']); - } - $this->contentRepository->catchUpProjection($classNamesAndAlias['className'], $options); - } - } - - /** - * @return class-string> - */ - private function resolveProjectionClassName(string $projectionAliasOrClassName): string - { - $lowerCaseProjectionName = strtolower($projectionAliasOrClassName); - $projectionClassNamesAndAliases = $this->projectionClassNamesAndAliases(); - foreach ($projectionClassNamesAndAliases as $classNamesAndAlias) { - if (strtolower($classNamesAndAlias['className']) === $lowerCaseProjectionName || strtolower($classNamesAndAlias['alias']) === $lowerCaseProjectionName) { - return $classNamesAndAlias['className']; - } - } - throw new \InvalidArgumentException(sprintf( - 'The projection "%s" is not registered for this Content Repository. The following projection aliases (or fully qualified class names) can be used: %s', - $projectionAliasOrClassName, - implode('', array_map(static fn (array $classNamesAndAlias) => sprintf(chr(10) . ' * %s (%s)', $classNamesAndAlias['alias'], $classNamesAndAlias['className']), $projectionClassNamesAndAliases)) - ), 1680519624); - } - - /** - * @return array>, alias: string}> - */ - private function projectionClassNamesAndAliases(): array - { - return array_map( - static fn (string $projectionClassName) => [ - 'className' => $projectionClassName, - 'alias' => self::projectionAlias($projectionClassName), - ], - $this->projections->getClassNames() - ); - } - - private static function projectionAlias(string $className): string - { - $alias = lcfirst(substr(strrchr($className, '\\') ?: '\\' . $className, 1)); - if (str_ends_with($alias, 'Projection')) { - $alias = substr($alias, 0, -10); - } - return $alias; - } -} diff --git a/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionCatchupServiceFactory.php b/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionCatchupServiceFactory.php deleted file mode 100644 index 126de8eec64..00000000000 --- a/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionCatchupServiceFactory.php +++ /dev/null @@ -1,30 +0,0 @@ - - * @internal this is currently only used by the {@see CrCommandController} - */ -#[Flow\Scope("singleton")] -final class ProjectionCatchupServiceFactory implements ContentRepositoryServiceFactoryInterface -{ - - public function build(ContentRepositoryServiceFactoryDependencies $serviceFactoryDependencies): ContentRepositoryServiceInterface - { - return new ProjectionCatchupService( - $serviceFactoryDependencies->projections, - $serviceFactoryDependencies->contentRepository, - $serviceFactoryDependencies->eventStore, - ); - } -} diff --git a/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionReplayService.php b/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionService.php similarity index 83% rename from Neos.ContentRepositoryRegistry/Classes/Service/ProjectionReplayService.php rename to Neos.ContentRepositoryRegistry/Classes/Service/ProjectionService.php index 87753b37e58..3a6cc5e242f 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionReplayService.php +++ b/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionService.php @@ -9,20 +9,17 @@ use Neos\ContentRepository\Core\Projection\ProjectionInterface; use Neos\ContentRepository\Core\Projection\Projections; use Neos\ContentRepository\Core\Projection\ProjectionStateInterface; -use Neos\ContentRepository\Export\ProcessingContext; -use Neos\ContentRepository\Export\ProcessorInterface; use Neos\EventStore\EventStoreInterface; use Neos\EventStore\Model\Event\SequenceNumber; use Neos\EventStore\Model\EventStream\VirtualStreamName; /** - * Content Repository service to perform Projection replays + * Content Repository service to perform Projection operations * * @internal this is currently only used by the {@see CrCommandController} */ -final class ProjectionReplayService implements ProcessorInterface, ContentRepositoryServiceInterface +final class ProjectionService implements ContentRepositoryServiceInterface { - public function __construct( private readonly Projections $projections, private readonly ContentRepository $contentRepository, @@ -30,11 +27,6 @@ public function __construct( ) { } - public function run(ProcessingContext $context): void - { - $this->replayAllProjections(CatchUpOptions::create()); - } - public function replayProjection(string $projectionAliasOrClassName, CatchUpOptions $options): void { $projectionClassName = $this->resolveProjectionClassName($projectionAliasOrClassName); @@ -60,6 +52,22 @@ public function resetAllProjections(): void } } + public function catchupProjection(string $projectionAliasOrClassName, CatchUpOptions $options): void + { + $projectionClassName = $this->resolveProjectionClassName($projectionAliasOrClassName); + $this->contentRepository->catchUpProjection($projectionClassName, $options); + } + + public function catchupAllProjections(CatchUpOptions $options, ?\Closure $progressCallback = null): void + { + foreach ($this->projectionClassNamesAndAliases() as $classNamesAndAlias) { + if ($progressCallback) { + $progressCallback($classNamesAndAlias['alias']); + } + $this->contentRepository->catchUpProjection($classNamesAndAlias['className'], $options); + } + } + public function highestSequenceNumber(): SequenceNumber { foreach ($this->eventStore->load(VirtualStreamName::all())->backwards()->limit(1) as $eventEnvelope) { diff --git a/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionReplayServiceFactory.php b/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionServiceFactory.php similarity index 81% rename from Neos.ContentRepositoryRegistry/Classes/Service/ProjectionReplayServiceFactory.php rename to Neos.ContentRepositoryRegistry/Classes/Service/ProjectionServiceFactory.php index 337297d9bb6..43b2c53fdf5 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionReplayServiceFactory.php +++ b/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionServiceFactory.php @@ -10,18 +10,17 @@ use Neos\Flow\Annotations as Flow; /** - * Factory for the {@see ProjectionReplayService} + * Factory for the {@see ProjectionService} * - * @implements ContentRepositoryServiceFactoryInterface + * @implements ContentRepositoryServiceFactoryInterface * @internal this is currently only used by the {@see CrCommandController} */ #[Flow\Scope("singleton")] -final class ProjectionReplayServiceFactory implements ContentRepositoryServiceFactoryInterface +final class ProjectionServiceFactory implements ContentRepositoryServiceFactoryInterface { - public function build(ContentRepositoryServiceFactoryDependencies $serviceFactoryDependencies): ContentRepositoryServiceInterface { - return new ProjectionReplayService( + return new ProjectionService( $serviceFactoryDependencies->projections, $serviceFactoryDependencies->contentRepository, $serviceFactoryDependencies->eventStore, diff --git a/Neos.Neos/Classes/Command/SiteCommandController.php b/Neos.Neos/Classes/Command/SiteCommandController.php index 7a1de11bc18..f0f93259a18 100644 --- a/Neos.Neos/Classes/Command/SiteCommandController.php +++ b/Neos.Neos/Classes/Command/SiteCommandController.php @@ -339,7 +339,7 @@ protected function determineTargetPath(?string $packageKey, ?string $path): stri } } if ($packageKey === null && $path === null) { - $this->outputLine('You have to specify either --package-key or --filename'); + $this->outputLine('You have to specify either --package-key or --path'); $this->quit(1); } if ($path === null) { diff --git a/Neos.Neos/Classes/Domain/Pruning/ContentRepositoryPruningProcessor.php b/Neos.Neos/Classes/Domain/Pruning/ContentRepositoryPruningProcessor.php index be73e71a1d1..d6f6330d373 100644 --- a/Neos.Neos/Classes/Domain/Pruning/ContentRepositoryPruningProcessor.php +++ b/Neos.Neos/Classes/Domain/Pruning/ContentRepositoryPruningProcessor.php @@ -14,24 +14,13 @@ namespace Neos\Neos\Domain\Pruning; -use JsonException; use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface; use Neos\ContentRepository\Core\Feature\ContentStreamEventStreamName; use Neos\ContentRepository\Core\Feature\WorkspaceEventStreamName; -use Neos\ContentRepository\Core\SharedModel\Node\NodeName; -use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; -use Neos\ContentRepository\Export\Event\ValueObject\ExportedEvent; use Neos\ContentRepository\Export\ProcessingContext; use Neos\ContentRepository\Export\ProcessorInterface; -use Neos\ContentRepository\Export\Severity; use Neos\EventStore\EventStoreInterface; -use Neos\Flow\Persistence\PersistenceManagerInterface; -use Neos\Neos\Domain\Model\Domain; -use Neos\Neos\Domain\Model\Site; -use Neos\Neos\Domain\Repository\DomainRepository; -use Neos\Neos\Domain\Repository\SiteRepository; -use Neos\Neos\Domain\Service\NodeTypeNameFactory; /** * Pruning processor that removes all events from the given cr @@ -47,10 +36,12 @@ public function __construct( public function run(ProcessingContext $context): void { foreach ($this->contentRepository->findContentStreams() as $contentStream) { + /** @phpstan-ignore-next-line calling internal method */ $streamName = ContentStreamEventStreamName::fromContentStreamId($contentStream->id)->getEventStreamName(); $this->eventStore->deleteStream($streamName); } foreach ($this->contentRepository->findWorkspaces() as $workspace) { + /** @phpstan-ignore-next-line calling internal method */ $streamName = WorkspaceEventStreamName::fromWorkspaceName($workspace->workspaceName)->getEventStreamName(); $this->eventStore->deleteStream($streamName); } diff --git a/Neos.Neos/Classes/Domain/Pruning/ContentRepositoryPruningProcessorFactory.php b/Neos.Neos/Classes/Domain/Pruning/ContentRepositoryPruningProcessorFactory.php index 84b645624f4..db2299fa348 100644 --- a/Neos.Neos/Classes/Domain/Pruning/ContentRepositoryPruningProcessorFactory.php +++ b/Neos.Neos/Classes/Domain/Pruning/ContentRepositoryPruningProcessorFactory.php @@ -17,22 +17,14 @@ use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceFactoryDependencies; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceFactoryInterface; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface; -use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; -use Neos\ContentRepository\Export\Processors\EventExportProcessor; -use Neos\Flow\Persistence\Doctrine\PersistenceManager; -use Neos\Flow\Persistence\PersistenceManagerInterface; -use Neos\Neos\Domain\Import\LiveWorkspaceIsEmptyProcessor; -use Neos\Neos\Domain\Repository\DomainRepository; -use Neos\Neos\Domain\Repository\SiteRepository; /** * @implements ContentRepositoryServiceFactoryInterface */ final readonly class ContentRepositoryPruningProcessorFactory implements ContentRepositoryServiceFactoryInterface { - - public function __construct( - ) { + public function __construct() + { } public function build(ContentRepositoryServiceFactoryDependencies $serviceFactoryDependencies): ContentRepositoryServiceInterface diff --git a/Neos.Neos/Classes/Domain/Pruning/RoleAndMetadataPruningProcessor.php b/Neos.Neos/Classes/Domain/Pruning/RoleAndMetadataPruningProcessor.php index c4aa7592586..81369e66307 100644 --- a/Neos.Neos/Classes/Domain/Pruning/RoleAndMetadataPruningProcessor.php +++ b/Neos.Neos/Classes/Domain/Pruning/RoleAndMetadataPruningProcessor.php @@ -14,25 +14,10 @@ namespace Neos\Neos\Domain\Pruning; -use JsonException; -use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface; -use Neos\ContentRepository\Core\Feature\ContentStreamEventStreamName; -use Neos\ContentRepository\Core\Feature\WorkspaceEventStreamName; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; -use Neos\ContentRepository\Core\SharedModel\Node\NodeName; -use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; -use Neos\ContentRepository\Export\Event\ValueObject\ExportedEvent; use Neos\ContentRepository\Export\ProcessingContext; use Neos\ContentRepository\Export\ProcessorInterface; -use Neos\ContentRepository\Export\Severity; -use Neos\EventStore\EventStoreInterface; -use Neos\Flow\Persistence\PersistenceManagerInterface; -use Neos\Neos\Domain\Model\Domain; -use Neos\Neos\Domain\Model\Site; -use Neos\Neos\Domain\Repository\DomainRepository; -use Neos\Neos\Domain\Repository\SiteRepository; -use Neos\Neos\Domain\Service\NodeTypeNameFactory; use Neos\Neos\Domain\Service\WorkspaceService; /** diff --git a/Neos.Neos/Classes/Domain/Pruning/RoleAndMetadataPruningProcessorFactory.php b/Neos.Neos/Classes/Domain/Pruning/RoleAndMetadataPruningProcessorFactory.php index 116821465f6..456e0c10623 100644 --- a/Neos.Neos/Classes/Domain/Pruning/RoleAndMetadataPruningProcessorFactory.php +++ b/Neos.Neos/Classes/Domain/Pruning/RoleAndMetadataPruningProcessorFactory.php @@ -17,13 +17,6 @@ use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceFactoryDependencies; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceFactoryInterface; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface; -use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; -use Neos\ContentRepository\Export\Processors\EventExportProcessor; -use Neos\Flow\Persistence\Doctrine\PersistenceManager; -use Neos\Flow\Persistence\PersistenceManagerInterface; -use Neos\Neos\Domain\Import\LiveWorkspaceIsEmptyProcessor; -use Neos\Neos\Domain\Repository\DomainRepository; -use Neos\Neos\Domain\Repository\SiteRepository; use Neos\Neos\Domain\Service\WorkspaceService; /** @@ -31,7 +24,6 @@ */ final readonly class RoleAndMetadataPruningProcessorFactory implements ContentRepositoryServiceFactoryInterface { - public function __construct( private WorkspaceService $workspaceService, ) { diff --git a/Neos.Neos/Classes/Domain/Pruning/SitePruningProcessorFactory.php b/Neos.Neos/Classes/Domain/Pruning/SitePruningProcessorFactory.php index 76f0e0b55d0..5c23d8cc895 100644 --- a/Neos.Neos/Classes/Domain/Pruning/SitePruningProcessorFactory.php +++ b/Neos.Neos/Classes/Domain/Pruning/SitePruningProcessorFactory.php @@ -26,11 +26,10 @@ use Neos\Neos\Domain\Repository\SiteRepository; /** - * @implements ContentRepositoryServiceFactoryInterface + * @implements ContentRepositoryServiceFactoryInterface */ final readonly class SitePruningProcessorFactory implements ContentRepositoryServiceFactoryInterface { - public function __construct( private WorkspaceName $workspaceName, private SiteRepository $siteRepository, diff --git a/Neos.Neos/Classes/Domain/Service/SiteImportService.php b/Neos.Neos/Classes/Domain/Service/SiteImportService.php index 57d9e8b7c3d..7b8ba4503ba 100644 --- a/Neos.Neos/Classes/Domain/Service/SiteImportService.php +++ b/Neos.Neos/Classes/Domain/Service/SiteImportService.php @@ -25,9 +25,9 @@ use Neos\ContentRepository\Export\Processors\AssetRepositoryImportProcessor; use Neos\ContentRepository\Export\Severity; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; -use Neos\ContentRepositoryRegistry\Service\ProjectionCatchupService; -use Neos\ContentRepositoryRegistry\Service\ProjectionCatchupServiceFactory; -use Neos\ContentRepositoryRegistry\Service\ProjectionReplayServiceFactory; +use Neos\ContentRepositoryRegistry\Processors\ProjectionCatchupProcessor; +use Neos\ContentRepositoryRegistry\Processors\ProjectionCatchupProcessorFactory; +use Neos\ContentRepositoryRegistry\Service\ProjectionServiceFactory; use Neos\Flow\Annotations as Flow; use Neos\Flow\Persistence\Doctrine\Service as DoctrineService; use Neos\Flow\Persistence\PersistenceManagerInterface; @@ -45,16 +45,16 @@ final readonly class SiteImportService { public function __construct( - private ContentRepositoryRegistry $contentRepositoryRegistry, - private DoctrineService $doctrineService, - private SiteRepository $siteRepository, - private DomainRepository $domainRepository, - private AssetRepository $assetRepository, - private ResourceRepository $resourceRepository, - private ResourceManager $resourceManager, - private PersistenceManagerInterface $persistenceManager, - private WorkspaceService $workspaceService, - private ProjectionCatchupServiceFactory $projectionCatchupServiceFactory, + private ContentRepositoryRegistry $contentRepositoryRegistry, + private DoctrineService $doctrineService, + private SiteRepository $siteRepository, + private DomainRepository $domainRepository, + private AssetRepository $assetRepository, + private ResourceRepository $resourceRepository, + private ResourceManager $resourceManager, + private PersistenceManagerInterface $persistenceManager, + private WorkspaceService $workspaceService, + private ProjectionCatchupProcessorFactory $projectionCatchupServiceFactory, ) { } diff --git a/Neos.Neos/Classes/Domain/Service/SitePruningService.php b/Neos.Neos/Classes/Domain/Service/SitePruningService.php index fc5069768d1..0c38ff399b6 100644 --- a/Neos.Neos/Classes/Domain/Service/SitePruningService.php +++ b/Neos.Neos/Classes/Domain/Service/SitePruningService.php @@ -25,9 +25,10 @@ use Neos\ContentRepository\Export\Processors\AssetRepositoryImportProcessor; use Neos\ContentRepository\Export\Severity; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; -use Neos\ContentRepositoryRegistry\Service\ProjectionCatchupService; -use Neos\ContentRepositoryRegistry\Service\ProjectionCatchupServiceFactory; -use Neos\ContentRepositoryRegistry\Service\ProjectionReplayServiceFactory; +use Neos\ContentRepositoryRegistry\Processors\ProjectionCatchupProcessor; +use Neos\ContentRepositoryRegistry\Processors\ProjectionCatchupProcessorFactory; +use Neos\ContentRepositoryRegistry\Processors\ProjectionReplayProcessorFactory; +use Neos\ContentRepositoryRegistry\Service\ProjectionServiceFactory; use Neos\Flow\Annotations as Flow; use Neos\Flow\Persistence\Doctrine\Service as DoctrineService; use Neos\Flow\Persistence\PersistenceManagerInterface; @@ -48,12 +49,12 @@ final readonly class SitePruningService { public function __construct( - private ContentRepositoryRegistry $contentRepositoryRegistry, - private SiteRepository $siteRepository, - private DomainRepository $domainRepository, - private PersistenceManagerInterface $persistenceManager, - private ProjectionReplayServiceFactory $projectionReplayServiceFactory, - private WorkspaceService $workspaceService, + private ContentRepositoryRegistry $contentRepositoryRegistry, + private SiteRepository $siteRepository, + private DomainRepository $domainRepository, + private PersistenceManagerInterface $persistenceManager, + private ProjectionReplayProcessorFactory $projectionReplayServiceFactory, + private WorkspaceService $workspaceService, ) { } @@ -63,7 +64,7 @@ public function __construct( */ public function pruneAll(ContentRepositoryId $contentRepositoryId, \Closure $onProcessor, \Closure $onMessage): void { - $filesystem = new Filesystem( new LocalFilesystemAdapter('.')); + $filesystem = new Filesystem(new LocalFilesystemAdapter('.')); $context = new ProcessingContext($filesystem, $onMessage); // TODO make configurable (?) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index ebbd2ba29a9..12d96178fbb 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -3,7 +3,7 @@ parameters: - message: "#^The internal method \"Neos\\\\ContentRepository\\\\Core\\\\Projection\\\\Projections\\:\\:getClassNames\" is called\\.$#" count: 1 - path: Neos.ContentRepositoryRegistry/Classes/Service/ProjectionReplayService.php + path: Neos.ContentRepositoryRegistry/Classes/Service/ProjectionService.php - message: "#^Method Neos\\\\Neos\\\\Controller\\\\Backend\\\\MenuHelper\\:\\:buildModuleList\\(\\) return type has no value type specified in iterable type array\\.$#" From bd1ff93b0fdf4f8a8c13f93856b66f31ac1d8b1d Mon Sep 17 00:00:00 2001 From: Martin Ficzel Date: Thu, 24 Oct 2024 14:46:43 +0200 Subject: [PATCH 036/214] Refactor migration of legacyData and add additional command `cr:exportLegacyData` alongside `cr:migrateLegacyData` --- .../Classes/Command/CrCommandController.php | 159 ++++++++++-------- .../Classes/Helpers/DomainDataLoader.php | 33 ++++ .../Classes/Helpers/SiteDataLoader.php | 33 ++++ .../Classes/LegacyExportService.php | 74 ++++++++ ...ory.php => LegacyExportServiceFactory.php} | 25 +-- .../Classes/LegacyMigrationService.php | 94 ----------- .../AssetExportProcessor.php} | 4 +- .../EventExportProcessor.php} | 42 +---- .../Processors/SitesExportProcessor.php | 66 ++++++++ .../Behavior/Bootstrap/FeatureContext.php | 8 +- .../composer.json | 2 +- .../Domain/Service/SiteImportService.php | 22 +-- .../Domain/Service/SitePruningService.php | 14 +- 13 files changed, 332 insertions(+), 244 deletions(-) create mode 100644 Neos.ContentRepository.LegacyNodeMigration/Classes/Helpers/DomainDataLoader.php create mode 100644 Neos.ContentRepository.LegacyNodeMigration/Classes/Helpers/SiteDataLoader.php create mode 100644 Neos.ContentRepository.LegacyNodeMigration/Classes/LegacyExportService.php rename Neos.ContentRepository.LegacyNodeMigration/Classes/{LegacyMigrationServiceFactory.php => LegacyExportServiceFactory.php} (56%) delete mode 100644 Neos.ContentRepository.LegacyNodeMigration/Classes/LegacyMigrationService.php rename Neos.ContentRepository.LegacyNodeMigration/Classes/{NodeDataToAssetsProcessor.php => Processors/AssetExportProcessor.php} (97%) rename Neos.ContentRepository.LegacyNodeMigration/Classes/{NodeDataToEventsProcessor.php => Processors/EventExportProcessor.php} (94%) create mode 100644 Neos.ContentRepository.LegacyNodeMigration/Classes/Processors/SitesExportProcessor.php diff --git a/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/CrCommandController.php b/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/CrCommandController.php index 90b0fa78049..3c35ce735d5 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/CrCommandController.php +++ b/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/CrCommandController.php @@ -18,36 +18,26 @@ use Doctrine\DBAL\DriverManager; use Doctrine\DBAL\Exception as DBALException; use Doctrine\DBAL\Exception\ConnectionException; -use Neos\ContentRepository\Core\Projection\CatchUpOptions; -use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; +use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; +use Neos\ContentRepository\Export\Severity; +use Neos\ContentRepository\LegacyNodeMigration\LegacyExportServiceFactory; use Neos\ContentRepository\LegacyNodeMigration\LegacyMigrationService; use Neos\ContentRepository\LegacyNodeMigration\LegacyMigrationServiceFactory; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; -use Neos\ContentRepositoryRegistry\Factory\EventStore\DoctrineEventStoreFactory; -use Neos\ContentRepositoryRegistry\Service\ProjectionServiceFactory; use Neos\Flow\Cli\CommandController; -use Neos\Flow\Persistence\PersistenceManagerInterface; use Neos\Flow\Property\PropertyMapper; -use Neos\Flow\ResourceManagement\ResourceManager; -use Neos\Flow\ResourceManagement\ResourceRepository; use Neos\Flow\Utility\Environment; -use Neos\Media\Domain\Repository\AssetRepository; -use Neos\Neos\Domain\Model\Site; -use Neos\Neos\Domain\Repository\SiteRepository; +use Neos\Neos\Domain\Service\SiteImportService; +use Neos\Utility\Files; class CrCommandController extends CommandController { public function __construct( private readonly Connection $connection, private readonly Environment $environment, - private readonly PersistenceManagerInterface $persistenceManager, - private readonly AssetRepository $assetRepository, - private readonly ResourceRepository $resourceRepository, - private readonly ResourceManager $resourceManager, private readonly PropertyMapper $propertyMapper, private readonly ContentRepositoryRegistry $contentRepositoryRegistry, - private readonly SiteRepository $siteRepository, - private readonly ProjectionServiceFactory $projectionServiceFactory, + private readonly SiteImportService $siteImportService, ) { parent::__construct(); } @@ -55,11 +45,10 @@ public function __construct( /** * Migrate from the Legacy CR * - * @param bool $verbose If set, all notices will be rendered * @param string|null $config JSON encoded configuration, for example '{"dbal": {"dbname": "some-other-db"}, "resourcesPath": "/some/absolute/path"}' * @throws \Exception */ - public function migrateLegacyDataCommand(bool $verbose = false, string $config = null): void + public function migrateLegacyDataCommand(string $contentRepository = 'default', bool $verbose = false, string $config = null): void { if ($config !== null) { try { @@ -83,68 +72,84 @@ public function migrateLegacyDataCommand(bool $verbose = false, string $config = } $this->verifyDatabaseConnection($connection); + $contentRepositoryId = ContentRepositoryId::fromString($contentRepository); + $temporaryFilePath = $this->environment->getPathToTemporaryDirectory() . uniqid('Export', true); + Files::createDirectoryRecursively($temporaryFilePath); - $siteRows = $connection->fetchAllAssociativeIndexed('SELECT nodename, name, siteresourcespackagekey FROM neos_neos_domain_model_site'); - $siteNodeName = $this->output->select('Which site to migrate?', array_map(static fn (array $siteRow) => $siteRow['name'] . ' (' . $siteRow['siteresourcespackagekey'] . ')', $siteRows)); - assert(is_string($siteNodeName)); - $siteRow = $siteRows[$siteNodeName]; + $legacyExportService = $this->contentRepositoryRegistry->buildService( + $contentRepositoryId, + new LegacyExportServiceFactory( + $connection, + $resourcesPath, + $this->propertyMapper, + ) + ); - $site = $this->siteRepository->findOneByNodeName($siteNodeName); - if ($site !== null) { - if (!$this->output->askConfirmation(sprintf('Site "%s" already exists, update it? [n] ', $siteNodeName), false)) { - $this->outputLine('Cancelled...'); - $this->quit(); - } + $legacyExportService->exportToPath( + $temporaryFilePath, + $this->createOnProcessorClosure(), + $this->createOnMessageClosure($verbose) + ); - $site->setSiteResourcesPackageKey($siteRow['siteresourcespackagekey']); - $site->setState(Site::STATE_ONLINE); - $site->setName($siteRow['name']); - $this->siteRepository->update($site); - $this->persistenceManager->persistAll(); - } else { - $site = new Site($siteNodeName); - $site->setSiteResourcesPackageKey($siteRow['siteresourcespackagekey']); - $site->setState(Site::STATE_ONLINE); - $site->setName($siteRow['name']); - $this->siteRepository->add($site); - $this->persistenceManager->persistAll(); - } + $this->siteImportService->importFromPath( + $contentRepositoryId, + $temporaryFilePath, + $this->createOnProcessorClosure(), + $this->createOnMessageClosure($verbose) + ); + + Files::unlink($temporaryFilePath); - $contentRepositoryId = $site->getConfiguration()->contentRepositoryId; + $this->outputLine('Done'); + } - $eventTableName = DoctrineEventStoreFactory::databaseTableName($contentRepositoryId); - $confirmed = $this->output->askConfirmation(sprintf('We will clear the events from "%s". ARE YOU SURE [n]? ', $eventTableName), false); - if (!$confirmed) { - $this->outputLine('Cancelled...'); - $this->quit(); + /** + * Export from the Legacy CR into a specified directory path + * + * @param string $path The path to the directory, will be created if missing + * @param string|null $config JSON encoded configuration, for example '{"dbal": {"dbname": "some-other-db"}, "resourcesPath": "/some/absolute/path"}' + * @throws \Exception + */ + public function exportLegacyDataCommand(string $path, bool $verbose = false, string $config = null): void + { + if ($config !== null) { + try { + $parsedConfig = json_decode($config, true, 512, JSON_THROW_ON_ERROR); + } catch (\JsonException $e) { + throw new \InvalidArgumentException(sprintf('Failed to parse --config parameter: %s', $e->getMessage()), 1659526855, $e); + } + $resourcesPath = $parsedConfig['resourcesPath'] ?? self::defaultResourcesPath(); + try { + $connection = isset($parsedConfig['dbal']) ? DriverManager::getConnection(array_merge($this->connection->getParams(), $parsedConfig['dbal']), new Configuration()) : $this->connection; + } catch (DBALException $e) { + throw new \InvalidArgumentException(sprintf('Failed to get database connection, check the --config parameter: %s', $e->getMessage()), 1659527201, $e); + } + } else { + $resourcesPath = $this->determineResourcesPath(); + if (!$this->output->askConfirmation(sprintf('Do you want to migrate nodes from the current database "%s@%s" (y/n)? ', $this->connection->getParams()['dbname'] ?? '?', $this->connection->getParams()['host'] ?? '?'))) { + $connection = $this->adjustDataBaseConnection($this->connection); + } else { + $connection = $this->connection; + } } - $this->connection->executeStatement('TRUNCATE ' . $connection->quoteIdentifier($eventTableName)); - // we also need to reset the projections; in order to ensure the system runs deterministically - $projectionService = $this->contentRepositoryRegistry->buildService($contentRepositoryId, $this->projectionServiceFactory); - $projectionService->resetAllProjections(); - $this->outputLine('Truncated events'); + $this->verifyDatabaseConnection($connection); - $legacyMigrationService = $this->contentRepositoryRegistry->buildService( - $contentRepositoryId, - new LegacyMigrationServiceFactory( + Files::createDirectoryRecursively($path); + $legacyExportService = $this->contentRepositoryRegistry->buildService( + ContentRepositoryId::fromString('default'), + new LegacyExportServiceFactory( $connection, $resourcesPath, - $this->environment, - $this->persistenceManager, - $this->assetRepository, - $this->resourceRepository, - $this->resourceManager, $this->propertyMapper, ) ); - assert($legacyMigrationService instanceof LegacyMigrationService); - $legacyMigrationService->runAllProcessors($this->outputLine(...), $verbose); + $legacyExportService->exportToPath( + $path, + $this->createOnProcessorClosure(), + $this->createOnMessageClosure($verbose) + ); - $this->outputLine(); - - $this->outputLine('Replaying projections'); - $projectionService->replayAllProjections(CatchUpOptions::create()); $this->outputLine('Done'); } @@ -199,4 +204,26 @@ private static function defaultResourcesPath(): string { return FLOW_PATH_DATA . 'Persistent/Resources'; } + + protected function createOnProcessorClosure(): \Closure + { + $onProcessor = function (string $processorLabel) { + $this->outputLine('%s...', [$processorLabel]); + }; + return $onProcessor; + } + + protected function createOnMessageClosure(bool $verbose): \Closure + { + return function (Severity $severity, string $message) use ($verbose) { + if (!$verbose && $severity === Severity::NOTICE) { + return; + } + $this->outputLine(match ($severity) { + Severity::NOTICE => $message, + Severity::WARNING => sprintf('Warning: %s', $message), + Severity::ERROR => sprintf('Error: %s', $message), + }); + }; + } } diff --git a/Neos.ContentRepository.LegacyNodeMigration/Classes/Helpers/DomainDataLoader.php b/Neos.ContentRepository.LegacyNodeMigration/Classes/Helpers/DomainDataLoader.php new file mode 100644 index 00000000000..beb74a87fee --- /dev/null +++ b/Neos.ContentRepository.LegacyNodeMigration/Classes/Helpers/DomainDataLoader.php @@ -0,0 +1,33 @@ +> + */ +final class DomainDataLoader implements \IteratorAggregate +{ + public function __construct( + private readonly Connection $connection, + ) { + } + + /** + * @return \Traversable> + */ + public function getIterator(): \Traversable + { + $query = $this->connection->executeQuery(' + SELECT + * + FROM + neos_neos_domain_model_domain + '); + return $query->iterateAssociative(); + } +} + + diff --git a/Neos.ContentRepository.LegacyNodeMigration/Classes/Helpers/SiteDataLoader.php b/Neos.ContentRepository.LegacyNodeMigration/Classes/Helpers/SiteDataLoader.php new file mode 100644 index 00000000000..2d878f9302f --- /dev/null +++ b/Neos.ContentRepository.LegacyNodeMigration/Classes/Helpers/SiteDataLoader.php @@ -0,0 +1,33 @@ +> + */ +final class SiteDataLoader implements \IteratorAggregate +{ + public function __construct( + private readonly Connection $connection, + ) { + } + + /** + * @return \Traversable> + */ + public function getIterator(): \Traversable + { + $query = $this->connection->executeQuery(' + SELECT + * + FROM + neos_neos_domain_model_site + '); + return $query->iterateAssociative(); + } +} + + diff --git a/Neos.ContentRepository.LegacyNodeMigration/Classes/LegacyExportService.php b/Neos.ContentRepository.LegacyNodeMigration/Classes/LegacyExportService.php new file mode 100644 index 00000000000..451cb162d86 --- /dev/null +++ b/Neos.ContentRepository.LegacyNodeMigration/Classes/LegacyExportService.php @@ -0,0 +1,74 @@ +connection), new FileSystemResourceLoader($this->resourcesPath)); + + $processors = Processors::fromArray([ + 'Exporting assets' => new AssetExportProcessor($this->nodeTypeManager, $assetExporter, new NodeDataLoader($this->connection)), + 'Exporting node data' => new EventExportProcessor($this->nodeTypeManager, $this->propertyMapper, $this->propertyConverter, $this->interDimensionalVariationGraph, $this->eventNormalizer, new NodeDataLoader($this->connection)), + 'Exporting sites data' => new SitesExportProcessor(new SiteDataLoader($this->connection), new DomainDataLoader($this->connection)), + ]); + + $processingContext = new ProcessingContext($filesystem, $onMessage); + foreach ($processors as $processorLabel => $processor) { + ($onProcessor)($processorLabel); + $processor->run($processingContext); + } + } +} diff --git a/Neos.ContentRepository.LegacyNodeMigration/Classes/LegacyMigrationServiceFactory.php b/Neos.ContentRepository.LegacyNodeMigration/Classes/LegacyExportServiceFactory.php similarity index 56% rename from Neos.ContentRepository.LegacyNodeMigration/Classes/LegacyMigrationServiceFactory.php rename to Neos.ContentRepository.LegacyNodeMigration/Classes/LegacyExportServiceFactory.php index 80ab8a5f84a..8497f3ff376 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Classes/LegacyMigrationServiceFactory.php +++ b/Neos.ContentRepository.LegacyNodeMigration/Classes/LegacyExportServiceFactory.php @@ -14,52 +14,35 @@ * source code. */ - use Doctrine\DBAL\Connection; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceFactoryDependencies; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceFactoryInterface; -use Neos\Flow\Persistence\PersistenceManagerInterface; use Neos\Flow\Property\PropertyMapper; -use Neos\Flow\ResourceManagement\ResourceManager; -use Neos\Flow\ResourceManagement\ResourceRepository; use Neos\Flow\Utility\Environment; -use Neos\Media\Domain\Repository\AssetRepository; /** - * @implements ContentRepositoryServiceFactoryInterface + * @implements ContentRepositoryServiceFactoryInterface */ -class LegacyMigrationServiceFactory implements ContentRepositoryServiceFactoryInterface +class LegacyExportServiceFactory implements ContentRepositoryServiceFactoryInterface { public function __construct( private readonly Connection $connection, private readonly string $resourcesPath, - private readonly Environment $environment, - private readonly PersistenceManagerInterface $persistenceManager, - private readonly AssetRepository $assetRepository, - private readonly ResourceRepository $resourceRepository, - private readonly ResourceManager $resourceManager, private readonly PropertyMapper $propertyMapper, ) { } public function build( ContentRepositoryServiceFactoryDependencies $serviceFactoryDependencies - ): LegacyMigrationService { - return new LegacyMigrationService( + ): LegacyExportService { + return new LegacyExportService( $this->connection, $this->resourcesPath, - $this->environment, - $this->persistenceManager, - $this->assetRepository, - $this->resourceRepository, - $this->resourceManager, $serviceFactoryDependencies->interDimensionalVariationGraph, $serviceFactoryDependencies->nodeTypeManager, $this->propertyMapper, $serviceFactoryDependencies->eventNormalizer, $serviceFactoryDependencies->propertyConverter, - $serviceFactoryDependencies->eventStore, - $serviceFactoryDependencies->contentRepository, ); } } diff --git a/Neos.ContentRepository.LegacyNodeMigration/Classes/LegacyMigrationService.php b/Neos.ContentRepository.LegacyNodeMigration/Classes/LegacyMigrationService.php deleted file mode 100644 index d057db43fb9..00000000000 --- a/Neos.ContentRepository.LegacyNodeMigration/Classes/LegacyMigrationService.php +++ /dev/null @@ -1,94 +0,0 @@ -environment->getPathToTemporaryDirectory() . uniqid('Export', true); - Files::createDirectoryRecursively($temporaryFilePath); - $filesystem = new Filesystem(new LocalFilesystemAdapter($temporaryFilePath)); - - $assetExporter = new AssetExporter($filesystem, new DbalAssetLoader($this->connection), new FileSystemResourceLoader($this->resourcesPath)); - - $processors = Processors::fromArray([ - 'Exporting assets' => new NodeDataToAssetsProcessor($this->nodeTypeManager, $assetExporter, new NodeDataLoader($this->connection)), - 'Exporting node data' => new NodeDataToEventsProcessor($this->nodeTypeManager, $this->propertyMapper, $this->propertyConverter, $this->interDimensionalVariationGraph, $this->eventNormalizer, new NodeDataLoader($this->connection)), - 'Importing assets' => new AssetRepositoryImportProcessor($this->assetRepository, $this->resourceRepository, $this->resourceManager, $this->persistenceManager), - 'Importing events' => new EventStoreImportProcessor(WorkspaceName::forLive(), true, $this->eventStore, $this->eventNormalizer, $this->contentRepository), - ]); - $processingContext = new ProcessingContext($filesystem, function (Severity $severity, string $message) use ($verbose, $outputLineFn) { - if ($severity !== Severity::NOTICE || $verbose) { - $outputLineFn('<%1$s>%2$s', [$severity === Severity::ERROR ? 'error' : 'comment', $message]); - } - }); - foreach ($processors as $label => $processor) { - $outputLineFn($label . '...'); - $processor->run($processingContext); - $outputLineFn(); - } - Files::unlink($temporaryFilePath); - } -} diff --git a/Neos.ContentRepository.LegacyNodeMigration/Classes/NodeDataToAssetsProcessor.php b/Neos.ContentRepository.LegacyNodeMigration/Classes/Processors/AssetExportProcessor.php similarity index 97% rename from Neos.ContentRepository.LegacyNodeMigration/Classes/NodeDataToAssetsProcessor.php rename to Neos.ContentRepository.LegacyNodeMigration/Classes/Processors/AssetExportProcessor.php index 6bde0f844e5..57794d129eb 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Classes/NodeDataToAssetsProcessor.php +++ b/Neos.ContentRepository.LegacyNodeMigration/Classes/Processors/AssetExportProcessor.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Neos\ContentRepository\LegacyNodeMigration; +namespace Neos\ContentRepository\LegacyNodeMigration\Processors; use Neos\ContentRepository\Core\NodeType\NodeTypeManager; use Neos\ContentRepository\Core\NodeType\NodeTypeName; @@ -14,7 +14,7 @@ use Neos\Utility\Exception\InvalidTypeException; use Neos\Utility\TypeHandling; -final class NodeDataToAssetsProcessor implements ProcessorInterface +final class AssetExportProcessor implements ProcessorInterface { /** * @var array diff --git a/Neos.ContentRepository.LegacyNodeMigration/Classes/NodeDataToEventsProcessor.php b/Neos.ContentRepository.LegacyNodeMigration/Classes/Processors/EventExportProcessor.php similarity index 94% rename from Neos.ContentRepository.LegacyNodeMigration/Classes/NodeDataToEventsProcessor.php rename to Neos.ContentRepository.LegacyNodeMigration/Classes/Processors/EventExportProcessor.php index 48ea7dd0bdb..10e083b3822 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Classes/NodeDataToEventsProcessor.php +++ b/Neos.ContentRepository.LegacyNodeMigration/Classes/Processors/EventExportProcessor.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Neos\ContentRepository\LegacyNodeMigration; +namespace Neos\ContentRepository\LegacyNodeMigration\Processors; use Doctrine\DBAL\Platforms\PostgreSQLPlatform; use Doctrine\DBAL\Types\ConversionException; @@ -54,7 +54,7 @@ use Neos\Neos\Domain\Service\NodeTypeNameFactory; use Webmozart\Assert\Assert; -final class NodeDataToEventsProcessor implements ProcessorInterface +final class EventExportProcessor implements ProcessorInterface { private NodeTypeName $sitesNodeTypeName; private WorkspaceName $workspaceName; @@ -68,8 +68,6 @@ final class NodeDataToEventsProcessor implements ProcessorInterface private int $numberOfExportedEvents = 0; - private bool $metaDataExported = false; - /** * @var resource|null */ @@ -97,18 +95,6 @@ public function setContentStreamId(ContentStreamId $contentStreamId): void $this->contentStreamId = $contentStreamId; } - public function setSitesNodeType(NodeTypeName $nodeTypeName): void - { - $nodeType = $this->nodeTypeManager->getNodeType($nodeTypeName); - if (!$nodeType?->isOfType(NodeTypeNameFactory::NAME_SITES)) { - throw new \InvalidArgumentException( - sprintf('Sites NodeType "%s" must be of type "%s"', $nodeTypeName->value, NodeTypeNameFactory::NAME_SITES), - 1695802415 - ); - } - $this->sitesNodeTypeName = $nodeTypeName; - } - public function run(ProcessingContext $context): void { $this->resetRuntimeState(); @@ -120,10 +106,6 @@ public function run(ProcessingContext $context): void $this->exportEvent(new RootNodeAggregateWithNodeWasCreated($this->workspaceName, $this->contentStreamId, $sitesNodeAggregateId, $this->sitesNodeTypeName, $this->interDimensionalVariationGraph->getDimensionSpacePoints(), NodeAggregateClassification::CLASSIFICATION_ROOT)); continue; } - if ($this->metaDataExported === false && $nodeDataRow['parentpath'] === '/sites') { - $this->exportMetaData($context, $nodeDataRow); - $this->metaDataExported = true; - } try { $this->processNodeData($context, $nodeDataRow); } catch (MigrationException $e) { @@ -149,7 +131,6 @@ private function resetRuntimeState(): void $this->visitedNodes = new VisitedNodeAggregates(); $this->nodeReferencesWereSetEvents = []; $this->numberOfExportedEvents = 0; - $this->metaDataExported = false; $this->eventFileResource = fopen('php://temp/maxmemory:5242880', 'rb+') ?: null; Assert::resource($this->eventFileResource, null, 'Failed to create temporary event file resource'); } @@ -162,6 +143,8 @@ private function exportEvent(EventInterface $event): void } catch (\JsonException $e) { throw new \RuntimeException(sprintf('Failed to JSON-decode "%s": %s', $normalizedEvent->data->value, $e->getMessage()), 1723032243, $e); } + // do not export crid and workspace as they are always imported into a single workspace + unset($exportedEventPayload['contentStreamId'], $exportedEventPayload['workspaceName']); $exportedEvent = new ExportedEvent( $normalizedEvent->id->value, $normalizedEvent->type->value, @@ -173,23 +156,6 @@ private function exportEvent(EventInterface $event): void $this->numberOfExportedEvents++; } - /** - * @param array $nodeDataRow - */ - private function exportMetaData(ProcessingContext $context, array $nodeDataRow): void - { - if ($context->files->fileExists('meta.json')) { - $data = json_decode($context->files->read('meta.json'), true, 512, JSON_THROW_ON_ERROR); - } else { - $data = []; - } - $data['version'] = 1; - $data['sitePackageKey'] = strtok($nodeDataRow['nodetype'], ':'); - $data['siteNodeName'] = substr($nodeDataRow['path'], 7); - $data['siteNodeType'] = $nodeDataRow['nodetype']; - $context->files->write('meta.json', json_encode($data, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT)); - } - /** * @param array $nodeDataRow */ diff --git a/Neos.ContentRepository.LegacyNodeMigration/Classes/Processors/SitesExportProcessor.php b/Neos.ContentRepository.LegacyNodeMigration/Classes/Processors/SitesExportProcessor.php new file mode 100644 index 00000000000..d8b7fe6b3f7 --- /dev/null +++ b/Neos.ContentRepository.LegacyNodeMigration/Classes/Processors/SitesExportProcessor.php @@ -0,0 +1,66 @@ +> $siteRows + * @param iterable> $domainRows + */ + public function __construct( + private readonly iterable $siteRows, + private readonly iterable $domainRows + ) { + } + + public function run(ProcessingContext $context): void + { + $sitesData = $this->getSiteData(); + $context->files->write('sites.json', json_encode($sitesData, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT)); + } + + /** + * @return SiteShape[] + */ + private function getSiteData(): array + { + $siteData = []; + foreach ($this->siteRows as $siteRow) { + $siteData[] = [ + "name" => $siteRow['name'], + "nodeName" => $siteRow['nodename'], + "siteResourcesPackageKey" => $siteRow['siteresourcespackagekey'], + "online" => $siteRow['state'] === 1, + "domains" => array_filter( + array_map( + function(array $domainRow) use ($siteRow) { + if ($siteRow['persistence_object_identifier'] !== $domainRow['site']) { + return null; + } + return [ + 'hostname' => $domainRow['hostname'], + 'scheme' => $domainRow['scheme'], + 'port' => $domainRow['port'], + 'active' => $domainRow['active'], + 'primary' => $domainRow === $siteRow['primarydomain'], + ]; + }, + iterator_to_array($this->domainRows) + ) + ) + ]; + } + + return $siteData; + } +} diff --git a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Bootstrap/FeatureContext.php b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Bootstrap/FeatureContext.php index 7a506466d0c..188d6b53731 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Bootstrap/FeatureContext.php +++ b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Bootstrap/FeatureContext.php @@ -26,8 +26,8 @@ use Neos\ContentRepository\Export\Asset\ValueObject\SerializedAsset; use Neos\ContentRepository\Export\Asset\ValueObject\SerializedImageVariant; use Neos\ContentRepository\Export\Asset\ValueObject\SerializedResource; -use Neos\ContentRepository\LegacyNodeMigration\NodeDataToAssetsProcessor; -use Neos\ContentRepository\LegacyNodeMigration\NodeDataToEventsProcessor; +use Neos\ContentRepository\LegacyNodeMigration\Processors\AssetExportProcessor; +use Neos\ContentRepository\LegacyNodeMigration\SitesToSitesProcessor; use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\CRTestSuiteTrait; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Property\PropertyMapper; @@ -106,7 +106,7 @@ public function build(ContentRepositoryServiceFactoryDependencies $serviceFactor }; $this->getContentRepositoryService($propertyConverterAccess); - $migration = new NodeDataToEventsProcessor( + $migration = new SitesToSitesProcessor( $nodeTypeManager, $propertyMapper, $propertyConverterAccess->propertyConverter, @@ -202,7 +202,7 @@ public function findAssetById(string $assetId): SerializedAsset|SerializedImageV }; $assetExporter = new AssetExporter($this->crImportExportTrait_filesystem, $mockAssetLoader, $mockResourceLoader); - $migration = new NodeDataToAssetsProcessor($nodeTypeManager, $assetExporter, $this->nodeDataRows); + $migration = new AssetExportProcessor($nodeTypeManager, $assetExporter, $this->nodeDataRows); $this->runCrImportExportProcessors($migration); } diff --git a/Neos.ContentRepository.LegacyNodeMigration/composer.json b/Neos.ContentRepository.LegacyNodeMigration/composer.json index f88ba50c268..e6a0f1c067f 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/composer.json +++ b/Neos.ContentRepository.LegacyNodeMigration/composer.json @@ -12,7 +12,7 @@ ], "require": { "php": ">=8.2", - "neoe/neos": "self.version", + "neos/neos": "self.version", "neos/contentrepository-core": "self.version", "neos/contentrepository-export": "self.version", "league/flysystem": "^3" diff --git a/Neos.Neos/Classes/Domain/Service/SiteImportService.php b/Neos.Neos/Classes/Domain/Service/SiteImportService.php index 7b8ba4503ba..67c2e457d19 100644 --- a/Neos.Neos/Classes/Domain/Service/SiteImportService.php +++ b/Neos.Neos/Classes/Domain/Service/SiteImportService.php @@ -45,16 +45,16 @@ final readonly class SiteImportService { public function __construct( - private ContentRepositoryRegistry $contentRepositoryRegistry, - private DoctrineService $doctrineService, - private SiteRepository $siteRepository, - private DomainRepository $domainRepository, - private AssetRepository $assetRepository, - private ResourceRepository $resourceRepository, - private ResourceManager $resourceManager, - private PersistenceManagerInterface $persistenceManager, - private WorkspaceService $workspaceService, - private ProjectionCatchupProcessorFactory $projectionCatchupServiceFactory, + private ContentRepositoryRegistry $contentRepositoryRegistry, + private DoctrineService $doctrineService, + private SiteRepository $siteRepository, + private DomainRepository $domainRepository, + private AssetRepository $assetRepository, + private ResourceRepository $resourceRepository, + private ResourceManager $resourceManager, + private PersistenceManagerInterface $persistenceManager, + private WorkspaceService $workspaceService, + private ProjectionCatchupProcessorFactory $projectionCatchupProcessorFactory, ) { } @@ -81,7 +81,7 @@ public function importFromPath(ContentRepositoryId $contentRepositoryId, string 'Create Live workspace' => new LiveWorkspaceCreationProcessor($contentRepository, $this->workspaceService), 'Import events' => $this->contentRepositoryRegistry->buildService($contentRepositoryId, new EventStoreImportProcessorFactory(WorkspaceName::forLive(), keepEventIds: true)), 'Import assets' => new AssetRepositoryImportProcessor($this->assetRepository, $this->resourceRepository, $this->resourceManager, $this->persistenceManager), - 'Catchup all projections' => $this->contentRepositoryRegistry->buildService($contentRepositoryId, $this->projectionCatchupServiceFactory), + 'Catchup all projections' => $this->contentRepositoryRegistry->buildService($contentRepositoryId, $this->projectionCatchupProcessorFactory), ]; foreach ($processors as $processorLabel => $processor) { diff --git a/Neos.Neos/Classes/Domain/Service/SitePruningService.php b/Neos.Neos/Classes/Domain/Service/SitePruningService.php index 0c38ff399b6..bb082eff395 100644 --- a/Neos.Neos/Classes/Domain/Service/SitePruningService.php +++ b/Neos.Neos/Classes/Domain/Service/SitePruningService.php @@ -49,12 +49,12 @@ final readonly class SitePruningService { public function __construct( - private ContentRepositoryRegistry $contentRepositoryRegistry, - private SiteRepository $siteRepository, - private DomainRepository $domainRepository, - private PersistenceManagerInterface $persistenceManager, - private ProjectionReplayProcessorFactory $projectionReplayServiceFactory, - private WorkspaceService $workspaceService, + private ContentRepositoryRegistry $contentRepositoryRegistry, + private SiteRepository $siteRepository, + private DomainRepository $domainRepository, + private PersistenceManagerInterface $persistenceManager, + private ProjectionReplayProcessorFactory $projectionReplayProcessorFactory, + private WorkspaceService $workspaceService, ) { } @@ -89,7 +89,7 @@ public function pruneAll(ContentRepositoryId $contentRepositoryId, \Closure $onP $this->workspaceService ) ), - 'Replay all projections' => $this->contentRepositoryRegistry->buildService($contentRepositoryId, $this->projectionReplayServiceFactory), + 'Replay all projections' => $this->contentRepositoryRegistry->buildService($contentRepositoryId, $this->projectionReplayProcessorFactory), ]; foreach ($processors as $processorLabel => $processor) { From c0536d305bfacc318183243f7a60aa1a12d11b21 Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Thu, 24 Oct 2024 16:30:07 +0200 Subject: [PATCH 037/214] Tweak `AuthProvideInterface` method names and add doc comments --- .../Classes/ContentRepository.php | 7 +++++-- .../Classes/Feature/Security/AuthProviderInterface.php | 6 ++++-- .../Classes/Feature/Security/StaticAuthProvider.php | 4 ++-- .../Projection/ContentGraph/ContentGraphInterface.php | 3 ++- .../Features/Bootstrap/Helpers/FakeAuthProvider.php | 4 ++-- Neos.Neos/Classes/Controller/Frontend/NodeController.php | 7 ++++--- .../ContentRepositoryAuthorizationService.php | 2 ++ .../ContentRepositoryAuthProvider.php | 9 ++++++--- Neos.Neos/Configuration/Policy.yaml | 2 ++ 9 files changed, 29 insertions(+), 15 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/ContentRepository.php b/Neos.ContentRepository.Core/Classes/ContentRepository.php index e3eb1b81dec..e698ba424f7 100644 --- a/Neos.ContentRepository.Core/Classes/ContentRepository.php +++ b/Neos.ContentRepository.Core/Classes/ContentRepository.php @@ -104,7 +104,7 @@ public function __construct( */ public function handle(CommandInterface $command): void { - $privilege = $this->authProvider->getCommandPrivilege($command); + $privilege = $this->authProvider->canExecuteCommand($command); if (!$privilege->granted) { throw AccessDenied::becauseCommandIsNotGranted($command, $privilege->reason); } @@ -266,7 +266,7 @@ public function resetProjectionState(string $projectionClassName): void */ public function getContentGraph(WorkspaceName $workspaceName): ContentGraphInterface { - $privilege = $this->authProvider->getReadNodesFromWorkspacePrivilege($workspaceName); + $privilege = $this->authProvider->canReadNodesFromWorkspace($workspaceName); if (!$privilege->granted) { throw AccessDenied::becauseWorkspaceCantBeRead($workspaceName, $privilege->reason); } @@ -278,6 +278,9 @@ public function getContentGraph(WorkspaceName $workspaceName): ContentGraphInter } /** + * Main API to retrieve a content subgraph, taking VisibilityConstraints of the current user + * into account ({@see AuthProviderInterface::getVisibilityConstraints()}) + * * @throws WorkspaceDoesNotExist if the workspace does not exist * @throws AccessDenied if no read access is granted to the workspace ({@see AuthProviderInterface}) */ diff --git a/Neos.ContentRepository.Core/Classes/Feature/Security/AuthProviderInterface.php b/Neos.ContentRepository.Core/Classes/Feature/Security/AuthProviderInterface.php index 2d2bca17bfc..e6f8524e3e1 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/Security/AuthProviderInterface.php +++ b/Neos.ContentRepository.Core/Classes/Feature/Security/AuthProviderInterface.php @@ -11,15 +11,17 @@ use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; /** + * Provides authorization decisions for the current user, for one Content Repository. + * * @internal except for CR factory implementations */ interface AuthProviderInterface { public function getAuthenticatedUserId(): ?UserId; - public function getReadNodesFromWorkspacePrivilege(WorkspaceName $workspaceName): Privilege; + public function canReadNodesFromWorkspace(WorkspaceName $workspaceName): Privilege; public function getVisibilityConstraints(WorkspaceName $workspaceName): VisibilityConstraints; - public function getCommandPrivilege(CommandInterface $command): Privilege; + public function canExecuteCommand(CommandInterface $command): Privilege; } diff --git a/Neos.ContentRepository.Core/Classes/Feature/Security/StaticAuthProvider.php b/Neos.ContentRepository.Core/Classes/Feature/Security/StaticAuthProvider.php index 5c5b72e1223..2d44bcf63fe 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/Security/StaticAuthProvider.php +++ b/Neos.ContentRepository.Core/Classes/Feature/Security/StaticAuthProvider.php @@ -32,12 +32,12 @@ public function getVisibilityConstraints(WorkspaceName $workspaceName): Visibili return VisibilityConstraints::default(); } - public function getReadNodesFromWorkspacePrivilege(WorkspaceName $workspaceName): Privilege + public function canReadNodesFromWorkspace(WorkspaceName $workspaceName): Privilege { return Privilege::granted(self::class . ' always grants privileges'); } - public function getCommandPrivilege(CommandInterface $command): Privilege + public function canExecuteCommand(CommandInterface $command): Privilege { return Privilege::granted(self::class . ' always grants privileges'); } diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentGraphInterface.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentGraphInterface.php index e8cac17eb67..c3de5f221d9 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentGraphInterface.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentGraphInterface.php @@ -14,6 +14,7 @@ namespace Neos\ContentRepository\Core\Projection\ContentGraph; +use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; @@ -50,7 +51,7 @@ public function getContentRepositoryId(): ContentRepositoryId; public function getWorkspaceName(): WorkspaceName; /** - * @api main API method of ContentGraph + * @api You most likely want to use {@see ContentRepository::getContentSubgraph()} because it automatically determines VisibilityConstraints for the current user. */ public function getSubgraph( DimensionSpacePoint $dimensionSpacePoint, diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Helpers/FakeAuthProvider.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Helpers/FakeAuthProvider.php index ac7def3d8d0..916e98ffee3 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Helpers/FakeAuthProvider.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Helpers/FakeAuthProvider.php @@ -30,12 +30,12 @@ public function getVisibilityConstraints(WorkspaceName $workspaceName): Visibili return VisibilityConstraints::withoutRestrictions(); } - public function getReadNodesFromWorkspacePrivilege(WorkspaceName $workspaceName): Privilege + public function canReadNodesFromWorkspace(WorkspaceName $workspaceName): Privilege { return Privilege::granted(self::class . ' always grants privileges'); } - public function getCommandPrivilege(CommandInterface $command): Privilege + public function canExecuteCommand(CommandInterface $command): Privilege { return Privilege::granted(self::class . ' always grants privileges'); } diff --git a/Neos.Neos/Classes/Controller/Frontend/NodeController.php b/Neos.Neos/Classes/Controller/Frontend/NodeController.php index db146184bdb..7191c4e496f 100644 --- a/Neos.Neos/Classes/Controller/Frontend/NodeController.php +++ b/Neos.Neos/Classes/Controller/Frontend/NodeController.php @@ -200,15 +200,16 @@ public function showAction(string $node): void } $contentRepository = $this->contentRepositoryRegistry->get($nodeAddress->contentRepositoryId); - // By default, the visibility constraints only contain the SubtreeTags the authenticated user has _no_ access to - // Neos backend users have access to the "disabled" SubtreeTag so that they can see/edit disabled nodes. - // In this showAction (= "frontend") we have to explicitly remove those disabled nodes, even if the user was authenticated $authenticatedAccount = $this->securityContext->getAccount(); if ($authenticatedAccount !== null) { $visibilityConstraints = $this->contentRepositoryAuthorizationService->getVisibilityConstraintsForAccount($contentRepository->id, $authenticatedAccount); } else { $visibilityConstraints = $this->contentRepositoryAuthorizationService->getVisibilityConstraintsForAnonymousUser($contentRepository->id); } + // By default, the visibility constraints only contain the SubtreeTags the authenticated user has _no_ access to + // Neos backend users have access to the "disabled" SubtreeTag so that they can see/edit disabled nodes. + // In this showAction (= "frontend") we have to explicitly remove those disabled nodes, even if the user was authenticated, + // to ensure that disabled nodes are NEVER shown recursively. $visibilityConstraints = $visibilityConstraints->withAddedSubtreeTag(SubtreeTag::disabled()); $uncachedSubgraph = $contentRepository->getContentGraph($nodeAddress->workspaceName)->getSubgraph($nodeAddress->dimensionSpacePoint, $visibilityConstraints); diff --git a/Neos.Neos/Classes/Security/Authorization/ContentRepositoryAuthorizationService.php b/Neos.Neos/Classes/Security/Authorization/ContentRepositoryAuthorizationService.php index 6bb13616c79..58717299d95 100644 --- a/Neos.Neos/Classes/Security/Authorization/ContentRepositoryAuthorizationService.php +++ b/Neos.Neos/Classes/Security/Authorization/ContentRepositoryAuthorizationService.php @@ -24,6 +24,8 @@ use Neos\Party\Domain\Service\PartyService; /** + * Central point which does ContentRepository authorization decisions within Neos. + * * @api */ #[Flow\Scope('singleton')] diff --git a/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php index 60f73360eb2..980f645caac 100644 --- a/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php +++ b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php @@ -27,7 +27,10 @@ use Neos\Neos\Security\Authorization\ContentRepositoryAuthorizationService; /** - * @api + * Implementation of Content Repository {@see AuthProviderInterface} which ties the authorization + * to Neos. + * + * @internal use {@see ContentRepositoryAuthorizationService} to ask for specific authorization decisions */ final class ContentRepositoryAuthProvider implements AuthProviderInterface { @@ -63,7 +66,7 @@ public function getVisibilityConstraints(WorkspaceName $workspaceName): Visibili return $this->authorizationService->getVisibilityConstraintsForAnonymousUser($this->contentRepositoryId); } - public function getReadNodesFromWorkspacePrivilege(WorkspaceName $workspaceName): Privilege + public function canReadNodesFromWorkspace(WorkspaceName $workspaceName): Privilege { if ($this->securityContext->areAuthorizationChecksDisabled()) { return Privilege::granted('Authorization checks are disabled'); @@ -77,7 +80,7 @@ public function getReadNodesFromWorkspacePrivilege(WorkspaceName $workspaceName) return $workspacePermissions->read ? Privilege::granted($workspacePermissions->getReason()) : Privilege::denied($workspacePermissions->getReason()); } - public function getCommandPrivilege(CommandInterface $command): Privilege + public function canExecuteCommand(CommandInterface $command): Privilege { if ($this->securityContext->areAuthorizationChecksDisabled()) { return Privilege::granted('Authorization checks are disabled'); diff --git a/Neos.Neos/Configuration/Policy.yaml b/Neos.Neos/Configuration/Policy.yaml index 1cd85ab83c2..32fed1bd759 100644 --- a/Neos.Neos/Configuration/Policy.yaml +++ b/Neos.Neos/Configuration/Policy.yaml @@ -146,6 +146,8 @@ privilegeTargets: 'Neos\Neos\Security\Authorization\Privilege\SubtreeTagPrivilege': 'Neos.Neos:ContentRepository.ReadDisabledNodes': + # !!! matcher payload in this case is a ContentRepository SubtreeTag, + # i.e. nodes with ths specified tag are only read if the user has the corresponding privilegeTarget assigned. matcher: 'disabled' roles: From 38bd6f3c969592f493d3c07e2e89c674bba58d5d Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Thu, 24 Oct 2024 16:30:36 +0200 Subject: [PATCH 038/214] Ignore disabled authorization checks when evaluating visibility constraints --- .../ContentRepositoryAuthProvider.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php index 980f645caac..67ece65438a 100644 --- a/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php +++ b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php @@ -56,9 +56,6 @@ public function getAuthenticatedUserId(): ?UserId public function getVisibilityConstraints(WorkspaceName $workspaceName): VisibilityConstraints { - if ($this->securityContext->areAuthorizationChecksDisabled()) { - return VisibilityConstraints::default(); - } $authenticatedAccount = $this->securityContext->getAccount(); if ($authenticatedAccount) { return $this->authorizationService->getVisibilityConstraintsForAccount($this->contentRepositoryId, $authenticatedAccount); From 14523b2086b1d13c38ce03484405cc2f1a45430e Mon Sep 17 00:00:00 2001 From: Martin Ficzel Date: Thu, 24 Oct 2024 17:03:48 +0200 Subject: [PATCH 039/214] Rename `cr:migratelegacydata`, `cr:exportlegacydata`, to `site:migratelegacydata`, `site:exportlegacydata` and adjust the readmes --- .../{CrCommandController.php => SiteCommandController.php} | 2 +- .../Classes/Service/ProjectionService.php | 2 +- README.md | 6 ++++-- 3 files changed, 6 insertions(+), 4 deletions(-) rename Neos.ContentRepository.LegacyNodeMigration/Classes/Command/{CrCommandController.php => SiteCommandController.php} (99%) diff --git a/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/CrCommandController.php b/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/SiteCommandController.php similarity index 99% rename from Neos.ContentRepository.LegacyNodeMigration/Classes/Command/CrCommandController.php rename to Neos.ContentRepository.LegacyNodeMigration/Classes/Command/SiteCommandController.php index 3c35ce735d5..afa3dafc704 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/CrCommandController.php +++ b/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/SiteCommandController.php @@ -30,7 +30,7 @@ use Neos\Neos\Domain\Service\SiteImportService; use Neos\Utility\Files; -class CrCommandController extends CommandController +class SiteCommandController extends CommandController { public function __construct( private readonly Connection $connection, diff --git a/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionService.php b/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionService.php index 3a6cc5e242f..02ae8cd9bc9 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionService.php +++ b/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionService.php @@ -16,7 +16,7 @@ /** * Content Repository service to perform Projection operations * - * @internal this is currently only used by the {@see CrCommandController} + * @internal this is currently only used by the {@see SiteCommandController} */ final class ProjectionService implements ContentRepositoryServiceInterface { diff --git a/README.md b/README.md index b7cb5af08a2..ea185d368e3 100644 --- a/README.md +++ b/README.md @@ -76,14 +76,16 @@ You can chose from one of the following options: # the following config points to a Neos 8.0 database (adjust to your needs), created by # the legacy "./flow site:import Neos.Demo" command. -./flow cr:migrateLegacyData --config '{"dbal": {"dbname": "neos80"}, "resourcesPath": "/path/to/neos-8.0/Data/Persistent/Resources"}' +./flow site:migrateLegacyData --config '{"dbal": {"dbname": "neos80"}, "resourcesPath": "/path/to/neos-8.0/Data/Persistent/Resources"}' ``` #### Importing an existing (Neos >= 9.0) Site from an Export ``` bash +# make sure this cr is empty +./flow site:pruneAll # import the event stream from the Neos.Demo package -./flow cr:import Packages/Sites/Neos.Demo/Resources/Private/Content +./flow site:importAll Packages/Sites/Neos.Demo/Resources/Private/Content ``` ### Running Neos From 1c2afb74b62dbd52fc446a5a930eeb15c1794641 Mon Sep 17 00:00:00 2001 From: Martin Ficzel Date: Thu, 24 Oct 2024 20:15:45 +0200 Subject: [PATCH 040/214] Adjust behat tests for legacy export and add sites.feature --- .../Bootstrap/CrImportExportTrait.php | 23 +++++ .../Features/EventExportProcessor.feature | 4 +- .../Tests/Behavior/behat.yml.dist | 11 +++ .../Processors/EventExportProcessor.php | 5 -- .../Processors/SitesExportProcessor.php | 32 +++---- .../Behavior/Bootstrap/FeatureContext.php | 53 ++++++++++-- .../Tests/Behavior/Features/Basic.feature | 6 +- .../Tests/Behavior/Features/Hidden.feature | 84 +++++++++---------- ...iddenWithoutTimeableNodeVisibility.feature | 84 +++++++++---------- .../Tests/Behavior/Features/Sites.feature | 46 ++++++++++ 10 files changed, 231 insertions(+), 117 deletions(-) create mode 100644 Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/Sites.feature diff --git a/Neos.ContentRepository.Export/Tests/Behavior/Features/Bootstrap/CrImportExportTrait.php b/Neos.ContentRepository.Export/Tests/Behavior/Features/Bootstrap/CrImportExportTrait.php index 8fed952b2bd..38f5024e6c0 100644 --- a/Neos.ContentRepository.Export/Tests/Behavior/Features/Bootstrap/CrImportExportTrait.php +++ b/Neos.ContentRepository.Export/Tests/Behavior/Features/Bootstrap/CrImportExportTrait.php @@ -181,6 +181,29 @@ public function iExpectTheFollowingEventsToBeExported(TableNode $table): void Assert::assertCount(count($table->getHash()), $exportedEvents, 'Expected number of events does not match actual number'); } + /** + * @Then I expect the following sites to be exported + */ + public function iExpectTheFollowingSitesToBeExported(TableNode $table): void + { + if (!$this->crImportExportTrait_filesystem->has('sites.json')) { + Assert::fail('No events were exported'); + } + $actualSitesJson = $this->crImportExportTrait_filesystem->read('sites.json'); + $actualSiteRows = json_decode($actualSitesJson, true, 512, JSON_THROW_ON_ERROR); + + $expectedSites = $table->getHash(); + foreach ($expectedSites as $key => $expectedSiteData) { + $actualSiteData = $actualSiteRows[$key] ?? []; + $expectedSiteData = array_map( + fn(string $value) => json_decode($value, true, 512, JSON_THROW_ON_ERROR), + $expectedSiteData + ); + Assert::assertEquals($expectedSiteData, $actualSiteData, 'Actual site: ' . json_encode($actualSiteData, JSON_THROW_ON_ERROR)); + } + Assert::assertCount(count($table->getHash()), $actualSiteRows, 'Expected number of sites does not match actual number'); + } + /** * @Then I expect the following errors to be logged */ diff --git a/Neos.ContentRepository.Export/Tests/Behavior/Features/EventExportProcessor.feature b/Neos.ContentRepository.Export/Tests/Behavior/Features/EventExportProcessor.feature index 9c3f28cad09..a8a67030e9e 100644 --- a/Neos.ContentRepository.Export/Tests/Behavior/Features/EventExportProcessor.feature +++ b/Neos.ContentRepository.Export/Tests/Behavior/Features/EventExportProcessor.feature @@ -37,7 +37,7 @@ Feature: As a user of the CR I want to export the event stream using the EventEx When the events are exported Then I expect the following jsonl: """ - {"identifier":"random-event-uuid","type":"RootNodeAggregateWithNodeWasCreated","payload":{"workspaceName":"live","contentStreamId":"cs-identifier","nodeAggregateId":"lady-eleonode-rootford","nodeTypeName":"Neos.ContentRepository:Root","coveredDimensionSpacePoints":[{"language":"de"},{"language":"gsw"},{"language":"fr"}],"nodeAggregateClassification":"root"},"metadata":{"commandClass":"Neos\\ContentRepository\\Core\\Feature\\RootNodeCreation\\Command\\CreateRootNodeAggregateWithNode","commandPayload":{"workspaceName":"live","nodeAggregateId":"lady-eleonode-rootford","nodeTypeName":"Neos.ContentRepository:Root","tetheredDescendantNodeAggregateIds":[]},"initiatingUserId":"system","initiatingTimestamp":"random-time"}} - {"identifier":"random-event-uuid","type":"NodeAggregateWithNodeWasCreated","payload":{"workspaceName":"live","contentStreamId":"cs-identifier","nodeAggregateId":"nody-mc-nodeface","nodeTypeName":"Neos.ContentRepository.Testing:Document","originDimensionSpacePoint":{"language":"de"},"succeedingSiblingsForCoverage":[{"dimensionSpacePoint":{"language":"de"},"nodeAggregateId":null},{"dimensionSpacePoint":{"language":"gsw"},"nodeAggregateId":null},{"dimensionSpacePoint":{"language":"fr"},"nodeAggregateId":null}],"parentNodeAggregateId":"lady-eleonode-rootford","nodeName":"child-document","initialPropertyValues":[],"nodeAggregateClassification":"regular"},"metadata":{"initiatingTimestamp":"random-time"}} + {"identifier":"random-event-uuid","type":"RootNodeAggregateWithNodeWasCreated","payload":{"nodeAggregateId":"lady-eleonode-rootford","nodeTypeName":"Neos.ContentRepository:Root","coveredDimensionSpacePoints":[{"language":"de"},{"language":"gsw"},{"language":"fr"}],"nodeAggregateClassification":"root"},"metadata":{"commandClass":"Neos\\ContentRepository\\Core\\Feature\\RootNodeCreation\\Command\\CreateRootNodeAggregateWithNode","commandPayload":{"workspaceName":"live","nodeAggregateId":"lady-eleonode-rootford","nodeTypeName":"Neos.ContentRepository:Root","tetheredDescendantNodeAggregateIds":[]},"initiatingUserId":"system","initiatingTimestamp":"random-time"}} + {"identifier":"random-event-uuid","type":"NodeAggregateWithNodeWasCreated","payload":{"nodeAggregateId":"nody-mc-nodeface","nodeTypeName":"Neos.ContentRepository.Testing:Document","originDimensionSpacePoint":{"language":"de"},"succeedingSiblingsForCoverage":[{"dimensionSpacePoint":{"language":"de"},"nodeAggregateId":null},{"dimensionSpacePoint":{"language":"gsw"},"nodeAggregateId":null},{"dimensionSpacePoint":{"language":"fr"},"nodeAggregateId":null}],"parentNodeAggregateId":"lady-eleonode-rootford","nodeName":"child-document","initialPropertyValues":[],"nodeAggregateClassification":"regular"},"metadata":{"initiatingTimestamp":"random-time"}} """ diff --git a/Neos.ContentRepository.Export/Tests/Behavior/behat.yml.dist b/Neos.ContentRepository.Export/Tests/Behavior/behat.yml.dist index e69de29bb2d..11e2845b599 100644 --- a/Neos.ContentRepository.Export/Tests/Behavior/behat.yml.dist +++ b/Neos.ContentRepository.Export/Tests/Behavior/behat.yml.dist @@ -0,0 +1,11 @@ + +default: + autoload: + '': "%paths.base%/Features/Bootstrap" + suites: + cr: + paths: + - "%paths.base%/Features" + + contexts: + - FeatureContext diff --git a/Neos.ContentRepository.LegacyNodeMigration/Classes/Processors/EventExportProcessor.php b/Neos.ContentRepository.LegacyNodeMigration/Classes/Processors/EventExportProcessor.php index 10e083b3822..747178f37a7 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Classes/Processors/EventExportProcessor.php +++ b/Neos.ContentRepository.LegacyNodeMigration/Classes/Processors/EventExportProcessor.php @@ -90,11 +90,6 @@ public function __construct( $this->visitedNodes = new VisitedNodeAggregates(); } - public function setContentStreamId(ContentStreamId $contentStreamId): void - { - $this->contentStreamId = $contentStreamId; - } - public function run(ProcessingContext $context): void { $this->resetRuntimeState(); diff --git a/Neos.ContentRepository.LegacyNodeMigration/Classes/Processors/SitesExportProcessor.php b/Neos.ContentRepository.LegacyNodeMigration/Classes/Processors/SitesExportProcessor.php index d8b7fe6b3f7..9b66a0f0f94 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Classes/Processors/SitesExportProcessor.php +++ b/Neos.ContentRepository.LegacyNodeMigration/Classes/Processors/SitesExportProcessor.php @@ -41,21 +41,23 @@ private function getSiteData(): array "nodeName" => $siteRow['nodename'], "siteResourcesPackageKey" => $siteRow['siteresourcespackagekey'], "online" => $siteRow['state'] === 1, - "domains" => array_filter( - array_map( - function(array $domainRow) use ($siteRow) { - if ($siteRow['persistence_object_identifier'] !== $domainRow['site']) { - return null; - } - return [ - 'hostname' => $domainRow['hostname'], - 'scheme' => $domainRow['scheme'], - 'port' => $domainRow['port'], - 'active' => $domainRow['active'], - 'primary' => $domainRow === $siteRow['primarydomain'], - ]; - }, - iterator_to_array($this->domainRows) + "domains" => array_values( + array_filter( + array_map( + function(array $domainRow) use ($siteRow) { + if ($siteRow['persistence_object_identifier'] !== $domainRow['site']) { + return null; + } + return [ + 'hostname' => $domainRow['hostname'], + 'scheme' => $domainRow['scheme'], + 'port' => $domainRow['port'], + 'active' => $domainRow['active'], + 'primary' => $domainRow['persistence_object_identifier'] === $siteRow['primarydomain'], + ]; + }, + iterator_to_array($this->domainRows) + ) ) ) ]; diff --git a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Bootstrap/FeatureContext.php b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Bootstrap/FeatureContext.php index 188d6b53731..a1aa3031b1e 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Bootstrap/FeatureContext.php +++ b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Bootstrap/FeatureContext.php @@ -27,7 +27,8 @@ use Neos\ContentRepository\Export\Asset\ValueObject\SerializedImageVariant; use Neos\ContentRepository\Export\Asset\ValueObject\SerializedResource; use Neos\ContentRepository\LegacyNodeMigration\Processors\AssetExportProcessor; -use Neos\ContentRepository\LegacyNodeMigration\SitesToSitesProcessor; +use Neos\ContentRepository\LegacyNodeMigration\Processors\EventExportProcessor; +use Neos\ContentRepository\LegacyNodeMigration\Processors\SitesExportProcessor; use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\CRTestSuiteTrait; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Property\PropertyMapper; @@ -47,6 +48,8 @@ class FeatureContext implements Context protected $isolated = false; private array $nodeDataRows = []; + private array $siteDataRows = []; + private array $domainDataRows = []; /** @var array */ private array $mockResources = []; /** @var array */ @@ -86,9 +89,9 @@ public function iHaveTheFollowingNodeDataRows(TableNode $nodeDataRows): void /** * @When I run the event migration - * @When I run the event migration for content stream :contentStream + * @When I run the event migration for workspace :workspace */ - public function iRunTheEventMigration(string $contentStream = null): void + public function iRunTheEventMigration(string $workspace = null): void { $nodeTypeManager = $this->currentContentRepository->getNodeTypeManager(); $propertyMapper = $this->getObject(PropertyMapper::class); @@ -106,7 +109,7 @@ public function build(ContentRepositoryServiceFactoryDependencies $serviceFactor }; $this->getContentRepositoryService($propertyConverterAccess); - $migration = new SitesToSitesProcessor( + $eventExportProcessor = new EventExportProcessor( $nodeTypeManager, $propertyMapper, $propertyConverterAccess->propertyConverter, @@ -114,10 +117,8 @@ public function build(ContentRepositoryServiceFactoryDependencies $serviceFactor $this->getObject(EventNormalizer::class), $this->nodeDataRows ); - if ($contentStream !== null) { - $migration->setContentStreamId(ContentStreamId::fromString($contentStream)); - } - $this->runCrImportExportProcessors($migration); + + $this->runCrImportExportProcessors($eventExportProcessor); } /** @@ -206,6 +207,42 @@ public function findAssetById(string $assetId): SerializedAsset|SerializedImageV $this->runCrImportExportProcessors($migration); } + /** + * @When I have the following site data rows: + */ + public function iHaveTheFollowingSiteDataRows(TableNode $siteDataRows): void + { + $this->siteDataRows = array_map( + fn (array $row) => array_map( + fn(string $value) => json_decode($value, true), + $row + ), + $siteDataRows->getHash() + ); + } + + /** + * @When I have the following domain data rows: + */ + public function iHaveTheFollowingDomainDataRows(TableNode $domainDataRows): void + { + $this->domainDataRows = array_map(static function (array $row) { + return array_map( + fn(string $value) => json_decode($value, true), + $row + ); + }, $domainDataRows->getHash()); + } + + /** + * @When I run the site migration + */ + public function iRunTheSiteMigration(): void + { + $migration = new SitesExportProcessor($this->siteDataRows, $this->domainDataRows); + $this->runCrImportExportProcessors($migration); + } + /** ---------------------------------- */ protected function getContentRepositoryService( diff --git a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/Basic.feature b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/Basic.feature index 6dd05a8d31c..f99d5d3c399 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/Basic.feature +++ b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/Basic.feature @@ -22,8 +22,8 @@ Feature: Simple migrations without content dimensions | Identifier | Path | Node Type | Properties | | sites-node-id | /sites | unstructured | | | site-node-id | /sites/test-site | Some.Package:Homepage | {"text": "foo"} | - And I run the event migration for content stream "cs-id" + And I run the event migration Then I expect the following events to be exported | Type | Payload | - | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | - | NodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} | + | RootNodeAggregateWithNodeWasCreated | {"nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | + | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} | diff --git a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/Hidden.feature b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/Hidden.feature index ef3ec403e0e..bb212a59393 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/Hidden.feature +++ b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/Hidden.feature @@ -30,46 +30,46 @@ Feature: Simple migrations without content dimensions for hidden state migration | Identifier | Path | Node Type | Properties | Hidden | Hidden after DateTime | Hidden before DateTime | | sites-node-id | /sites | unstructured | | 0 | | | | site-node-id | /sites/test-site | Some.Package:Homepage | {"text": "foo"} | 1 | | | - And I run the event migration for content stream "cs-id" + And I run the event migration Then I expect the following events to be exported | Type | Payload | - | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | - | NodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} | - | SubtreeWasTagged | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "tag": "disabled"} | + | RootNodeAggregateWithNodeWasCreated | {"nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | + | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} | + | SubtreeWasTagged | {"nodeAggregateId": "site-node-id", "tag": "disabled"} | Scenario: A node with a "hidden" property false must not get disabled When I have the following node data rows: | Identifier | Path | Node Type | Properties | Hidden | Hidden after DateTime | Hidden before DateTime | | sites-node-id | /sites | unstructured | | 0 | | | | site-node-id | /sites/test-site | Some.Package:Homepage | {"text": "foo"} | 0 | | | - And I run the event migration for content stream "cs-id" + And I run the event migration Then I expect the following events to be exported | Type | Payload | - | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | - | NodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} | + | RootNodeAggregateWithNodeWasCreated | {"nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | + | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} | Scenario: A node with active "hidden after" property, after a "hidden before" property must get disabled When I have the following node data rows: | Identifier | Path | Node Type | Properties | Hidden | Hidden after DateTime | Hidden before DateTime | | sites-node-id | /sites | unstructured | | 0 | | | | site-node-id | /sites/test-site | Some.Package:Homepage | {"text": "foo"} | 0 | 1990-01-01 10:10:10 | 1989-01-01 10:10:10 | - And I run the event migration for content stream "cs-id" + And I run the event migration Then I expect the following events to be exported | Type | Payload | - | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | - | NodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}, "disableAfterDateTime": {"type": "DateTimeImmutable", "value": "1990-01-01 10:10:10"}, "enableAfterDateTime": {"type": "DateTimeImmutable", "value": "1989-01-01 10:10:10"}}} | - | SubtreeWasTagged | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "tag": "disabled"} | + | RootNodeAggregateWithNodeWasCreated | {"nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | + | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}, "disableAfterDateTime": {"type": "DateTimeImmutable", "value": "1990-01-01 10:10:10"}, "enableAfterDateTime": {"type": "DateTimeImmutable", "value": "1989-01-01 10:10:10"}}} | + | SubtreeWasTagged | {"nodeAggregateId": "site-node-id", "tag": "disabled"} | Scenario: A node with active "hidden before" property, after a "hidden after" property must not get disabled When I have the following node data rows: | Identifier | Path | Node Type | Properties | Hidden | Hidden after DateTime | Hidden before DateTime | | sites-node-id | /sites | unstructured | | 0 | | | | site-node-id | /sites/test-site | Some.Package:Homepage | {"text": "foo"} | 0 | 1989-01-01 10:10:10 | 1990-01-01 10:10:10 | - And I run the event migration for content stream "cs-id" + And I run the event migration Then I expect the following events to be exported | Type | Payload | - | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | - | NodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}, "disableAfterDateTime": {"type": "DateTimeImmutable", "value": "1989-01-01 10:10:10"}, "enableAfterDateTime": {"type": "DateTimeImmutable", "value": "1990-01-01 10:10:10"}}} | + | RootNodeAggregateWithNodeWasCreated | {"nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | + | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}, "disableAfterDateTime": {"type": "DateTimeImmutable", "value": "1989-01-01 10:10:10"}, "enableAfterDateTime": {"type": "DateTimeImmutable", "value": "1990-01-01 10:10:10"}}} | Scenario: A node with a active "hidden before" property and a "hidden after" property in future must not get disabled @@ -77,90 +77,90 @@ Feature: Simple migrations without content dimensions for hidden state migration | Identifier | Path | Node Type | Properties | Hidden | Hidden after DateTime | Hidden before DateTime | | sites-node-id | /sites | unstructured | | 0 | | | | site-node-id | /sites/test-site | Some.Package:Homepage | {"text": "foo"} | 0 | 2099-01-01 10:10:10 | 1990-01-01 10:10:10 | - And I run the event migration for content stream "cs-id" + And I run the event migration Then I expect the following events to be exported | Type | Payload | - | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | - | NodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}, "disableAfterDateTime": {"type": "DateTimeImmutable", "value": "2099-01-01 10:10:10"}, "enableAfterDateTime": {"type": "DateTimeImmutable", "value": "1990-01-01 10:10:10"}}} | + | RootNodeAggregateWithNodeWasCreated | {"nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | + | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}, "disableAfterDateTime": {"type": "DateTimeImmutable", "value": "2099-01-01 10:10:10"}, "enableAfterDateTime": {"type": "DateTimeImmutable", "value": "1990-01-01 10:10:10"}}} | Scenario: A node with a active "hidden after" property and a "hidden before" property in future must get disabled When I have the following node data rows: | Identifier | Path | Node Type | Properties | Hidden | Hidden after DateTime | Hidden before DateTime | | sites-node-id | /sites | unstructured | | 0 | | | | site-node-id | /sites/test-site | Some.Package:Homepage | {"text": "foo"} | 0 | 1990-01-01 10:10:10 | 2099-01-01 10:10:10 | - And I run the event migration for content stream "cs-id" + And I run the event migration Then I expect the following events to be exported | Type | Payload | - | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | - | NodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}, "disableAfterDateTime": {"type": "DateTimeImmutable", "value": "1990-01-01 10:10:10"}, "enableAfterDateTime": {"type": "DateTimeImmutable", "value": "2099-01-01 10:10:10"}}} | - | SubtreeWasTagged | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "tag": "disabled"} | + | RootNodeAggregateWithNodeWasCreated | {"nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | + | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}, "disableAfterDateTime": {"type": "DateTimeImmutable", "value": "1990-01-01 10:10:10"}, "enableAfterDateTime": {"type": "DateTimeImmutable", "value": "2099-01-01 10:10:10"}}} | + | SubtreeWasTagged | {"nodeAggregateId": "site-node-id", "tag": "disabled"} | Scenario: A node with a "hidden after" property in future and a "hidden before" property later in future must not get disabled When I have the following node data rows: | Identifier | Path | Node Type | Properties | Hidden | Hidden after DateTime | Hidden before DateTime | | sites-node-id | /sites | unstructured | | 0 | | | | site-node-id | /sites/test-site | Some.Package:Homepage | {"text": "foo"} | 0 | 2098-01-01 10:10:10 | 2099-01-01 10:10:10 | - And I run the event migration for content stream "cs-id" + And I run the event migration Then I expect the following events to be exported | Type | Payload | - | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | - | NodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}, "disableAfterDateTime": {"type": "DateTimeImmutable", "value": "2098-01-01 10:10:10"}, "enableAfterDateTime": {"type": "DateTimeImmutable", "value": "2099-01-01 10:10:10"}}} | + | RootNodeAggregateWithNodeWasCreated | {"nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | + | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}, "disableAfterDateTime": {"type": "DateTimeImmutable", "value": "2098-01-01 10:10:10"}, "enableAfterDateTime": {"type": "DateTimeImmutable", "value": "2099-01-01 10:10:10"}}} | Scenario: A node with a "hidden before" property in future and a "hidden after" property later in future must get disabled When I have the following node data rows: | Identifier | Path | Node Type | Properties | Hidden | Hidden after DateTime | Hidden before DateTime | | sites-node-id | /sites | unstructured | | 0 | | | | site-node-id | /sites/test-site | Some.Package:Homepage | {"text": "foo"} | 0 | 2099-01-01 10:10:10 | 2098-01-01 10:10:10 | - And I run the event migration for content stream "cs-id" + And I run the event migration Then I expect the following events to be exported | Type | Payload | - | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | - | NodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}, "disableAfterDateTime": {"type": "DateTimeImmutable", "value": "2099-01-01 10:10:10"}, "enableAfterDateTime": {"type": "DateTimeImmutable", "value": "2098-01-01 10:10:10"}}} | - | SubtreeWasTagged | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "tag": "disabled"} | + | RootNodeAggregateWithNodeWasCreated | {"nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | + | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}, "disableAfterDateTime": {"type": "DateTimeImmutable", "value": "2099-01-01 10:10:10"}, "enableAfterDateTime": {"type": "DateTimeImmutable", "value": "2098-01-01 10:10:10"}}} | + | SubtreeWasTagged | {"nodeAggregateId": "site-node-id", "tag": "disabled"} | Scenario: A node with a active "hidden before" property must not get disabled When I have the following node data rows: | Identifier | Path | Node Type | Properties | Hidden | Hidden after DateTime | Hidden before DateTime | | sites-node-id | /sites | unstructured | | 0 | | | | site-node-id | /sites/test-site | Some.Package:Homepage | {"text": "foo"} | 0 | | 1990-01-01 10:10:10 | - And I run the event migration for content stream "cs-id" + And I run the event migration Then I expect the following events to be exported | Type | Payload | - | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | - | NodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}, "enableAfterDateTime": {"type": "DateTimeImmutable", "value": "1990-01-01 10:10:10"}}} | + | RootNodeAggregateWithNodeWasCreated | {"nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | + | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}, "enableAfterDateTime": {"type": "DateTimeImmutable", "value": "1990-01-01 10:10:10"}}} | Scenario: A node with a active "hidden after" property must get disabled When I have the following node data rows: | Identifier | Path | Node Type | Properties | Hidden | Hidden after DateTime | Hidden before DateTime | | sites-node-id | /sites | unstructured | | 0 | | | | site-node-id | /sites/test-site | Some.Package:Homepage | {"text": "foo"} | 0 | 1990-01-01 10:10:10 | | - And I run the event migration for content stream "cs-id" + And I run the event migration Then I expect the following events to be exported | Type | Payload | - | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | - | NodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}, "disableAfterDateTime": {"type": "DateTimeImmutable", "value": "1990-01-01 10:10:10"}}} | - | SubtreeWasTagged | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "tag": "disabled"} | + | RootNodeAggregateWithNodeWasCreated | {"nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | + | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}, "disableAfterDateTime": {"type": "DateTimeImmutable", "value": "1990-01-01 10:10:10"}}} | + | SubtreeWasTagged | {"nodeAggregateId": "site-node-id", "tag": "disabled"} | Scenario: A node with a "hidden after" property in future must not get disabled When I have the following node data rows: | Identifier | Path | Node Type | Properties | Hidden | Hidden after DateTime | Hidden before DateTime | | sites-node-id | /sites | unstructured | | 0 | | | | site-node-id | /sites/test-site | Some.Package:Homepage | {"text": "foo"} | 0 | 2099-01-01 10:10:10 | | - And I run the event migration for content stream "cs-id" + And I run the event migration Then I expect the following events to be exported | Type | Payload | - | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | - | NodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}, "disableAfterDateTime": {"type": "DateTimeImmutable", "value": "2099-01-01 10:10:10"}}} | + | RootNodeAggregateWithNodeWasCreated | {"nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | + | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}, "disableAfterDateTime": {"type": "DateTimeImmutable", "value": "2099-01-01 10:10:10"}}} | Scenario: A node with a "hidden before" property in future must get disabled When I have the following node data rows: | Identifier | Path | Node Type | Properties | Hidden | Hidden after DateTime | Hidden before DateTime | | sites-node-id | /sites | unstructured | | 0 | | | | site-node-id | /sites/test-site | Some.Package:Homepage | {"text": "foo"} | 0 | | 2099-01-01 10:10:10 | - And I run the event migration for content stream "cs-id" + And I run the event migration Then I expect the following events to be exported | Type | Payload | - | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | - | NodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}, "enableAfterDateTime": {"type": "DateTimeImmutable", "value": "2099-01-01 10:10:10"}}} | - | SubtreeWasTagged | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "tag": "disabled"} | + | RootNodeAggregateWithNodeWasCreated | {"nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | + | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}, "enableAfterDateTime": {"type": "DateTimeImmutable", "value": "2099-01-01 10:10:10"}}} | + | SubtreeWasTagged | {"nodeAggregateId": "site-node-id", "tag": "disabled"} | diff --git a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/HiddenWithoutTimeableNodeVisibility.feature b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/HiddenWithoutTimeableNodeVisibility.feature index bb701dab822..59d82c5b8e8 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/HiddenWithoutTimeableNodeVisibility.feature +++ b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/HiddenWithoutTimeableNodeVisibility.feature @@ -23,35 +23,35 @@ Feature: Simple migrations without content dimensions for hidden state migration | Identifier | Path | Node Type | Properties | Hidden | Hidden after DateTime | Hidden before DateTime | | sites-node-id | /sites | unstructured | | 0 | | | | site-node-id | /sites/test-site | Some.Package:Homepage | {"text": "foo"} | 1 | | | - And I run the event migration for content stream "cs-id" + And I run the event migration Then I expect the following events to be exported | Type | Payload | - | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | - | NodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} | - | SubtreeWasTagged | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "tag": "disabled"} | + | RootNodeAggregateWithNodeWasCreated | {"nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | + | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} | + | SubtreeWasTagged | {"nodeAggregateId": "site-node-id", "tag": "disabled"} | Scenario: A node with a "hidden" property false must not get disabled When I have the following node data rows: | Identifier | Path | Node Type | Properties | Hidden | Hidden after DateTime | Hidden before DateTime | | sites-node-id | /sites | unstructured | | 0 | | | | site-node-id | /sites/test-site | Some.Package:Homepage | {"text": "foo"} | 0 | | | - And I run the event migration for content stream "cs-id" + And I run the event migration Then I expect the following events to be exported | Type | Payload | - | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | - | NodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} | + | RootNodeAggregateWithNodeWasCreated | {"nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | + | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} | Scenario: A node with active "hidden after" property, after a "hidden before" property must get disabled When I have the following node data rows: | Identifier | Path | Node Type | Properties | Hidden | Hidden after DateTime | Hidden before DateTime | | sites-node-id | /sites | unstructured | | 0 | | | | site-node-id | /sites/test-site | Some.Package:Homepage | {"text": "foo"} | 0 | 1990-01-01 10:10:10 | 1989-01-01 10:10:10 | - And I run the event migration for content stream "cs-id" + And I run the event migration Then I expect the following events to be exported | Type | Payload | - | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | - | NodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} | - | SubtreeWasTagged | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "tag": "disabled"} | + | RootNodeAggregateWithNodeWasCreated | {"nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | + | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} | + | SubtreeWasTagged | {"nodeAggregateId": "site-node-id", "tag": "disabled"} | And I expect the following warnings to be logged | Skipped the migration of your "hiddenBeforeDateTime" and "hiddenAfterDateTime" properties as your target NodeTypes do not inherit "Neos.TimeableNodeVisibility:Timeable". Please install neos/timeable-node-visibility, if you want to migrate them. | @@ -60,11 +60,11 @@ Feature: Simple migrations without content dimensions for hidden state migration | Identifier | Path | Node Type | Properties | Hidden | Hidden after DateTime | Hidden before DateTime | | sites-node-id | /sites | unstructured | | 0 | | | | site-node-id | /sites/test-site | Some.Package:Homepage | {"text": "foo"} | 0 | 1989-01-01 10:10:10 | 1990-01-01 10:10:10 | - And I run the event migration for content stream "cs-id" + And I run the event migration Then I expect the following events to be exported | Type | Payload | - | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | - | NodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} | + | RootNodeAggregateWithNodeWasCreated | {"nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | + | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} | And I expect the following warnings to be logged | Skipped the migration of your "hiddenBeforeDateTime" and "hiddenAfterDateTime" properties as your target NodeTypes do not inherit "Neos.TimeableNodeVisibility:Timeable". Please install neos/timeable-node-visibility, if you want to migrate them. | @@ -73,11 +73,11 @@ Feature: Simple migrations without content dimensions for hidden state migration | Identifier | Path | Node Type | Properties | Hidden | Hidden after DateTime | Hidden before DateTime | | sites-node-id | /sites | unstructured | | 0 | | | | site-node-id | /sites/test-site | Some.Package:Homepage | {"text": "foo"} | 0 | 2099-01-01 10:10:10 | 1990-01-01 10:10:10 | - And I run the event migration for content stream "cs-id" + And I run the event migration Then I expect the following events to be exported | Type | Payload | - | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | - | NodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} | + | RootNodeAggregateWithNodeWasCreated | {"nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | + | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} | And I expect the following warnings to be logged | Skipped the migration of your "hiddenBeforeDateTime" and "hiddenAfterDateTime" properties as your target NodeTypes do not inherit "Neos.TimeableNodeVisibility:Timeable". Please install neos/timeable-node-visibility, if you want to migrate them. | @@ -86,12 +86,12 @@ Feature: Simple migrations without content dimensions for hidden state migration | Identifier | Path | Node Type | Properties | Hidden | Hidden after DateTime | Hidden before DateTime | | sites-node-id | /sites | unstructured | | 0 | | | | site-node-id | /sites/test-site | Some.Package:Homepage | {"text": "foo"} | 0 | 1990-01-01 10:10:10 | 2099-01-01 10:10:10 | - And I run the event migration for content stream "cs-id" + And I run the event migration Then I expect the following events to be exported | Type | Payload | - | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | - | NodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} | - | SubtreeWasTagged | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "tag": "disabled"} | + | RootNodeAggregateWithNodeWasCreated | {"nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | + | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} | + | SubtreeWasTagged | {"nodeAggregateId": "site-node-id", "tag": "disabled"} | And I expect the following warnings to be logged | Skipped the migration of your "hiddenBeforeDateTime" and "hiddenAfterDateTime" properties as your target NodeTypes do not inherit "Neos.TimeableNodeVisibility:Timeable". Please install neos/timeable-node-visibility, if you want to migrate them. | @@ -100,11 +100,11 @@ Feature: Simple migrations without content dimensions for hidden state migration | Identifier | Path | Node Type | Properties | Hidden | Hidden after DateTime | Hidden before DateTime | | sites-node-id | /sites | unstructured | | 0 | | | | site-node-id | /sites/test-site | Some.Package:Homepage | {"text": "foo"} | 0 | 2098-01-01 10:10:10 | 2099-01-01 10:10:10 | - And I run the event migration for content stream "cs-id" + And I run the event migration Then I expect the following events to be exported | Type | Payload | - | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | - | NodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} | + | RootNodeAggregateWithNodeWasCreated | {"nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | + | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} | And I expect the following warnings to be logged | Skipped the migration of your "hiddenBeforeDateTime" and "hiddenAfterDateTime" properties as your target NodeTypes do not inherit "Neos.TimeableNodeVisibility:Timeable". Please install neos/timeable-node-visibility, if you want to migrate them. | @@ -113,12 +113,12 @@ Feature: Simple migrations without content dimensions for hidden state migration | Identifier | Path | Node Type | Properties | Hidden | Hidden after DateTime | Hidden before DateTime | | sites-node-id | /sites | unstructured | | 0 | | | | site-node-id | /sites/test-site | Some.Package:Homepage | {"text": "foo"} | 0 | 2099-01-01 10:10:10 | 2098-01-01 10:10:10 | - And I run the event migration for content stream "cs-id" + And I run the event migration Then I expect the following events to be exported | Type | Payload | - | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | - | NodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} | - | SubtreeWasTagged | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "tag": "disabled"} | + | RootNodeAggregateWithNodeWasCreated | {"nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | + | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} | + | SubtreeWasTagged | {"nodeAggregateId": "site-node-id", "tag": "disabled"} | And I expect the following warnings to be logged | Skipped the migration of your "hiddenBeforeDateTime" and "hiddenAfterDateTime" properties as your target NodeTypes do not inherit "Neos.TimeableNodeVisibility:Timeable". Please install neos/timeable-node-visibility, if you want to migrate them. | @@ -127,11 +127,11 @@ Feature: Simple migrations without content dimensions for hidden state migration | Identifier | Path | Node Type | Properties | Hidden | Hidden after DateTime | Hidden before DateTime | | sites-node-id | /sites | unstructured | | 0 | | | | site-node-id | /sites/test-site | Some.Package:Homepage | {"text": "foo"} | 0 | | 1990-01-01 10:10:10 | - And I run the event migration for content stream "cs-id" + And I run the event migration Then I expect the following events to be exported | Type | Payload | - | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | - | NodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} | + | RootNodeAggregateWithNodeWasCreated | {"nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | + | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} | And I expect the following warnings to be logged | Skipped the migration of your "hiddenBeforeDateTime" and "hiddenAfterDateTime" properties as your target NodeTypes do not inherit "Neos.TimeableNodeVisibility:Timeable". Please install neos/timeable-node-visibility, if you want to migrate them. | @@ -140,12 +140,12 @@ Feature: Simple migrations without content dimensions for hidden state migration | Identifier | Path | Node Type | Properties | Hidden | Hidden after DateTime | Hidden before DateTime | | sites-node-id | /sites | unstructured | | 0 | | | | site-node-id | /sites/test-site | Some.Package:Homepage | {"text": "foo"} | 0 | 1990-01-01 10:10:10 | | - And I run the event migration for content stream "cs-id" + And I run the event migration Then I expect the following events to be exported | Type | Payload | - | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | - | NodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} | - | SubtreeWasTagged | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "tag": "disabled"} | + | RootNodeAggregateWithNodeWasCreated | {"nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | + | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} | + | SubtreeWasTagged | {"nodeAggregateId": "site-node-id", "tag": "disabled"} | And I expect the following warnings to be logged | Skipped the migration of your "hiddenBeforeDateTime" and "hiddenAfterDateTime" properties as your target NodeTypes do not inherit "Neos.TimeableNodeVisibility:Timeable". Please install neos/timeable-node-visibility, if you want to migrate them. | @@ -154,11 +154,11 @@ Feature: Simple migrations without content dimensions for hidden state migration | Identifier | Path | Node Type | Properties | Hidden | Hidden after DateTime | Hidden before DateTime | | sites-node-id | /sites | unstructured | | 0 | | | | site-node-id | /sites/test-site | Some.Package:Homepage | {"text": "foo"} | 0 | 2099-01-01 10:10:10 | | - And I run the event migration for content stream "cs-id" + And I run the event migration Then I expect the following events to be exported | Type | Payload | - | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | - | NodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} | + | RootNodeAggregateWithNodeWasCreated | {"nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | + | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} | And I expect the following warnings to be logged | Skipped the migration of your "hiddenBeforeDateTime" and "hiddenAfterDateTime" properties as your target NodeTypes do not inherit "Neos.TimeableNodeVisibility:Timeable". Please install neos/timeable-node-visibility, if you want to migrate them. | @@ -167,11 +167,11 @@ Feature: Simple migrations without content dimensions for hidden state migration | Identifier | Path | Node Type | Properties | Hidden | Hidden after DateTime | Hidden before DateTime | | sites-node-id | /sites | unstructured | | 0 | | | | site-node-id | /sites/test-site | Some.Package:Homepage | {"text": "foo"} | 0 | | 2099-01-01 10:10:10 | - And I run the event migration for content stream "cs-id" + And I run the event migration Then I expect the following events to be exported | Type | Payload | - | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | - | NodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} | - | SubtreeWasTagged | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "tag": "disabled"} | + | RootNodeAggregateWithNodeWasCreated | {"nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | + | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} | + | SubtreeWasTagged | {"nodeAggregateId": "site-node-id", "tag": "disabled"} | And I expect the following warnings to be logged | Skipped the migration of your "hiddenBeforeDateTime" and "hiddenAfterDateTime" properties as your target NodeTypes do not inherit "Neos.TimeableNodeVisibility:Timeable". Please install neos/timeable-node-visibility, if you want to migrate them. | diff --git a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/Sites.feature b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/Sites.feature new file mode 100644 index 00000000000..12a1e808c17 --- /dev/null +++ b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/Sites.feature @@ -0,0 +1,46 @@ +@contentrepository +Feature: Simple migrations without content dimensions + + Background: + Given using no content dimensions + And using the following node types: + """yaml + 'Neos.Neos:Site': {} + 'Some.Package:Homepage': + superTypes: + 'Neos.Neos:Site': true + properties: + 'text': + type: string + defaultValue: 'My default text' + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + + Scenario: Site records without domains + When I have the following site data rows: + | persistence_object_identifier | name | nodename | siteresourcespackagekey | state | domains | primarydomain | + | "site1" | "Site 1" | "site_1_node" | "Site1.Package" | 1 | null | null | + | "site2" | "Site 2" | "site_2_node" | "Site2.Package" | 2 | null | null | + And I run the site migration + Then I expect the following sites to be exported + | name | nodeName | siteResourcesPackageKey | online | domains | + | "Site 1" | "site_1_node" | "Site1.Package" | true | [] | + | "Site 2" | "site_2_node" | "Site2.Package" | false | [] | + + Scenario: Site records with domains + When I have the following site data rows: + | persistence_object_identifier | name | nodename | siteresourcespackagekey | state | domains | primarydomain | + | "site1" | "Site 1" | "site_1_node" | "Site1.Package" | 1 | null | "domain2" | + | "site2" | "Site 2" | "site_2_node" | "Site2.Package" | 1 | null | null | + When I have the following domain data rows: + | persistence_object_identifier | hostname | scheme | port | active | site | + | "domain1" | "domain_1.tld" | "https" | 123 | true | "site1" | + | "domain2" | "domain_2.tld" | "http" | null | true | "site1" | + | "domain3" | "domain_3.tld" | null | null | true | "site2" | + | "domain4" | "domain_4.tld" | null | null | false | "site2" | + And I run the site migration + Then I expect the following sites to be exported + | name | nodeName | siteResourcesPackageKey | online | domains | + | "Site 1" | "site_1_node" | "Site1.Package" | true | [{"hostname": "domain_1.tld", "scheme": "https", "port": 123, "active": true, "primary": false},{"hostname": "domain_2.tld", "scheme": "http", "port": null, "active": true, "primary": true}] | + | "Site 2" | "site_2_node" | "Site2.Package" | true | [{"hostname": "domain_3.tld", "scheme": null, "port": null, "active": true, "primary": false},{"hostname": "domain_4.tld", "scheme": null, "port": null, "active": false, "primary": false}] | From 4423476ce6e39c5504f09c683a2c9953b6e9dce2 Mon Sep 17 00:00:00 2001 From: Martin Ficzel Date: Thu, 24 Oct 2024 21:02:07 +0200 Subject: [PATCH 041/214] Format arguments of site command controller properly --- .../Classes/Command/SiteCommandController.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/SiteCommandController.php b/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/SiteCommandController.php index afa3dafc704..5e81b340027 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/SiteCommandController.php +++ b/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/SiteCommandController.php @@ -33,11 +33,11 @@ class SiteCommandController extends CommandController { public function __construct( - private readonly Connection $connection, - private readonly Environment $environment, - private readonly PropertyMapper $propertyMapper, - private readonly ContentRepositoryRegistry $contentRepositoryRegistry, - private readonly SiteImportService $siteImportService, + private readonly Connection $connection, + private readonly Environment $environment, + private readonly PropertyMapper $propertyMapper, + private readonly ContentRepositoryRegistry $contentRepositoryRegistry, + private readonly SiteImportService $siteImportService, ) { parent::__construct(); } From 6b218f233828d97b655be6d9d59ecef52bf41d8e Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 26 Oct 2024 10:33:33 +0200 Subject: [PATCH 042/214] TASK: Simplify TestingFusionAutoIncludeHandler --- .../TestingFusionAutoIncludeHandler.php | 37 +++-- .../Features/Bootstrap/DispatcherTrait.php | 132 ------------------ .../Features/Bootstrap/FeatureContext.php | 2 +- .../Bootstrap/FrontendNodeControllerTrait.php | 92 ++++++++++++ 4 files changed, 120 insertions(+), 143 deletions(-) delete mode 100644 Neos.Neos/Tests/Behavior/Features/Bootstrap/DispatcherTrait.php create mode 100644 Neos.Neos/Tests/Behavior/Features/Bootstrap/FrontendNodeControllerTrait.php diff --git a/Neos.Neos/Classes/Testing/TestingFusionAutoIncludeHandler.php b/Neos.Neos/Classes/Testing/TestingFusionAutoIncludeHandler.php index 356f1aa318b..94e2dc76cb3 100644 --- a/Neos.Neos/Classes/Testing/TestingFusionAutoIncludeHandler.php +++ b/Neos.Neos/Classes/Testing/TestingFusionAutoIncludeHandler.php @@ -18,26 +18,43 @@ class TestingFusionAutoIncludeHandler implements FusionAutoIncludeHandler /** * @Flow\Inject */ - protected ResourceFusionAutoIncludeHandler $resourceFusionAutoIncludeHandler; + protected ResourceFusionAutoIncludeHandler $defaultHandler; - private ?FusionAutoIncludeHandler $overrideHandler = null; + /** + * @var array + */ + private array $overriddenIncludes = []; + + public function setIncludeFusionPackage(string $packageKey): void + { + $this->overriddenIncludes[$packageKey] = true; + } - public function overrideHandler(FusionAutoIncludeHandler $overrideHandler): void + public function setFusionForPackage(string $packageKey, FusionSourceCodeCollection $packageFusionSource): void { - $this->overrideHandler = $overrideHandler; + $this->overriddenIncludes[$packageKey] = $packageFusionSource; } - public function resetOverride(): void + public function reset(): void { - $this->overrideHandler = null; + $this->overriddenIncludes = []; } + /** + * If no override is set via {@see setIncludeFusionPackage} or {@see setFusionForPackage} we load all the fusion via the default implementation + */ public function loadFusionFromPackage(string $packageKey, FusionSourceCodeCollection $sourceCodeCollection): FusionSourceCodeCollection { - if ($this->overrideHandler !== null) { - return $this->overrideHandler->loadFusionFromPackage($packageKey, $sourceCodeCollection); - } else { - return $this->resourceFusionAutoIncludeHandler->loadFusionFromPackage($packageKey, $sourceCodeCollection); + if ($this->overriddenIncludes === []) { + return $this->defaultHandler->loadFusionFromPackage($packageKey, $sourceCodeCollection); + } + $override = $this->overriddenIncludes[$packageKey] ?? null; + if ($override === null) { + return $sourceCodeCollection; + } + if ($override === true) { + return $this->defaultHandler->loadFusionFromPackage($packageKey, $sourceCodeCollection); } + return $sourceCodeCollection->union($override); } } diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/DispatcherTrait.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/DispatcherTrait.php deleted file mode 100644 index 9293d79559d..00000000000 --- a/Neos.Neos/Tests/Behavior/Features/Bootstrap/DispatcherTrait.php +++ /dev/null @@ -1,132 +0,0 @@ - $className - * - * @return T - */ - abstract private function getObject(string $className): object; - - /** - * @BeforeScenario - */ - public function setupDispatcherTest(): void - { - $this->getObject(ContentCache::class)->flush(); - $this->getObject(\Neos\Neos\Testing\TestingFusionAutoIncludeHandler::class)->resetOverride(); - $this->response = null; - } - - /** - * @When the Fusion code for package :package is: - */ - public function iHaveTheFollowingFusionCodeForTheSite(PyStringNode $fusionCode, string $package) - { - $this->getObject(\Neos\Neos\Testing\TestingFusionAutoIncludeHandler::class)->overrideHandler( - new class ($package, $fusionCode->getRaw(), $this->getObject(\Neos\Neos\Domain\Service\ResourceFusionAutoIncludeHandler::class)) implements \Neos\Neos\Domain\Service\FusionAutoIncludeHandler - { - public function __construct( - private string $package, - private string $fusionCode, - private \Neos\Neos\Domain\Service\ResourceFusionAutoIncludeHandler $defaultHandler, - ) { - } - - public function loadFusionFromPackage( - string $packageKey, - \Neos\Fusion\Core\FusionSourceCodeCollection $sourceCodeCollection - ): \Neos\Fusion\Core\FusionSourceCodeCollection { - if ($packageKey === $this->package) { - return $sourceCodeCollection->union( - \Neos\Fusion\Core\FusionSourceCodeCollection::fromString( - $this->fusionCode - ) - ); - } elseif (in_array($packageKey, ['Neos.Neos', 'Neos.Fusion'])) { - return $this->defaultHandler->loadFusionFromPackage($packageKey, $sourceCodeCollection); - } else { - return $sourceCodeCollection; - } - } - } - ); - // $fakeFusionService = new class ($original) extends \Neos\Neos\Domain\Service\FusionService - // { - // public function __construct( - // private \Neos\Neos\Domain\Service\FusionService $original, - // private \Neos\Fusion\Core\FusionSourceCode $additionalFusion - // ) { - // } - // public function createFusionConfigurationFromSite(\Neos\Neos\Domain\Model\Site $site): \Neos\Fusion\Core\FusionConfiguration - // { - // $this->original->createFusionConfigurationFromSite($site)-> ... doest work - // } - // }; - - // doesnt work as the packages base path cannot change ... we would need to create an actual package in /Packages as rescanPackages() will be invoked - // vfsStream::setup('packages'); - // $this->getObject(\Neos\Flow\Package\PackageManager::class)->createPackage('Vendor.Site', [], 'vfs://packages/'); - // file_put_contents('resource://Vendor.Site/Private/Fusion/Root.fusion', $fusionCode->getRaw()); - } - - /** - * @When I dispatch the following request :requestUri - */ - public function iDispatchTheFollowingRequest(string $requestUri) - { - $httpRequest = $this->getObject(ServerRequestFactoryInterface::class)->createServerRequest('GET', $requestUri); - - $this->response = $this->getObject(\Neos\Flow\Http\Middleware\MiddlewaresChain::class)->handle( - $httpRequest - ); - } - - /** - * @Then I expect the following response header: - */ - public function iExpectTheFollowingResponseHeader(PyStringNode $expectedResult): void - { - Assert::assertNotNull($this->response); - Assert::assertSame($expectedResult->getRaw(), $this->response->getBody()->getContents()); - } - - /** - * @Then I expect the following response: - */ - public function iExpectTheFollowingResponse(PyStringNode $expectedResult): void - { - Assert::assertNotNull($this->response); - Assert::assertEquals($expectedResult->getRaw(), str_replace("\r\n", "\n", Message::toString($this->response))); - } -} diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/FeatureContext.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/FeatureContext.php index b02048f437e..fce5b7a3278 100644 --- a/Neos.Neos/Tests/Behavior/Features/Bootstrap/FeatureContext.php +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/FeatureContext.php @@ -39,7 +39,7 @@ class FeatureContext implements BehatContext use CRBehavioralTestsSubjectProvider; use RoutingTrait; use MigrationsTrait; - use DispatcherTrait; + use FrontendNodeControllerTrait; use FusionTrait; use ContentCacheTrait; diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/FrontendNodeControllerTrait.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/FrontendNodeControllerTrait.php new file mode 100644 index 00000000000..982262f17d2 --- /dev/null +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/FrontendNodeControllerTrait.php @@ -0,0 +1,92 @@ + $className + * + * @return T + */ + abstract private function getObject(string $className): object; + + /** + * @BeforeScenario + */ + public function setupFrontendNodeControllerTrait(): void + { + $this->getObject(ContentCache::class)->flush(); + $this->getObject(\Neos\Neos\Testing\TestingFusionAutoIncludeHandler::class)->reset(); + $this->frontendNodeControllerResponse = null; + } + + /** + * @When the Fusion code for package :package is: + */ + public function iHaveTheFollowingFusionCodeForTheSite(PyStringNode $fusionCode, string $package) + { + $testingFusionHandler = $this->getObject(\Neos\Neos\Testing\TestingFusionAutoIncludeHandler::class); + $testingFusionHandler->setFusionForPackage($package, \Neos\Fusion\Core\FusionSourceCodeCollection::fromString($fusionCode->getRaw())); + } + + /** + * @When I dispatch the following request :requestUri + */ + public function iDispatchTheFollowingRequest(string $requestUri) + { + $testingFusionHandler = $this->getObject(\Neos\Neos\Testing\TestingFusionAutoIncludeHandler::class); + $testingFusionHandler->setIncludeFusionPackage('Neos.Fusion'); + $testingFusionHandler->setIncludeFusionPackage('Neos.Neos'); + + $httpRequest = $this->getObject(ServerRequestFactoryInterface::class)->createServerRequest('GET', $requestUri); + + $this->frontendNodeControllerResponse = $this->getObject(\Neos\Flow\Http\Middleware\MiddlewaresChain::class)->handle( + $httpRequest + ); + } + + /** + * @Then I expect the following response header: + */ + public function iExpectTheFollowingResponseHeader(PyStringNode $expectedResult): void + { + Assert::assertNotNull($this->frontendNodeControllerResponse); + Assert::assertSame($expectedResult->getRaw(), $this->frontendNodeControllerResponse->getBody()->getContents()); + } + + /** + * @Then I expect the following response: + */ + public function iExpectTheFollowingResponse(PyStringNode $expectedResult): void + { + Assert::assertNotNull($this->frontendNodeControllerResponse); + Assert::assertEquals($expectedResult->getRaw(), str_replace("\r\n", "\n", Message::toString($this->frontendNodeControllerResponse))); + } +} From fb4a3e889a94e39683fe05af7e656cef587a5887 Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Sat, 26 Oct 2024 13:20:34 +0200 Subject: [PATCH 043/214] FEATURE: EditNodePrivilege! --- .../Classes/Domain/Model/NodePermissions.php | 60 +++++++++++ .../ContentRepositoryAuthorizationService.php | 100 ++++++++++++++---- ...p => AbstractSubtreeTagBasedPrivilege.php} | 19 ++-- .../Privilege/EditNodePrivilege.php | 28 +++++ .../Privilege/ReadNodePrivilege.php | 28 +++++ .../Privilege/SubtreeTagPrivilegeSubject.php | 14 ++- .../ContentRepositoryAuthProvider.php | 30 ++++++ Neos.Neos/Configuration/Policy.yaml | 2 +- 8 files changed, 248 insertions(+), 33 deletions(-) create mode 100644 Neos.Neos/Classes/Domain/Model/NodePermissions.php rename Neos.Neos/Classes/Security/Authorization/Privilege/{SubtreeTagPrivilege.php => AbstractSubtreeTagBasedPrivilege.php} (78%) create mode 100644 Neos.Neos/Classes/Security/Authorization/Privilege/EditNodePrivilege.php create mode 100644 Neos.Neos/Classes/Security/Authorization/Privilege/ReadNodePrivilege.php diff --git a/Neos.Neos/Classes/Domain/Model/NodePermissions.php b/Neos.Neos/Classes/Domain/Model/NodePermissions.php new file mode 100644 index 00000000000..760300720b2 --- /dev/null +++ b/Neos.Neos/Classes/Domain/Model/NodePermissions.php @@ -0,0 +1,60 @@ +reason; + } +} diff --git a/Neos.Neos/Classes/Security/Authorization/ContentRepositoryAuthorizationService.php b/Neos.Neos/Classes/Security/Authorization/ContentRepositoryAuthorizationService.php index 58717299d95..b6ebc7a8994 100644 --- a/Neos.Neos/Classes/Security/Authorization/ContentRepositoryAuthorizationService.php +++ b/Neos.Neos/Classes/Security/Authorization/ContentRepositoryAuthorizationService.php @@ -5,21 +5,26 @@ namespace Neos\Neos\Security\Authorization; use Neos\ContentRepository\Core\Feature\SubtreeTagging\Dto\SubtreeTags; +use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; +use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Annotations as Flow; use Neos\Flow\Security\Account; use Neos\Flow\Security\Authorization\PrivilegeManagerInterface; use Neos\Flow\Security\Policy\PolicyService; use Neos\Flow\Security\Policy\Role; +use Neos\Neos\Domain\Model\NodePermissions; use Neos\Neos\Domain\Model\User; use Neos\Neos\Domain\Model\WorkspacePermissions; use Neos\Neos\Domain\Model\WorkspaceRole; use Neos\Neos\Domain\Model\WorkspaceRoleSubject; use Neos\Neos\Domain\Model\WorkspaceRoleSubjects; use Neos\Neos\Domain\Service\WorkspaceService; -use Neos\Neos\Security\Authorization\Privilege\SubtreeTagPrivilege; +use Neos\Neos\Security\Authorization\Privilege\EditNodePrivilege; +use Neos\Neos\Security\Authorization\Privilege\ReadNodePrivilege; use Neos\Neos\Security\Authorization\Privilege\SubtreeTagPrivilegeSubject; use Neos\Party\Domain\Service\PartyService; @@ -36,10 +41,10 @@ private const FLOW_ROLE_AUTHENTICATED_USER = 'Neos.Flow:AuthenticatedUser'; private const FLOW_ROLE_NEOS_ADMINISTRATOR = 'Neos.Neos:Administrator'; - public function __construct( private PartyService $partyService, private WorkspaceService $workspaceService, + private ContentRepositoryRegistry $contentRepositoryRegistry, private PolicyService $policyService, private PrivilegeManagerInterface $privilegeManager, ) { @@ -95,23 +100,25 @@ public function getWorkspacePermissionsForAccount(ContentRepositoryId $contentRe ); } + public function getNodePermissionsForAnonymousUser(Node|NodeAddress $node): NodePermissions + { + $roles = $this->rolesOfAnonymousUser(); + return $this->nodePermissionsForRoles($node, $roles); + } + + public function getNodePermissionsForAccount(Node|NodeAddress $node, Account $account): NodePermissions + { + $roles = $this->expandAccountRoles($account); + return $this->nodePermissionsForRoles($node, $roles); + } + /** * Determines the default {@see VisibilityConstraints} for an anonymous user (aka "public access") */ public function getVisibilityConstraintsForAnonymousUser(ContentRepositoryId $contentRepositoryId): VisibilityConstraints { - $roles = [ - self::FLOW_ROLE_EVERYBODY => $this->policyService->getRole(self::FLOW_ROLE_EVERYBODY), - self::FLOW_ROLE_ANONYMOUS => $this->policyService->getRole(self::FLOW_ROLE_ANONYMOUS), - ]; - $restrictedSubtreeTags = []; - /** @var SubtreeTagPrivilege $privilege */ - foreach ($this->policyService->getAllPrivilegesByType(SubtreeTagPrivilege::class) as $privilege) { - if (!$this->privilegeManager->isGrantedForRoles($roles, SubtreeTagPrivilege::class, new SubtreeTagPrivilegeSubject($privilege->getSubtreeTag(), $contentRepositoryId))) { - $restrictedSubtreeTags[] = $privilege->getSubtreeTag(); - } - } - return new VisibilityConstraints(SubtreeTags::fromArray($restrictedSubtreeTags)); + $roles = $this->rolesOfAnonymousUser(); + return new VisibilityConstraints($this->restrictedSubtreeTagsForRoles($contentRepositoryId, $roles)); } /** @@ -120,18 +127,22 @@ public function getVisibilityConstraintsForAnonymousUser(ContentRepositoryId $co public function getVisibilityConstraintsForAccount(ContentRepositoryId $contentRepositoryId, Account $account): VisibilityConstraints { $roles = $this->expandAccountRoles($account); - $restrictedSubtreeTags = []; - /** @var SubtreeTagPrivilege $privilege */ - foreach ($this->policyService->getAllPrivilegesByType(SubtreeTagPrivilege::class) as $privilege) { - if (!$this->privilegeManager->isGrantedForRoles($roles, SubtreeTagPrivilege::class, new SubtreeTagPrivilegeSubject($privilege->getSubtreeTag(), $contentRepositoryId))) { - $restrictedSubtreeTags[] = $privilege->getSubtreeTag(); - } - } - return new VisibilityConstraints(SubtreeTags::fromArray($restrictedSubtreeTags)); + return new VisibilityConstraints($this->restrictedSubtreeTagsForRoles($contentRepositoryId, $roles)); } // ------------------------------ + /** + * @return array + */ + private function rolesOfAnonymousUser(): array + { + return [ + self::FLOW_ROLE_EVERYBODY => $this->policyService->getRole(self::FLOW_ROLE_EVERYBODY), + self::FLOW_ROLE_ANONYMOUS => $this->policyService->getRole(self::FLOW_ROLE_ANONYMOUS), + ]; + } + /** * @return array */ @@ -154,9 +165,54 @@ private function expandAccountRoles(Account $account): array return $roles; } + /** + * @param array $roles + */ + private function restrictedSubtreeTagsForRoles(ContentRepositoryId $contentRepositoryId, array $roles): SubtreeTags + { + $restrictedSubtreeTags = SubtreeTags::createEmpty(); + /** @var ReadNodePrivilege $privilege */ + foreach ($this->policyService->getAllPrivilegesByType(ReadNodePrivilege::class) as $privilege) { + if (!$this->privilegeManager->isGrantedForRoles($roles, ReadNodePrivilege::class, new SubtreeTagPrivilegeSubject($privilege->getSubtreeTags(), $contentRepositoryId))) { + $restrictedSubtreeTags = $restrictedSubtreeTags->merge($privilege->getSubtreeTags()); + } + } + return $restrictedSubtreeTags; + } + private function neosUserFromAccount(Account $account): ?User { $user = $this->partyService->getAssignedPartyOfAccount($account); return $user instanceof User ? $user : null; } + + /** + * @param array $roles + */ + private function nodePermissionsForRoles(Node|NodeAddress $node, array $roles): NodePermissions + { + if ($node instanceof NodeAddress) { + $converted = $this->nodeForNodeAddress($node); + if ($converted === null) { + return NodePermissions::none(sprintf('Node "%s" not found in Content Repository "%s"', $node->aggregateId->value, $node->contentRepositoryId->value)); + } + $node = $converted; + } + $subtreeTagPrivilegeSubject = new SubtreeTagPrivilegeSubject($node->tags->all(), $node->contentRepositoryId); + $readGranted = $this->privilegeManager->isGrantedForRoles($roles, ReadNodePrivilege::class, $subtreeTagPrivilegeSubject, $readReason); + $writeGranted = $this->privilegeManager->isGrantedForRoles($roles, EditNodePrivilege::class, $subtreeTagPrivilegeSubject, $writeReason); + return NodePermissions::create( + read: $readGranted, + edit: $writeGranted, + reason: $readReason . "\n" . $writeReason, + ); + } + + private function nodeForNodeAddress(NodeAddress $nodeAddress): ?Node + { + return $this->contentRepositoryRegistry->get($nodeAddress->contentRepositoryId) + ->getContentGraph($nodeAddress->workspaceName) + ->getSubgraph($nodeAddress->dimensionSpacePoint, VisibilityConstraints::withoutRestrictions()) + ->findNodeById($nodeAddress->aggregateId); + } } diff --git a/Neos.Neos/Classes/Security/Authorization/Privilege/SubtreeTagPrivilege.php b/Neos.Neos/Classes/Security/Authorization/Privilege/AbstractSubtreeTagBasedPrivilege.php similarity index 78% rename from Neos.Neos/Classes/Security/Authorization/Privilege/SubtreeTagPrivilege.php rename to Neos.Neos/Classes/Security/Authorization/Privilege/AbstractSubtreeTagBasedPrivilege.php index da4eb47357c..8aaea9f1d35 100644 --- a/Neos.Neos/Classes/Security/Authorization/Privilege/SubtreeTagPrivilege.php +++ b/Neos.Neos/Classes/Security/Authorization/Privilege/AbstractSubtreeTagBasedPrivilege.php @@ -15,6 +15,7 @@ namespace Neos\Neos\Security\Authorization\Privilege; use Neos\ContentRepository\Core\Feature\SubtreeTagging\Dto\SubtreeTag; +use Neos\ContentRepository\Core\Feature\SubtreeTagging\Dto\SubtreeTags; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\Flow\Security\Authorization\Privilege\AbstractPrivilege; use Neos\Flow\Security\Authorization\Privilege\PrivilegeSubjectInterface; @@ -23,14 +24,15 @@ /** * TODO docs */ -class SubtreeTagPrivilege extends AbstractPrivilege +abstract class AbstractSubtreeTagBasedPrivilege extends AbstractPrivilege { - private SubtreeTag|null $subtreeTagRuntimeCache = null; + private bool $initialized = false; + private SubtreeTags|null $subtreeTagsRuntimeCache = null; private ContentRepositoryId|null $contentRepositoryIdRuntimeCache = null; private function initialize(): void { - if ($this->subtreeTagRuntimeCache !== null) { + if ($this->initialized) { return; } $subtreeTag = $this->getParsedMatcher(); @@ -38,7 +40,8 @@ private function initialize(): void [$contentRepositoryId, $subtreeTag] = explode(':', $subtreeTag); $this->contentRepositoryIdRuntimeCache = ContentRepositoryId::fromString($contentRepositoryId); } - $this->subtreeTagRuntimeCache = SubtreeTag::fromString($subtreeTag); + $this->subtreeTagsRuntimeCache = SubtreeTags::fromStrings($subtreeTag); + $this->initialized = true; } /** @@ -57,14 +60,14 @@ public function matchesSubject(PrivilegeSubjectInterface $subject): bool if ($contentRepositoryId !== null && $subject->contentRepositoryId !== null && !$contentRepositoryId->equals($subject->contentRepositoryId)) { return false; } - return $subject->subTreeTag->equals($this->getSubtreeTag()); + return !$this->getSubtreeTags()->intersection($subject->subTreeTags)->isEmpty(); } - public function getSubtreeTag(): SubtreeTag + public function getSubtreeTags(): SubtreeTags { $this->initialize(); - assert($this->subtreeTagRuntimeCache !== null); - return $this->subtreeTagRuntimeCache; + assert($this->subtreeTagsRuntimeCache !== null); + return $this->subtreeTagsRuntimeCache; } public function getContentRepositoryId(): ?ContentRepositoryId diff --git a/Neos.Neos/Classes/Security/Authorization/Privilege/EditNodePrivilege.php b/Neos.Neos/Classes/Security/Authorization/Privilege/EditNodePrivilege.php new file mode 100644 index 00000000000..52d94c0516b --- /dev/null +++ b/Neos.Neos/Classes/Security/Authorization/Privilege/EditNodePrivilege.php @@ -0,0 +1,28 @@ +subTreeTags->count() > 1 ? 's' : '') . ' "' . implode('", "', $this->subTreeTags->toStringArray()) . '"'; + if ($this->contentRepositoryId !== null) { + $label .= ' in Content Repository "' . $this->contentRepositoryId->value . '"'; + } + return $label; + } } diff --git a/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php index 67ece65438a..c1fc1710a1a 100644 --- a/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php +++ b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php @@ -5,10 +5,14 @@ namespace Neos\Neos\Security\ContentRepositoryAuthProvider; use Neos\ContentRepository\Core\CommandHandler\CommandInterface; +use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\Feature\Common\RebasableToOtherWorkspaceInterface; +use Neos\ContentRepository\Core\Feature\NodeModification\Command\SetNodeProperties; +use Neos\ContentRepository\Core\Feature\NodeModification\Command\SetSerializedNodeProperties; use Neos\ContentRepository\Core\Feature\Security\AuthProviderInterface; use Neos\ContentRepository\Core\Feature\Security\Dto\Privilege; use Neos\ContentRepository\Core\Feature\Security\Dto\UserId; +use Neos\ContentRepository\Core\Feature\SubtreeTagging\Dto\SubtreeTag; use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Command\CreateRootWorkspace; use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Command\CreateWorkspace; use Neos\ContentRepository\Core\Feature\WorkspaceModification\Command\ChangeBaseWorkspace; @@ -20,8 +24,11 @@ use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Command\RebaseWorkspace; use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; +use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Security\Context as SecurityContext; +use Neos\Neos\Domain\Model\NodePermissions; use Neos\Neos\Domain\Model\WorkspacePermissions; use Neos\Neos\Domain\Service\UserService; use Neos\Neos\Security\Authorization\ContentRepositoryAuthorizationService; @@ -82,6 +89,20 @@ public function canExecuteCommand(CommandInterface $command): Privilege if ($this->securityContext->areAuthorizationChecksDisabled()) { return Privilege::granted('Authorization checks are disabled'); } + if ($command instanceof SetNodeProperties) { + $nodePermissions = $this->getNodePermissionsForCurrentUser( + NodeAddress::create( + $this->contentRepositoryId, + $command->workspaceName, + $command->originDimensionSpacePoint->toDimensionSpacePoint(), + $command->nodeAggregateId, + ) + ); + if (!$nodePermissions->edit) { + return Privilege::denied($nodePermissions->getReason()); + } + return $this->requireWorkspacePermission($command->workspaceName, self::WORKSPACE_PERMISSION_WRITE); + } // Note: We check against the {@see RebasableToOtherWorkspaceInterface} because that is implemented by all // commands that interact with nodes on a content stream. With that it's likely that we don't have to adjust the @@ -134,4 +155,13 @@ private function getWorkspacePermissionsForCurrentUser(WorkspaceName $workspaceN } return $this->authorizationService->getWorkspacePermissionsForAccount($this->contentRepositoryId, $workspaceName, $authenticatedAccount); } + + private function getNodePermissionsForCurrentUser(NodeAddress $nodeAddress): NodePermissions + { + $authenticatedAccount = $this->securityContext->getAccount(); + if ($authenticatedAccount === null) { + return $this->authorizationService->getNodePermissionsForAnonymousUser($nodeAddress); + } + return $this->authorizationService->getNodePermissionsForAccount($nodeAddress, $authenticatedAccount); + } } diff --git a/Neos.Neos/Configuration/Policy.yaml b/Neos.Neos/Configuration/Policy.yaml index 32fed1bd759..c441d522255 100644 --- a/Neos.Neos/Configuration/Policy.yaml +++ b/Neos.Neos/Configuration/Policy.yaml @@ -143,7 +143,7 @@ privilegeTargets: matcher: 'administration/dimensions' - 'Neos\Neos\Security\Authorization\Privilege\SubtreeTagPrivilege': + 'Neos\Neos\Security\Authorization\Privilege\ReadNodePrivilege': 'Neos.Neos:ContentRepository.ReadDisabledNodes': # !!! matcher payload in this case is a ContentRepository SubtreeTag, From c05e53d94a77fb997d23011b667eec901babe4b9 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 27 Oct 2024 19:01:15 +0100 Subject: [PATCH 044/214] TASK: Dont expose `$visibilityConstraints` in `SiteNodeUtility` --- .../Domain/Service/SiteNodeUtility.php | 27 +++++-------------- .../Classes/View/FusionExceptionView.php | 3 +-- 2 files changed, 8 insertions(+), 22 deletions(-) diff --git a/Neos.Neos/Classes/Domain/Service/SiteNodeUtility.php b/Neos.Neos/Classes/Domain/Service/SiteNodeUtility.php index 262d24e4461..bbcfbee80cd 100644 --- a/Neos.Neos/Classes/Domain/Service/SiteNodeUtility.php +++ b/Neos.Neos/Classes/Domain/Service/SiteNodeUtility.php @@ -17,19 +17,15 @@ use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; -use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Annotations as Flow; use Neos\Neos\Domain\Model\Site; use Neos\Neos\Domain\Repository\SiteRepository; -use Neos\Neos\Utility\NodeTypeWithFallbackProvider; #[Flow\Scope('singleton')] final class SiteNodeUtility { - use NodeTypeWithFallbackProvider; - public function __construct( private readonly ContentRepositoryRegistry $contentRepositoryRegistry ) { @@ -44,8 +40,7 @@ public function __construct( * $siteNode = $this->siteNodeUtility->findSiteNodeBySite( * $site, * WorkspaceName::forLive(), - * DimensionSpacePoint::createWithoutDimensions(), - * VisibilityConstraints::frontend() + * DimensionSpacePoint::createWithoutDimensions() * ); * ``` * @@ -54,26 +49,18 @@ public function __construct( public function findSiteNodeBySite( Site $site, WorkspaceName $workspaceName, - DimensionSpacePoint $dimensionSpacePoint, - VisibilityConstraints $visibilityConstraints + DimensionSpacePoint $dimensionSpacePoint ): Node { $contentRepository = $this->contentRepositoryRegistry->get($site->getConfiguration()->contentRepositoryId); - $contentGraph = $contentRepository->getContentGraph($workspaceName); - $subgraph = $contentGraph->getSubgraph( - $dimensionSpacePoint, - $visibilityConstraints, - ); + $subgraph = $contentRepository->getContentSubgraph($workspaceName, $dimensionSpacePoint); - $rootNodeAggregate = $contentGraph->findRootNodeAggregateByType( - NodeTypeNameFactory::forSites() - ); - if (!$rootNodeAggregate) { + $rootNode = $subgraph->findRootNodeByType(NodeTypeNameFactory::forSites()); + + if (!$rootNode) { throw new \RuntimeException(sprintf('No sites root node found in content repository "%s", while fetching site node "%s"', $contentRepository->id->value, $site->getNodeName()), 1719046570); } - $rootNode = $rootNodeAggregate->getNodeByCoveredDimensionSpacePoint($dimensionSpacePoint); - $siteNode = $subgraph->findNodeByPath( $site->getNodeName()->toNodeName(), $rootNode->aggregateId @@ -83,7 +70,7 @@ public function findSiteNodeBySite( throw new \RuntimeException(sprintf('No site node found for site "%s"', $site->getNodeName()), 1697140379); } - if (!$this->getNodeType($siteNode)->isOfType(NodeTypeNameFactory::NAME_SITE)) { + if (!$contentRepository->getNodeTypeManager()->getNodeType($siteNode->nodeTypeName)?->isOfType(NodeTypeNameFactory::NAME_SITE)) { throw new \RuntimeException(sprintf( 'The site node "%s" (type: "%s") must be of type "%s"', $siteNode->aggregateId->value, diff --git a/Neos.Neos/Classes/View/FusionExceptionView.php b/Neos.Neos/Classes/View/FusionExceptionView.php index 041ec02cb67..abdd8fd7d95 100644 --- a/Neos.Neos/Classes/View/FusionExceptionView.php +++ b/Neos.Neos/Classes/View/FusionExceptionView.php @@ -122,8 +122,7 @@ public function render(): ResponseInterface|StreamInterface $currentSiteNode = $this->siteNodeUtility->findSiteNodeBySite( $site, WorkspaceName::forLive(), - $arbitraryRootDimensionSpacePoint, - VisibilityConstraints::default() + $arbitraryRootDimensionSpacePoint ); } catch (WorkspaceDoesNotExist | \RuntimeException) { return $this->renderErrorWelcomeScreen(); From 0bf97e2779edc863178ac1882a0c6328fa5a62b5 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 27 Oct 2024 19:03:04 +0100 Subject: [PATCH 045/214] TASK: Use current user visibility constraints for data source controller --- .../Service/Controller/DataSourceController.php | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Neos.Neos/Classes/Service/Controller/DataSourceController.php b/Neos.Neos/Classes/Service/Controller/DataSourceController.php index db29b994b7b..265d151c766 100644 --- a/Neos.Neos/Classes/Service/Controller/DataSourceController.php +++ b/Neos.Neos/Classes/Service/Controller/DataSourceController.php @@ -15,7 +15,6 @@ namespace Neos\Neos\Service\Controller; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; -use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Annotations as Flow; @@ -68,12 +67,12 @@ public function indexAction($dataSourceIdentifier, string $node = null): void unset($arguments['dataSourceIdentifier']); unset($arguments['node']); - $values = $dataSource->getData($this->deserializeNodeFromLegacyAddress($node), $arguments); + $values = $dataSource->getData($this->deserializeNodeFromNodeAddress($node), $arguments); $this->view->assign('value', $values); } - private function deserializeNodeFromLegacyAddress(?string $stringFormattedNodeAddress): ?Node + private function deserializeNodeFromNodeAddress(?string $stringFormattedNodeAddress): ?Node { if (!$stringFormattedNodeAddress) { return null; @@ -82,10 +81,8 @@ private function deserializeNodeFromLegacyAddress(?string $stringFormattedNodeAd $nodeAddress = NodeAddress::fromJsonString($stringFormattedNodeAddress); $contentRepository = $this->contentRepositoryRegistry->get($nodeAddress->contentRepositoryId); - return $contentRepository->getContentGraph($nodeAddress->workspaceName)->getSubgraph( - $nodeAddress->dimensionSpacePoint, - VisibilityConstraints::withoutRestrictions() - )->findNodeById($nodeAddress->aggregateId); + return $contentRepository->getContentSubgraph($nodeAddress->workspaceName, $nodeAddress->dimensionSpacePoint) + ->findNodeById($nodeAddress->aggregateId); } /** From a9fe6ceb980eaddaddefe3e0ae088f7e2a965965 Mon Sep 17 00:00:00 2001 From: Martin Ficzel Date: Mon, 28 Oct 2024 19:04:04 +0100 Subject: [PATCH 046/214] Adjust after QS Feedback: - Add identifier "neos.cr.internal" for ignored internal calls - Reuse shapes from Export in Import - Remove comment about extensibility via configuration --- .../Classes/PhpstanRules/ApiOrInternalAnnotationRule.php | 2 +- ...InternalMethodsNotAllowedOutsideContentRepositoryRule.php | 2 +- .../Classes/Domain/Import/LiveWorkspaceIsEmptyProcessor.php | 2 +- Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php | 5 +++-- Neos.Neos/Classes/Domain/Service/SiteExportService.php | 1 - Neos.Neos/Classes/Domain/Service/SiteImportService.php | 1 - Neos.Neos/Classes/Domain/Service/SitePruningService.php | 1 - composer.json | 4 ++-- 8 files changed, 8 insertions(+), 10 deletions(-) diff --git a/Neos.ContentRepository.BehavioralTests/Classes/PhpstanRules/ApiOrInternalAnnotationRule.php b/Neos.ContentRepository.BehavioralTests/Classes/PhpstanRules/ApiOrInternalAnnotationRule.php index 1f76b0e16bd..3821a0f9f2e 100644 --- a/Neos.ContentRepository.BehavioralTests/Classes/PhpstanRules/ApiOrInternalAnnotationRule.php +++ b/Neos.ContentRepository.BehavioralTests/Classes/PhpstanRules/ApiOrInternalAnnotationRule.php @@ -54,7 +54,7 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message( 'Class needs @api or @internal annotation.' - )->build(), + )->identifier('neos.cr.internal')->build(), ]; } return []; diff --git a/Neos.ContentRepository.BehavioralTests/Classes/PhpstanRules/InternalMethodsNotAllowedOutsideContentRepositoryRule.php b/Neos.ContentRepository.BehavioralTests/Classes/PhpstanRules/InternalMethodsNotAllowedOutsideContentRepositoryRule.php index 5dae4e82dfc..7334cfa78db 100644 --- a/Neos.ContentRepository.BehavioralTests/Classes/PhpstanRules/InternalMethodsNotAllowedOutsideContentRepositoryRule.php +++ b/Neos.ContentRepository.BehavioralTests/Classes/PhpstanRules/InternalMethodsNotAllowedOutsideContentRepositoryRule.php @@ -74,7 +74,7 @@ public function processNode(Node $node, Scope $scope): array $targetClassName, $node->name->toString() ) - )->build(), + )->identifier('neos.cr.internal')->build(), ]; } } diff --git a/Neos.Neos/Classes/Domain/Import/LiveWorkspaceIsEmptyProcessor.php b/Neos.Neos/Classes/Domain/Import/LiveWorkspaceIsEmptyProcessor.php index 3aee0df7594..6d69c4f771e 100644 --- a/Neos.Neos/Classes/Domain/Import/LiveWorkspaceIsEmptyProcessor.php +++ b/Neos.Neos/Classes/Domain/Import/LiveWorkspaceIsEmptyProcessor.php @@ -43,7 +43,7 @@ public function run(ProcessingContext $context): void private function workspaceHasEvents(WorkspaceName $workspaceName): bool { - /** @phpstan-ignore-next-line internal method of the cr is called */ + /** @phpstan-ignore neos.cr.internal */ $workspaceStreamName = WorkspaceEventStreamName::fromWorkspaceName($workspaceName)->getEventStreamName(); $eventStream = $this->eventStore->load($workspaceStreamName); foreach ($eventStream as $event) { diff --git a/Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php b/Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php index 947011aea80..d1a1a06d3ba 100644 --- a/Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php +++ b/Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php @@ -21,6 +21,7 @@ use Neos\ContentRepository\Export\ProcessorInterface; use Neos\ContentRepository\Export\Severity; use Neos\Flow\Persistence\PersistenceManagerInterface; +use Neos\Neos\Domain\Export\SiteExportProcessor; use Neos\Neos\Domain\Model\Domain; use Neos\Neos\Domain\Model\Site; use Neos\Neos\Domain\Repository\DomainRepository; @@ -29,8 +30,8 @@ /** * Import processor that creates and persists a Neos {@see Site} instance * - * @phpstan-type DomainShape array{hostname: string, scheme?: ?string, port?: ?int, active?: ?bool, primary?: ?bool } - * @phpstan-type SiteShape array{name:string, siteResourcesPackageKey:string, nodeName?: string, online?:bool, domains?: ?DomainShape[] } + * @phpstan-import-type DomainShape from SiteExportProcessor + * @phpstan-import-type SiteShape from SiteExportProcessor */ final readonly class SiteCreationProcessor implements ProcessorInterface { diff --git a/Neos.Neos/Classes/Domain/Service/SiteExportService.php b/Neos.Neos/Classes/Domain/Service/SiteExportService.php index 87c581edea9..913351c5112 100644 --- a/Neos.Neos/Classes/Domain/Service/SiteExportService.php +++ b/Neos.Neos/Classes/Domain/Service/SiteExportService.php @@ -59,7 +59,6 @@ public function exportToPath(ContentRepositoryId $contentRepositoryId, string $p throw new \RuntimeException('Failed to find live workspace', 1716652280); } - // TODO make configurable (?) /** @var array $processors */ $processors = [ 'Exporting events' => $this->contentRepositoryRegistry->buildService( diff --git a/Neos.Neos/Classes/Domain/Service/SiteImportService.php b/Neos.Neos/Classes/Domain/Service/SiteImportService.php index 67c2e457d19..483dd2056df 100644 --- a/Neos.Neos/Classes/Domain/Service/SiteImportService.php +++ b/Neos.Neos/Classes/Domain/Service/SiteImportService.php @@ -71,7 +71,6 @@ public function importFromPath(ContentRepositoryId $contentRepositoryId, string $context = new ProcessingContext($filesystem, $onMessage); $contentRepository = $this->contentRepositoryRegistry->get($contentRepositoryId); - // TODO make configurable (?) /** @var array $processors */ $processors = [ 'Run doctrine migrations' => new DoctrineMigrateProcessor($this->doctrineService), diff --git a/Neos.Neos/Classes/Domain/Service/SitePruningService.php b/Neos.Neos/Classes/Domain/Service/SitePruningService.php index bb082eff395..15eabcf7911 100644 --- a/Neos.Neos/Classes/Domain/Service/SitePruningService.php +++ b/Neos.Neos/Classes/Domain/Service/SitePruningService.php @@ -67,7 +67,6 @@ public function pruneAll(ContentRepositoryId $contentRepositoryId, \Closure $onP $filesystem = new Filesystem(new LocalFilesystemAdapter('.')); $context = new ProcessingContext($filesystem, $onMessage); - // TODO make configurable (?) /** @var array $processors */ $processors = [ 'Remove site nodes' => $this->contentRepositoryRegistry->buildService( diff --git a/composer.json b/composer.json index 4425532f575..5c059f778c1 100644 --- a/composer.json +++ b/composer.json @@ -96,7 +96,7 @@ "scripts": { "lint:phpcs": "../../bin/phpcs --colors", "lint:phpcs:fix": "../../bin/phpcbf --colors", - "lint:phpstan": "../../bin/phpstan analyse", + "lint:phpstan": "../../bin/phpstan analyse -v", "lint:phpstan-generate-baseline": "../../bin/phpstan analyse --generate-baseline", "lint:distributionintegrity": "[ -d 'Neos.ContentRepository' ] && { echo 'Package Neos.ContentRepository should not exist.' 1>&2; exit 1; } || exit 0;", "lint": [ @@ -293,7 +293,7 @@ }, "require-dev": { "roave/security-advisories": "dev-latest", - "phpstan/phpstan": "^1.8", + "phpstan/phpstan": "^1.11", "squizlabs/php_codesniffer": "^3.6", "phpunit/phpunit": "^9.0", "neos/behat": "*", From 14c9f84fd0b899d84ab17dc837d356f7a5494581 Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Tue, 29 Oct 2024 14:51:52 +0100 Subject: [PATCH 047/214] Introduce named `VisibilityConstraints` constructor --- .../Projection/ContentGraph/VisibilityConstraints.php | 10 +++++++++- .../ContentRepositoryAuthorizationService.php | 6 +++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/VisibilityConstraints.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/VisibilityConstraints.php index 598b9945a15..f1c4f3b93ae 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/VisibilityConstraints.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/VisibilityConstraints.php @@ -29,11 +29,19 @@ /** * @param SubtreeTags $tagConstraints A set of {@see SubtreeTag} instances that will be _excluded_ from the results of any content graph query */ - public function __construct( + private function __construct( public SubtreeTags $tagConstraints, ) { } + /** + * @param SubtreeTags $tagConstraints A set of {@see SubtreeTag} instances that will be _excluded_ from the results of any content graph query + */ + public static function fromTagConstraints(SubtreeTags $tagConstraints): self + { + return new self($tagConstraints); + } + public function getHash(): string { return md5(implode('|', $this->tagConstraints->toStringArray())); diff --git a/Neos.Neos/Classes/Security/Authorization/ContentRepositoryAuthorizationService.php b/Neos.Neos/Classes/Security/Authorization/ContentRepositoryAuthorizationService.php index b6ebc7a8994..12bc6b5e0bb 100644 --- a/Neos.Neos/Classes/Security/Authorization/ContentRepositoryAuthorizationService.php +++ b/Neos.Neos/Classes/Security/Authorization/ContentRepositoryAuthorizationService.php @@ -118,7 +118,7 @@ public function getNodePermissionsForAccount(Node|NodeAddress $node, Account $ac public function getVisibilityConstraintsForAnonymousUser(ContentRepositoryId $contentRepositoryId): VisibilityConstraints { $roles = $this->rolesOfAnonymousUser(); - return new VisibilityConstraints($this->restrictedSubtreeTagsForRoles($contentRepositoryId, $roles)); + return VisibilityConstraints::fromTagConstraints($this->tagConstraintsForRoles($contentRepositoryId, $roles)); } /** @@ -127,7 +127,7 @@ public function getVisibilityConstraintsForAnonymousUser(ContentRepositoryId $co public function getVisibilityConstraintsForAccount(ContentRepositoryId $contentRepositoryId, Account $account): VisibilityConstraints { $roles = $this->expandAccountRoles($account); - return new VisibilityConstraints($this->restrictedSubtreeTagsForRoles($contentRepositoryId, $roles)); + return VisibilityConstraints::fromTagConstraints($this->tagConstraintsForRoles($contentRepositoryId, $roles)); } // ------------------------------ @@ -168,7 +168,7 @@ private function expandAccountRoles(Account $account): array /** * @param array $roles */ - private function restrictedSubtreeTagsForRoles(ContentRepositoryId $contentRepositoryId, array $roles): SubtreeTags + private function tagConstraintsForRoles(ContentRepositoryId $contentRepositoryId, array $roles): SubtreeTags { $restrictedSubtreeTags = SubtreeTags::createEmpty(); /** @var ReadNodePrivilege $privilege */ From ac70e83fd8d9889858586526c87e7aa595c4bb2c Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Tue, 29 Oct 2024 14:53:10 +0100 Subject: [PATCH 048/214] Adjust Behat tests to refer to Visibility constraints "default" instead of "frontend" --- ...bleNodeAggregate_WithoutDimensions.feature | 2 +- ...isableNodeAggregate_WithDimensions.feature | 20 ++++++------- ...bleNodeAggregate_WithoutDimensions.feature | 6 ++-- ...EnableNodeAggregate_WithDimensions.feature | 30 +++++++++---------- ...ithDisabledAncestor_WithDimensions.feature | 2 +- ...09-CreateNodeVariantOfDisabledNode.feature | 2 +- ...WithDisabledNodesWithoutDimensions.feature | 2 +- .../AddDimensionShineThrough.feature | 6 ++-- .../Migration/MoveDimensionSpacePoint.feature | 6 ++-- .../RemoveNodeAggregateAfterDisabling.feature | 2 +- .../Bootstrap/CRTestSuiteRuntimeVariables.php | 4 +-- 11 files changed, 41 insertions(+), 41 deletions(-) diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/02-DisableNodeAggregate_WithoutDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/02-DisableNodeAggregate_WithoutDimensions.feature index 67181a191e0..5f847014041 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/02-DisableNodeAggregate_WithoutDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/02-DisableNodeAggregate_WithoutDimensions.feature @@ -109,7 +109,7 @@ Feature: Disable a node aggregate And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document" to lead to node cs-identifier;nody-mc-nodeface;{} And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{} - When VisibilityConstraints are set to "frontend" + When VisibilityConstraints are set to "default" Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: | Name | NodeDiscriminator | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/03-DisableNodeAggregate_WithDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/03-DisableNodeAggregate_WithDimensions.feature index 356eccb0f7d..9574d8303d1 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/03-DisableNodeAggregate_WithDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/03-DisableNodeAggregate_WithDimensions.feature @@ -121,7 +121,7 @@ Feature: Disable a node aggregate And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document" to lead to node cs-identifier;nody-mc-nodeface;{"language":"mul"} And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"language":"mul"} - When VisibilityConstraints are set to "frontend" + When VisibilityConstraints are set to "default" Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: | Name | NodeDiscriminator | @@ -149,7 +149,7 @@ Feature: Disable a node aggregate # Tests for the generalization When I am in dimension space point {"language":"mul"} - And VisibilityConstraints are set to "frontend" + And VisibilityConstraints are set to "default" Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: | Name | NodeDiscriminator | @@ -196,7 +196,7 @@ Feature: Disable a node aggregate # Tests for the virtual specialization When I am in dimension space point {"language":"gsw"} - And VisibilityConstraints are set to "frontend" + And VisibilityConstraints are set to "default" Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: | Name | NodeDiscriminator | @@ -225,7 +225,7 @@ Feature: Disable a node aggregate # Tests for the real specialization When I am in dimension space point {"language":"ltz"} - And VisibilityConstraints are set to "frontend" + And VisibilityConstraints are set to "default" Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: | Name | NodeDiscriminator | @@ -254,7 +254,7 @@ Feature: Disable a node aggregate # Tests for the peer variant When I am in dimension space point {"language":"en"} - And VisibilityConstraints are set to "frontend" + And VisibilityConstraints are set to "default" Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: | Name | NodeDiscriminator | @@ -372,7 +372,7 @@ Feature: Disable a node aggregate And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document" to lead to node cs-identifier;nody-mc-nodeface;{"language":"mul"} And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"language":"mul"} - When VisibilityConstraints are set to "frontend" + When VisibilityConstraints are set to "default" Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: | Name | NodeDiscriminator | @@ -400,7 +400,7 @@ Feature: Disable a node aggregate # Tests for the generalization When I am in dimension space point {"language":"mul"} - And VisibilityConstraints are set to "frontend" + And VisibilityConstraints are set to "default" Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: | Name | NodeDiscriminator | @@ -429,7 +429,7 @@ Feature: Disable a node aggregate # Tests for the virtual specialization When I am in dimension space point {"language":"gsw"} - And VisibilityConstraints are set to "frontend" + And VisibilityConstraints are set to "default" Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: | Name | NodeDiscriminator | @@ -458,7 +458,7 @@ Feature: Disable a node aggregate # Tests for the real specialization When I am in dimension space point {"language":"ltz"} - And VisibilityConstraints are set to "frontend" + And VisibilityConstraints are set to "default" Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: | Name | NodeDiscriminator | @@ -487,7 +487,7 @@ Feature: Disable a node aggregate # Tests for the peer variant When I am in dimension space point {"language":"en"} - And VisibilityConstraints are set to "frontend" + And VisibilityConstraints are set to "default" Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: | Name | NodeDiscriminator | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/05-EnableNodeAggregate_WithoutDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/05-EnableNodeAggregate_WithoutDimensions.feature index 6a77ec9a4d5..63f7fe64cf7 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/05-EnableNodeAggregate_WithoutDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/05-EnableNodeAggregate_WithoutDimensions.feature @@ -69,7 +69,7 @@ Feature: Enable a node aggregate And I expect this node aggregate to disable dimension space points [] When I am in workspace "live" and dimension space point {} - And VisibilityConstraints are set to "frontend" + And VisibilityConstraints are set to "default" Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: | Name | NodeDiscriminator | @@ -144,7 +144,7 @@ Feature: Enable a node aggregate And I expect this node aggregate to disable dimension space points [{}] When I am in workspace "live" and dimension space point {} - And VisibilityConstraints are set to "frontend" + And VisibilityConstraints are set to "default" Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: | Name | NodeDiscriminator | @@ -219,7 +219,7 @@ Feature: Enable a node aggregate And I expect this node aggregate to disable dimension space points [] When I am in workspace "live" and dimension space point {} - And VisibilityConstraints are set to "frontend" + And VisibilityConstraints are set to "default" Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: | Name | NodeDiscriminator | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/06-EnableNodeAggregate_WithDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/06-EnableNodeAggregate_WithDimensions.feature index 49c6a27f297..34ff1cdcbd0 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/06-EnableNodeAggregate_WithDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/06-EnableNodeAggregate_WithDimensions.feature @@ -144,7 +144,7 @@ Feature: Enable a node aggregate And I expect node aggregate identifier "the-great-nodini" and node path "document/court-magician" to lead to node cs-identifier;the-great-nodini;{"language":"mul"} And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"language":"mul"} - When VisibilityConstraints are set to "frontend" + When VisibilityConstraints are set to "default" Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: | Name | NodeDiscriminator | @@ -194,7 +194,7 @@ Feature: Enable a node aggregate # Tests for the generalization When I am in dimension space point {"language":"mul"} - And VisibilityConstraints are set to "frontend" + And VisibilityConstraints are set to "default" Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: | Name | NodeDiscriminator | @@ -224,7 +224,7 @@ Feature: Enable a node aggregate # Tests for the virtual specialization When I am in dimension space point {"language":"gsw"} - And VisibilityConstraints are set to "frontend" + And VisibilityConstraints are set to "default" Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: | Name | NodeDiscriminator | @@ -275,7 +275,7 @@ Feature: Enable a node aggregate # Tests for the real specialization When I am in dimension space point {"language":"ltz"} - And VisibilityConstraints are set to "frontend" + And VisibilityConstraints are set to "default" Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: | Name | NodeDiscriminator | @@ -326,7 +326,7 @@ Feature: Enable a node aggregate # Tests for the peer variant When I am in dimension space point {"language":"en"} - And VisibilityConstraints are set to "frontend" + And VisibilityConstraints are set to "default" Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: | Name | NodeDiscriminator | @@ -439,7 +439,7 @@ Feature: Enable a node aggregate And I expect node aggregate identifier "the-great-nodini" and node path "document/court-magician" to lead to node cs-identifier;the-great-nodini;{"language":"mul"} And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"language":"mul"} - When VisibilityConstraints are set to "frontend" + When VisibilityConstraints are set to "default" Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: | Name | NodeDiscriminator | @@ -489,7 +489,7 @@ Feature: Enable a node aggregate # Tests for the generalization When I am in dimension space point {"language":"mul"} - And VisibilityConstraints are set to "frontend" + And VisibilityConstraints are set to "default" Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: | Name | NodeDiscriminator | @@ -539,7 +539,7 @@ Feature: Enable a node aggregate # Tests for the virtual specialization When I am in dimension space point {"language":"gsw"} - And VisibilityConstraints are set to "frontend" + And VisibilityConstraints are set to "default" Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: | Name | NodeDiscriminator | @@ -590,7 +590,7 @@ Feature: Enable a node aggregate # Tests for the real specialization When I am in dimension space point {"language":"ltz"} - And VisibilityConstraints are set to "frontend" + And VisibilityConstraints are set to "default" Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: | Name | NodeDiscriminator | @@ -641,7 +641,7 @@ Feature: Enable a node aggregate # Tests for the peer variant When I am in dimension space point {"language":"en"} - And VisibilityConstraints are set to "frontend" + And VisibilityConstraints are set to "default" Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: | Name | NodeDiscriminator | @@ -717,25 +717,25 @@ Feature: Enable a node aggregate And I expect node aggregate identifier "the-great-nodini" and node path "document/court-magician" to lead to node cs-identifier;the-great-nodini;{"language":"mul"} And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"language":"mul"} - When VisibilityConstraints are set to "frontend" + When VisibilityConstraints are set to "default" Then I expect node aggregate identifier "the-great-nodini" and node path "document/court-magician" to lead to no node # Tests for the generalization When I am in dimension space point {"language":"mul"} - And VisibilityConstraints are set to "frontend" + And VisibilityConstraints are set to "default" Then I expect node aggregate identifier "the-great-nodini" and node path "document/court-magician" to lead to no node # Tests for the virtual specialization When I am in dimension space point {"language":"gsw"} - And VisibilityConstraints are set to "frontend" + And VisibilityConstraints are set to "default" Then I expect node aggregate identifier "the-great-nodini" and node path "document/court-magician" to lead to no node # Tests for the real specialization When I am in dimension space point {"language":"ltz"} - And VisibilityConstraints are set to "frontend" + And VisibilityConstraints are set to "default" Then I expect node aggregate identifier "the-great-nodini" and node path "document/court-magician" to lead to no node # Tests for the peer variant When I am in dimension space point {"language":"en"} - And VisibilityConstraints are set to "frontend" + And VisibilityConstraints are set to "default" Then I expect node aggregate identifier "the-great-nodini" and node path "document/court-magician" to lead to no node diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/08-CreateNodeAggregateWithNodeWithDisabledAncestor_WithDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/08-CreateNodeAggregateWithNodeWithDisabledAncestor_WithDimensions.feature index 0ae1ae2cdae..c3b0ef4813e 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/08-CreateNodeAggregateWithNodeWithDisabledAncestor_WithDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/08-CreateNodeAggregateWithNodeWithDisabledAncestor_WithDimensions.feature @@ -35,7 +35,7 @@ Feature: Creation of nodes underneath disabled nodes | nodeAggregateId | "the-great-nodini" | | sourceOrigin | {"language":"mul"} | | targetOrigin | {"language":"ltz"} | - And VisibilityConstraints are set to "frontend" + And VisibilityConstraints are set to "default" Scenario: Create a new node with parent disabled with strategy allSpecializations Given the command DisableNodeAggregate is executed with payload: diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/09-CreateNodeVariantOfDisabledNode.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/09-CreateNodeVariantOfDisabledNode.feature index be26f2395e0..be6c01a921c 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/09-CreateNodeVariantOfDisabledNode.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/09-CreateNodeVariantOfDisabledNode.feature @@ -24,7 +24,7 @@ Feature: Variation of hidden nodes | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | | nodeTypeName | "Neos.ContentRepository:Root" | - And VisibilityConstraints are set to "frontend" + And VisibilityConstraints are set to "default" Scenario: Specialize a node where the specialization target is enabled Given I am in dimension space point {"language":"de"} diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ContentStreamForking/ForkContentStreamWithDisabledNodesWithoutDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ContentStreamForking/ForkContentStreamWithDisabledNodesWithoutDimensions.feature index 780ef1d28ad..de97506aa49 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ContentStreamForking/ForkContentStreamWithDisabledNodesWithoutDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ContentStreamForking/ForkContentStreamWithDisabledNodesWithoutDimensions.feature @@ -75,7 +75,7 @@ Feature: On forking a content stream, hidden nodes should be correctly copied as | 1 | the-great-nodini | | 2 | nodingers-cat | - And VisibilityConstraints are set to "frontend" + And VisibilityConstraints are set to "default" Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node user-cs-identifier;lady-eleonode-rootford;{} And I expect this node to have no child nodes And the subtree for node aggregate "lady-eleonode-rootford" with node types "" and 2 levels deep should be: diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/AddDimensionShineThrough.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/AddDimensionShineThrough.feature index 3f6180cc362..8387081b091 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/AddDimensionShineThrough.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/AddDimensionShineThrough.feature @@ -142,7 +142,7 @@ Feature: Add Dimension Specialization Then I expect node aggregate identifier "sir-david-nodenborough" to lead to no node When VisibilityConstraints are set to "withoutRestrictions" Then I expect a node identified by cs-identifier;sir-david-nodenborough;{"language":"de"} to exist in the content graph - When VisibilityConstraints are set to "frontend" + When VisibilityConstraints are set to "default" # we change the dimension configuration When I change the content dimensions in content repository "default" to: @@ -166,14 +166,14 @@ Feature: Add Dimension Specialization Then I expect node aggregate identifier "sir-david-nodenborough" to lead to no node When VisibilityConstraints are set to "withoutRestrictions" Then I expect a node identified by cs-identifier;sir-david-nodenborough;{"language":"de"} to exist in the content graph - When VisibilityConstraints are set to "frontend" + When VisibilityConstraints are set to "default" # The visibility edges were modified When I am in workspace "migration-workspace" and dimension space point {"language": "ch"} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to no node When VisibilityConstraints are set to "withoutRestrictions" Then I expect a node identified by cs-identifier;sir-david-nodenborough;{"language":"de"} to exist in the content graph - When VisibilityConstraints are set to "frontend" + When VisibilityConstraints are set to "default" When I run integrity violation detection Then I expect the integrity violation detection result to contain exactly 0 errors diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/MoveDimensionSpacePoint.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/MoveDimensionSpacePoint.feature index f9a70788f8b..53dfbb58e86 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/MoveDimensionSpacePoint.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/MoveDimensionSpacePoint.feature @@ -94,7 +94,7 @@ Feature: Move dimension space point Then I expect node aggregate identifier "sir-david-nodenborough" to lead to no node When VisibilityConstraints are set to "withoutRestrictions" Then I expect a node identified by cs-identifier;sir-david-nodenborough;{"language": "de"} to exist in the content graph - When VisibilityConstraints are set to "frontend" + When VisibilityConstraints are set to "default" # we change the dimension configuration When I change the content dimensions in content repository "default" to: @@ -118,14 +118,14 @@ Feature: Move dimension space point Then I expect node aggregate identifier "sir-david-nodenborough" to lead to no node When VisibilityConstraints are set to "withoutRestrictions" Then I expect a node identified by cs-identifier;sir-david-nodenborough;{"language": "de"} to exist in the content graph - When VisibilityConstraints are set to "frontend" + When VisibilityConstraints are set to "default" # The visibility edges were modified When I am in workspace "migration-workspace" and dimension space point {"language": "de_DE"} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to no node When VisibilityConstraints are set to "withoutRestrictions" Then I expect a node identified by migration-cs;sir-david-nodenborough;{"language": "de_DE"} to exist in the content graph - When VisibilityConstraints are set to "frontend" + When VisibilityConstraints are set to "default" When I run integrity violation detection Then I expect the integrity violation detection result to contain exactly 0 errors diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRemoval/RemoveNodeAggregateAfterDisabling.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRemoval/RemoveNodeAggregateAfterDisabling.feature index d583afa77fa..a6d60d05c84 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRemoval/RemoveNodeAggregateAfterDisabling.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRemoval/RemoveNodeAggregateAfterDisabling.feature @@ -71,7 +71,7 @@ Feature: Disable a node aggregate And I expect this node aggregate to disable dimension space points [] When I am in workspace "live" and dimension space point {} - And VisibilityConstraints are set to "frontend" + And VisibilityConstraints are set to "default" Then the subtree for node aggregate "lady-eleonode-rootford" with node types "" and 2 levels deep should be: | Level | nodeAggregateId | | 0 | lady-eleonode-rootford | diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteRuntimeVariables.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteRuntimeVariables.php index 31a28356a93..b6ca95a29b6 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteRuntimeVariables.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteRuntimeVariables.php @@ -111,13 +111,13 @@ public function iAmInWorkspaceAndDimensionSpacePoint(string $workspaceName, stri } /** - * @When /^VisibilityConstraints are set to "(withoutRestrictions|frontend)"$/ + * @When /^VisibilityConstraints are set to "(withoutRestrictions|default)"$/ */ public function visibilityConstraintsAreSetTo(string $restrictionType): void { $this->currentVisibilityConstraints = match ($restrictionType) { 'withoutRestrictions' => VisibilityConstraints::withoutRestrictions(), - 'frontend' => VisibilityConstraints::default(), + 'default' => VisibilityConstraints::default(), default => throw new \InvalidArgumentException('Visibility constraint "' . $restrictionType . '" not supported.'), }; } From abca5bbce0c375c1e4a3400dd97b087a568e7233 Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Tue, 29 Oct 2024 15:11:53 +0100 Subject: [PATCH 049/214] Mark `NodePermissions` and `WorkspacePermissions` internal --- Neos.Neos/Classes/Domain/Model/NodePermissions.php | 2 +- Neos.Neos/Classes/Domain/Model/WorkspacePermissions.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Neos.Neos/Classes/Domain/Model/NodePermissions.php b/Neos.Neos/Classes/Domain/Model/NodePermissions.php index 760300720b2..5a2ff8733a8 100644 --- a/Neos.Neos/Classes/Domain/Model/NodePermissions.php +++ b/Neos.Neos/Classes/Domain/Model/NodePermissions.php @@ -13,7 +13,7 @@ * - read: Permission to read the node and its properties and references * - edit: Permission to change the node * - * @api + * @internal */ #[Flow\Proxy(false)] final readonly class NodePermissions diff --git a/Neos.Neos/Classes/Domain/Model/WorkspacePermissions.php b/Neos.Neos/Classes/Domain/Model/WorkspacePermissions.php index cdb2461f6bb..421ed38c2cb 100644 --- a/Neos.Neos/Classes/Domain/Model/WorkspacePermissions.php +++ b/Neos.Neos/Classes/Domain/Model/WorkspacePermissions.php @@ -14,7 +14,7 @@ * - write: Permission to write to the corresponding workspace, including publishing a derived workspace to it * - manage: Permission to change the metadata and roles of the corresponding workspace (e.g. change description/title or add/remove workspace roles) * - * @api + * @internal */ #[Flow\Proxy(false)] final readonly class WorkspacePermissions From 1a4629701eab78ec9a7c58da5863e65479f16ae4 Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Tue, 29 Oct 2024 15:12:22 +0100 Subject: [PATCH 050/214] Deduplicate workspace role specificity determination --- Neos.Neos/Classes/Domain/Model/WorkspaceRole.php | 2 +- Neos.Neos/Classes/Domain/Service/WorkspaceService.php | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Neos.Neos/Classes/Domain/Model/WorkspaceRole.php b/Neos.Neos/Classes/Domain/Model/WorkspaceRole.php index a36269e22b8..0b487fb03f9 100644 --- a/Neos.Neos/Classes/Domain/Model/WorkspaceRole.php +++ b/Neos.Neos/Classes/Domain/Model/WorkspaceRole.php @@ -32,7 +32,7 @@ public function isAtLeast(self $role): bool return $this->specificity() >= $role->specificity(); } - private function specificity(): int + public function specificity(): int { return match ($this) { self::VIEWER => 1, diff --git a/Neos.Neos/Classes/Domain/Service/WorkspaceService.php b/Neos.Neos/Classes/Domain/Service/WorkspaceService.php index 6cc3ecfe4ac..3f7523c94e1 100644 --- a/Neos.Neos/Classes/Domain/Service/WorkspaceService.php +++ b/Neos.Neos/Classes/Domain/Service/WorkspaceService.php @@ -277,6 +277,7 @@ public function getWorkspaceRoleAssignments(ContentRepositoryId $contentReposito public function getMostPrivilegedWorkspaceRoleForSubjects(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, WorkspaceRoleSubjects $subjects): ?WorkspaceRole { $tableRole = self::TABLE_NAME_WORKSPACE_ROLE; + $roleCasesBySpecificity = implode("\n", array_map(static fn (WorkspaceRole $role) => "WHEN role='{$role->value}' THEN {$role->specificity()}\n", WorkspaceRole::cases())); $query = << Date: Wed, 30 Oct 2024 12:56:25 +0100 Subject: [PATCH 051/214] Tweak resolution of current node in `ContentRepositoryAuthProvider` --- .../ContentRepositoryAuthorizationService.php | 24 ++--------- .../ContentRepositoryAuthProvider.php | 41 +++++++++++-------- .../ContentRepositoryAuthProviderFactory.php | 4 +- 3 files changed, 31 insertions(+), 38 deletions(-) diff --git a/Neos.Neos/Classes/Security/Authorization/ContentRepositoryAuthorizationService.php b/Neos.Neos/Classes/Security/Authorization/ContentRepositoryAuthorizationService.php index 12bc6b5e0bb..2f6cc32cf1f 100644 --- a/Neos.Neos/Classes/Security/Authorization/ContentRepositoryAuthorizationService.php +++ b/Neos.Neos/Classes/Security/Authorization/ContentRepositoryAuthorizationService.php @@ -8,9 +8,7 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; -use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; -use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Annotations as Flow; use Neos\Flow\Security\Account; use Neos\Flow\Security\Authorization\PrivilegeManagerInterface; @@ -44,7 +42,6 @@ public function __construct( private PartyService $partyService, private WorkspaceService $workspaceService, - private ContentRepositoryRegistry $contentRepositoryRegistry, private PolicyService $policyService, private PrivilegeManagerInterface $privilegeManager, ) { @@ -100,13 +97,13 @@ public function getWorkspacePermissionsForAccount(ContentRepositoryId $contentRe ); } - public function getNodePermissionsForAnonymousUser(Node|NodeAddress $node): NodePermissions + public function getNodePermissionsForAnonymousUser(Node $node): NodePermissions { $roles = $this->rolesOfAnonymousUser(); return $this->nodePermissionsForRoles($node, $roles); } - public function getNodePermissionsForAccount(Node|NodeAddress $node, Account $account): NodePermissions + public function getNodePermissionsForAccount(Node $node, Account $account): NodePermissions { $roles = $this->expandAccountRoles($account); return $this->nodePermissionsForRoles($node, $roles); @@ -189,15 +186,8 @@ private function neosUserFromAccount(Account $account): ?User /** * @param array $roles */ - private function nodePermissionsForRoles(Node|NodeAddress $node, array $roles): NodePermissions + private function nodePermissionsForRoles(Node $node, array $roles): NodePermissions { - if ($node instanceof NodeAddress) { - $converted = $this->nodeForNodeAddress($node); - if ($converted === null) { - return NodePermissions::none(sprintf('Node "%s" not found in Content Repository "%s"', $node->aggregateId->value, $node->contentRepositoryId->value)); - } - $node = $converted; - } $subtreeTagPrivilegeSubject = new SubtreeTagPrivilegeSubject($node->tags->all(), $node->contentRepositoryId); $readGranted = $this->privilegeManager->isGrantedForRoles($roles, ReadNodePrivilege::class, $subtreeTagPrivilegeSubject, $readReason); $writeGranted = $this->privilegeManager->isGrantedForRoles($roles, EditNodePrivilege::class, $subtreeTagPrivilegeSubject, $writeReason); @@ -207,12 +197,4 @@ private function nodePermissionsForRoles(Node|NodeAddress $node, array $roles): reason: $readReason . "\n" . $writeReason, ); } - - private function nodeForNodeAddress(NodeAddress $nodeAddress): ?Node - { - return $this->contentRepositoryRegistry->get($nodeAddress->contentRepositoryId) - ->getContentGraph($nodeAddress->workspaceName) - ->getSubgraph($nodeAddress->dimensionSpacePoint, VisibilityConstraints::withoutRestrictions()) - ->findNodeById($nodeAddress->aggregateId); - } } diff --git a/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php index 75e1bd1ad3e..fc624112a4f 100644 --- a/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php +++ b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php @@ -6,6 +6,7 @@ use Neos\ContentRepository\Core\CommandHandler\CommandInterface; use Neos\ContentRepository\Core\ContentRepository; +use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\Feature\Common\RebasableToOtherWorkspaceInterface; use Neos\ContentRepository\Core\Feature\NodeModification\Command\SetNodeProperties; use Neos\ContentRepository\Core\Feature\NodeModification\Command\SetSerializedNodeProperties; @@ -22,9 +23,11 @@ use Neos\ContentRepository\Core\Feature\WorkspacePublication\Command\PublishIndividualNodesFromWorkspace; use Neos\ContentRepository\Core\Feature\WorkspacePublication\Command\PublishWorkspace; use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Command\RebaseWorkspace; +use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Security\Context as SecurityContext; @@ -39,16 +42,17 @@ * * @internal use {@see ContentRepositoryAuthorizationService} to ask for specific authorization decisions */ -final class ContentRepositoryAuthProvider implements AuthProviderInterface +final readonly class ContentRepositoryAuthProvider implements AuthProviderInterface { private const WORKSPACE_PERMISSION_WRITE = 'write'; private const WORKSPACE_PERMISSION_MANAGE = 'manage'; public function __construct( - private readonly ContentRepositoryId $contentRepositoryId, - private readonly UserService $userService, - private readonly ContentRepositoryAuthorizationService $authorizationService, - private readonly SecurityContext $securityContext, + private ContentRepositoryId $contentRepositoryId, + private UserService $userService, + private ContentRepositoryRegistry $contentRepositoryRegistry, + private ContentRepositoryAuthorizationService $authorizationService, + private SecurityContext $securityContext, ) { } @@ -90,14 +94,11 @@ public function canExecuteCommand(CommandInterface $command): Privilege return Privilege::granted('Authorization checks are disabled'); } if ($command instanceof SetNodeProperties) { - $nodePermissions = $this->getNodePermissionsForCurrentUser( - NodeAddress::create( - $this->contentRepositoryId, - $command->workspaceName, - $command->originDimensionSpacePoint->toDimensionSpacePoint(), - $command->nodeAggregateId, - ) - ); + $node = $this->getNode($command->workspaceName, $command->originDimensionSpacePoint->toDimensionSpacePoint(), $command->nodeAggregateId); + if ($node === null) { + return Privilege::denied(sprintf('Failed to load node "%s" in workspace "%s"', $command->nodeAggregateId->value, $command->workspaceName->value)); + } + $nodePermissions = $this->getNodePermissionsForCurrentUser($node); if (!$nodePermissions->edit) { return Privilege::denied($nodePermissions->getReason()); } @@ -156,12 +157,20 @@ private function getWorkspacePermissionsForCurrentUser(WorkspaceName $workspaceN return $this->authorizationService->getWorkspacePermissionsForAccount($this->contentRepositoryId, $workspaceName, $authenticatedAccount); } - private function getNodePermissionsForCurrentUser(NodeAddress $nodeAddress): NodePermissions + private function getNodePermissionsForCurrentUser(Node $node): NodePermissions { $authenticatedAccount = $this->securityContext->getAccount(); if ($authenticatedAccount === null) { - return $this->authorizationService->getNodePermissionsForAnonymousUser($nodeAddress); + return $this->authorizationService->getNodePermissionsForAnonymousUser($node); } - return $this->authorizationService->getNodePermissionsForAccount($nodeAddress, $authenticatedAccount); + return $this->authorizationService->getNodePermissionsForAccount($node, $authenticatedAccount); + } + + private function getNode(WorkspaceName $workspaceName, DimensionSpacePoint $dimensionSpacePoint, NodeAggregateId $nodeAggregateId): ?Node + { + return $this->contentRepositoryRegistry->get($this->contentRepositoryId) + ->getContentGraph($workspaceName) + ->getSubgraph($dimensionSpacePoint, VisibilityConstraints::withoutRestrictions()) + ->findNodeById($nodeAggregateId); } } diff --git a/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProviderFactory.php b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProviderFactory.php index 54bfc97294c..54db08a2c36 100644 --- a/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProviderFactory.php +++ b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProviderFactory.php @@ -5,6 +5,7 @@ namespace Neos\Neos\Security\ContentRepositoryAuthProvider; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; +use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\ContentRepositoryRegistry\Factory\AuthProvider\AuthProviderFactoryInterface; use Neos\Flow\Annotations as Flow; use Neos\Flow\Security\Context as SecurityContext; @@ -21,6 +22,7 @@ { public function __construct( private UserService $userService, + private ContentRepositoryRegistry $contentRepositoryRegistry, private ContentRepositoryAuthorizationService $contentRepositoryAuthorizationService, private SecurityContext $securityContext, ) { @@ -31,6 +33,6 @@ public function __construct( */ public function build(ContentRepositoryId $contentRepositoryId, array $options): ContentRepositoryAuthProvider { - return new ContentRepositoryAuthProvider($contentRepositoryId, $this->userService, $this->contentRepositoryAuthorizationService, $this->securityContext); + return new ContentRepositoryAuthProvider($contentRepositoryId, $this->userService, $this->contentRepositoryRegistry, $this->contentRepositoryAuthorizationService, $this->securityContext); } } From 7311ca9b35ff729f4c79c0fd33949fef74e4051d Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Wed, 30 Oct 2024 13:43:44 +0100 Subject: [PATCH 052/214] Revert `RebasableToOtherWorkspaceInterface` extension and check against concrete command classes --- .../RebasableToOtherWorkspaceInterface.php | 2 - .../Command/AddDimensionShineThrough.php | 5 - .../Command/MoveDimensionSpacePoint.php | 5 - ...gregateWithNodeAndSerializedProperties.php | 5 - .../Command/DisableNodeAggregate.php | 5 - .../Command/EnableNodeAggregate.php | 5 - .../Command/CopyNodesRecursively.php | 5 - .../Command/SetSerializedNodeProperties.php | 5 - .../NodeMove/Command/MoveNodeAggregate.php | 5 - .../Command/SetSerializedNodeReferences.php | 5 - .../Command/RemoveNodeAggregate.php | 5 - .../Command/ChangeNodeAggregateName.php | 5 - .../Command/ChangeNodeAggregateType.php | 5 - .../Command/CreateNodeVariant.php | 5 - .../CreateRootNodeAggregateWithNode.php | 5 - .../UpdateRootNodeAggregateDimensions.php | 5 - .../SubtreeTagging/Command/TagSubtree.php | 5 - .../SubtreeTagging/Command/UntagSubtree.php | 5 - .../ContentRepositoryAuthProvider.php | 91 +++++++++++++------ 19 files changed, 61 insertions(+), 117 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/Feature/Common/RebasableToOtherWorkspaceInterface.php b/Neos.ContentRepository.Core/Classes/Feature/Common/RebasableToOtherWorkspaceInterface.php index 84c0540e372..c7b3d5cbc59 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/Common/RebasableToOtherWorkspaceInterface.php +++ b/Neos.ContentRepository.Core/Classes/Feature/Common/RebasableToOtherWorkspaceInterface.php @@ -31,8 +31,6 @@ public function createCopyForWorkspace( WorkspaceName $targetWorkspaceName, ): self; - public function getWorkspaceName(): WorkspaceName; - /** * called during deserialization from metadata * @param array $array diff --git a/Neos.ContentRepository.Core/Classes/Feature/DimensionSpaceAdjustment/Command/AddDimensionShineThrough.php b/Neos.ContentRepository.Core/Classes/Feature/DimensionSpaceAdjustment/Command/AddDimensionShineThrough.php index e3143635b15..936505d278d 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/DimensionSpaceAdjustment/Command/AddDimensionShineThrough.php +++ b/Neos.ContentRepository.Core/Classes/Feature/DimensionSpaceAdjustment/Command/AddDimensionShineThrough.php @@ -84,11 +84,6 @@ public function createCopyForWorkspace(WorkspaceName $targetWorkspaceName): self ); } - public function getWorkspaceName(): WorkspaceName - { - return $this->workspaceName; - } - /** * @return array */ diff --git a/Neos.ContentRepository.Core/Classes/Feature/DimensionSpaceAdjustment/Command/MoveDimensionSpacePoint.php b/Neos.ContentRepository.Core/Classes/Feature/DimensionSpaceAdjustment/Command/MoveDimensionSpacePoint.php index e6ae20eb2b5..498ba261e49 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/DimensionSpaceAdjustment/Command/MoveDimensionSpacePoint.php +++ b/Neos.ContentRepository.Core/Classes/Feature/DimensionSpaceAdjustment/Command/MoveDimensionSpacePoint.php @@ -80,11 +80,6 @@ public function createCopyForWorkspace( ); } - public function getWorkspaceName(): WorkspaceName - { - return $this->workspaceName; - } - /** * @return array */ diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/Command/CreateNodeAggregateWithNodeAndSerializedProperties.php b/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/Command/CreateNodeAggregateWithNodeAndSerializedProperties.php index fa3cac5be87..79f5412eaa5 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/Command/CreateNodeAggregateWithNodeAndSerializedProperties.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/Command/CreateNodeAggregateWithNodeAndSerializedProperties.php @@ -176,9 +176,4 @@ public function createCopyForWorkspace( $this->tetheredDescendantNodeAggregateIds ); } - - public function getWorkspaceName(): WorkspaceName - { - return $this->workspaceName; - } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/Command/DisableNodeAggregate.php b/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/Command/DisableNodeAggregate.php index bb153eb5f7e..f92f938df9c 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/Command/DisableNodeAggregate.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/Command/DisableNodeAggregate.php @@ -99,9 +99,4 @@ public function createCopyForWorkspace( $this->nodeVariantSelectionStrategy ); } - - public function getWorkspaceName(): WorkspaceName - { - return $this->workspaceName; - } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/Command/EnableNodeAggregate.php b/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/Command/EnableNodeAggregate.php index 41d70615b29..de0ad11d57d 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/Command/EnableNodeAggregate.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/Command/EnableNodeAggregate.php @@ -99,9 +99,4 @@ public function createCopyForWorkspace( $this->nodeVariantSelectionStrategy ); } - - public function getWorkspaceName(): WorkspaceName - { - return $this->workspaceName; - } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/Command/CopyNodesRecursively.php b/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/Command/CopyNodesRecursively.php index 2f9be09c884..ba31a07c601 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/Command/CopyNodesRecursively.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/Command/CopyNodesRecursively.php @@ -183,9 +183,4 @@ public function createCopyForWorkspace( $this->nodeAggregateIdMapping ); } - - public function getWorkspaceName(): WorkspaceName - { - return $this->workspaceName; - } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Command/SetSerializedNodeProperties.php b/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Command/SetSerializedNodeProperties.php index 666b2fd2faa..a2d684f6035 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Command/SetSerializedNodeProperties.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Command/SetSerializedNodeProperties.php @@ -119,9 +119,4 @@ public function createCopyForWorkspace( $this->propertiesToUnset, ); } - - public function getWorkspaceName(): WorkspaceName - { - return $this->workspaceName; - } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeMove/Command/MoveNodeAggregate.php b/Neos.ContentRepository.Core/Classes/Feature/NodeMove/Command/MoveNodeAggregate.php index ca2fe213734..3a756163488 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeMove/Command/MoveNodeAggregate.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeMove/Command/MoveNodeAggregate.php @@ -131,9 +131,4 @@ public function createCopyForWorkspace( $this->newSucceedingSiblingNodeAggregateId ); } - - public function getWorkspaceName(): WorkspaceName - { - return $this->workspaceName; - } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Command/SetSerializedNodeReferences.php b/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Command/SetSerializedNodeReferences.php index 3b20699994a..1edf8409040 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Command/SetSerializedNodeReferences.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Command/SetSerializedNodeReferences.php @@ -107,9 +107,4 @@ public function createCopyForWorkspace( $this->references, ); } - - public function getWorkspaceName(): WorkspaceName - { - return $this->workspaceName; - } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeRemoval/Command/RemoveNodeAggregate.php b/Neos.ContentRepository.Core/Classes/Feature/NodeRemoval/Command/RemoveNodeAggregate.php index 654edc63dd5..085af255b8c 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeRemoval/Command/RemoveNodeAggregate.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeRemoval/Command/RemoveNodeAggregate.php @@ -119,9 +119,4 @@ public function createCopyForWorkspace( $this->removalAttachmentPoint, ); } - - public function getWorkspaceName(): WorkspaceName - { - return $this->workspaceName; - } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeRenaming/Command/ChangeNodeAggregateName.php b/Neos.ContentRepository.Core/Classes/Feature/NodeRenaming/Command/ChangeNodeAggregateName.php index 5df67f01181..38d1f195ed6 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeRenaming/Command/ChangeNodeAggregateName.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeRenaming/Command/ChangeNodeAggregateName.php @@ -93,9 +93,4 @@ public function createCopyForWorkspace( $this->newNodeName, ); } - - public function getWorkspaceName(): WorkspaceName - { - return $this->workspaceName; - } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeTypeChange/Command/ChangeNodeAggregateType.php b/Neos.ContentRepository.Core/Classes/Feature/NodeTypeChange/Command/ChangeNodeAggregateType.php index ede35595343..e979e4d25c1 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeTypeChange/Command/ChangeNodeAggregateType.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeTypeChange/Command/ChangeNodeAggregateType.php @@ -118,9 +118,4 @@ public function createCopyForWorkspace( $this->tetheredDescendantNodeAggregateIds ); } - - public function getWorkspaceName(): WorkspaceName - { - return $this->workspaceName; - } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeVariation/Command/CreateNodeVariant.php b/Neos.ContentRepository.Core/Classes/Feature/NodeVariation/Command/CreateNodeVariant.php index 0aa0f248109..54873f489dc 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeVariation/Command/CreateNodeVariant.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeVariation/Command/CreateNodeVariant.php @@ -97,9 +97,4 @@ public function createCopyForWorkspace( $this->targetOrigin, ); } - - public function getWorkspaceName(): WorkspaceName - { - return $this->workspaceName; - } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/RootNodeCreation/Command/CreateRootNodeAggregateWithNode.php b/Neos.ContentRepository.Core/Classes/Feature/RootNodeCreation/Command/CreateRootNodeAggregateWithNode.php index b2fe51dc5c6..9bbb1f320cc 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/RootNodeCreation/Command/CreateRootNodeAggregateWithNode.php +++ b/Neos.ContentRepository.Core/Classes/Feature/RootNodeCreation/Command/CreateRootNodeAggregateWithNode.php @@ -135,9 +135,4 @@ public function createCopyForWorkspace( $this->tetheredDescendantNodeAggregateIds ); } - - public function getWorkspaceName(): WorkspaceName - { - return $this->workspaceName; - } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/RootNodeCreation/Command/UpdateRootNodeAggregateDimensions.php b/Neos.ContentRepository.Core/Classes/Feature/RootNodeCreation/Command/UpdateRootNodeAggregateDimensions.php index 489c0d96ca5..302c05ed895 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/RootNodeCreation/Command/UpdateRootNodeAggregateDimensions.php +++ b/Neos.ContentRepository.Core/Classes/Feature/RootNodeCreation/Command/UpdateRootNodeAggregateDimensions.php @@ -78,9 +78,4 @@ public function createCopyForWorkspace( $this->nodeAggregateId, ); } - - public function getWorkspaceName(): WorkspaceName - { - return $this->workspaceName; - } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Command/TagSubtree.php b/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Command/TagSubtree.php index b77d199e155..d4d37c8b8cb 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Command/TagSubtree.php +++ b/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Command/TagSubtree.php @@ -89,11 +89,6 @@ public function createCopyForWorkspace(WorkspaceName $targetWorkspaceName): self ); } - public function getWorkspaceName(): WorkspaceName - { - return $this->workspaceName; - } - public function matchesNodeId(NodeIdToPublishOrDiscard $nodeIdToPublish): bool { return $this->nodeAggregateId->equals($nodeIdToPublish->nodeAggregateId) diff --git a/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Command/UntagSubtree.php b/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Command/UntagSubtree.php index 7c2009b55d5..1ae9b4624a2 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Command/UntagSubtree.php +++ b/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Command/UntagSubtree.php @@ -90,11 +90,6 @@ public function createCopyForWorkspace(WorkspaceName $targetWorkspaceName): self ); } - public function getWorkspaceName(): WorkspaceName - { - return $this->workspaceName; - } - public function matchesNodeId(NodeIdToPublishOrDiscard $nodeIdToPublish): bool { return $this->nodeAggregateId->equals($nodeIdToPublish->nodeAggregateId) diff --git a/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php index fc624112a4f..c12a78ba41d 100644 --- a/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php +++ b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php @@ -5,15 +5,31 @@ namespace Neos\Neos\Security\ContentRepositoryAuthProvider; use Neos\ContentRepository\Core\CommandHandler\CommandInterface; -use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\Feature\Common\RebasableToOtherWorkspaceInterface; +use Neos\ContentRepository\Core\Feature\DimensionSpaceAdjustment\Command\AddDimensionShineThrough; +use Neos\ContentRepository\Core\Feature\DimensionSpaceAdjustment\Command\MoveDimensionSpacePoint; +use Neos\ContentRepository\Core\Feature\NodeCreation\Command\CreateNodeAggregateWithNode; +use Neos\ContentRepository\Core\Feature\NodeCreation\Command\CreateNodeAggregateWithNodeAndSerializedProperties; +use Neos\ContentRepository\Core\Feature\NodeDisabling\Command\DisableNodeAggregate; +use Neos\ContentRepository\Core\Feature\NodeDisabling\Command\EnableNodeAggregate; +use Neos\ContentRepository\Core\Feature\NodeDuplication\Command\CopyNodesRecursively; use Neos\ContentRepository\Core\Feature\NodeModification\Command\SetNodeProperties; use Neos\ContentRepository\Core\Feature\NodeModification\Command\SetSerializedNodeProperties; +use Neos\ContentRepository\Core\Feature\NodeMove\Command\MoveNodeAggregate; +use Neos\ContentRepository\Core\Feature\NodeReferencing\Command\SetNodeReferences; +use Neos\ContentRepository\Core\Feature\NodeReferencing\Command\SetSerializedNodeReferences; +use Neos\ContentRepository\Core\Feature\NodeRemoval\Command\RemoveNodeAggregate; +use Neos\ContentRepository\Core\Feature\NodeRenaming\Command\ChangeNodeAggregateName; +use Neos\ContentRepository\Core\Feature\NodeTypeChange\Command\ChangeNodeAggregateType; +use Neos\ContentRepository\Core\Feature\NodeVariation\Command\CreateNodeVariant; +use Neos\ContentRepository\Core\Feature\RootNodeCreation\Command\CreateRootNodeAggregateWithNode; +use Neos\ContentRepository\Core\Feature\RootNodeCreation\Command\UpdateRootNodeAggregateDimensions; use Neos\ContentRepository\Core\Feature\Security\AuthProviderInterface; use Neos\ContentRepository\Core\Feature\Security\Dto\Privilege; use Neos\ContentRepository\Core\Feature\Security\Dto\UserId; -use Neos\ContentRepository\Core\Feature\SubtreeTagging\Dto\SubtreeTag; +use Neos\ContentRepository\Core\Feature\SubtreeTagging\Command\TagSubtree; +use Neos\ContentRepository\Core\Feature\SubtreeTagging\Command\UntagSubtree; use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Command\CreateRootWorkspace; use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Command\CreateWorkspace; use Neos\ContentRepository\Core\Feature\WorkspaceModification\Command\ChangeBaseWorkspace; @@ -93,48 +109,71 @@ public function canExecuteCommand(CommandInterface $command): Privilege if ($this->securityContext->areAuthorizationChecksDisabled()) { return Privilege::granted('Authorization checks are disabled'); } - if ($command instanceof SetNodeProperties) { - $node = $this->getNode($command->workspaceName, $command->originDimensionSpacePoint->toDimensionSpacePoint(), $command->nodeAggregateId); + + /** @var NodeAddress|null $nodeThatRequiresEditPrivilege */ + $nodeThatRequiresEditPrivilege = match ($command::class) { + CopyNodesRecursively::class => NodeAddress::create($this->contentRepositoryId, $command->workspaceName, $command->targetDimensionSpacePoint->toDimensionSpacePoint(), $command->targetParentNodeAggregateId), + CreateNodeAggregateWithNode::class, + CreateNodeAggregateWithNodeAndSerializedProperties::class => NodeAddress::create($this->contentRepositoryId, $command->workspaceName, $command->originDimensionSpacePoint->toDimensionSpacePoint(), $command->parentNodeAggregateId), + CreateNodeVariant::class => NodeAddress::create($this->contentRepositoryId, $command->workspaceName, $command->sourceOrigin->toDimensionSpacePoint(), $command->nodeAggregateId), + DisableNodeAggregate::class, + EnableNodeAggregate::class, + RemoveNodeAggregate::class, + TagSubtree::class, + UntagSubtree::class => NodeAddress::create($this->contentRepositoryId, $command->workspaceName, $command->coveredDimensionSpacePoint, $command->nodeAggregateId), + MoveNodeAggregate::class => NodeAddress::create($this->contentRepositoryId, $command->workspaceName, $command->dimensionSpacePoint, $command->nodeAggregateId), + SetNodeProperties::class, + SetSerializedNodeProperties::class => NodeAddress::create($this->contentRepositoryId, $command->workspaceName, $command->originDimensionSpacePoint->toDimensionSpacePoint(), $command->nodeAggregateId), + SetNodeReferences::class, + SetSerializedNodeReferences::class => NodeAddress::create($this->contentRepositoryId, $command->workspaceName, $command->sourceOriginDimensionSpacePoint->toDimensionSpacePoint(), $command->sourceNodeAggregateId), + default => null, + }; + if ($nodeThatRequiresEditPrivilege !== null) { + $workspacePermissions = $this->getWorkspacePermissionsForCurrentUser($nodeThatRequiresEditPrivilege->workspaceName); + if ($workspacePermissions->write) { + return Privilege::denied(sprintf('No write permissions on workspace "%s": %s', $nodeThatRequiresEditPrivilege->workspaceName->value, $workspacePermissions->getReason())); + } + $node = $this->contentRepositoryRegistry->get($this->contentRepositoryId) + ->getContentGraph($nodeThatRequiresEditPrivilege->workspaceName) + ->getSubgraph($nodeThatRequiresEditPrivilege->dimensionSpacePoint, VisibilityConstraints::withoutRestrictions()) + ->findNodeById($nodeThatRequiresEditPrivilege->aggregateId); if ($node === null) { - return Privilege::denied(sprintf('Failed to load node "%s" in workspace "%s"', $command->nodeAggregateId->value, $command->workspaceName->value)); + return Privilege::denied(sprintf('Failed to load node "%s" in workspace "%s"', $nodeThatRequiresEditPrivilege->aggregateId->value, $nodeThatRequiresEditPrivilege->workspaceName->value)); } $nodePermissions = $this->getNodePermissionsForCurrentUser($node); if (!$nodePermissions->edit) { - return Privilege::denied($nodePermissions->getReason()); + return Privilege::denied(sprintf('No edit permissions for node "%s" in workspace "%s": %s', $nodeThatRequiresEditPrivilege->aggregateId->value, $nodeThatRequiresEditPrivilege->workspaceName->value, $nodePermissions->getReason())); } - return $this->requireWorkspacePermission($command->workspaceName, self::WORKSPACE_PERMISSION_WRITE); + return Privilege::granted(sprintf('Edit permissions for node "%s" in workspace "%s" granted: %s', $nodeThatRequiresEditPrivilege->aggregateId->value, $nodeThatRequiresEditPrivilege->workspaceName->value, $nodePermissions->getReason())); } - - // Note: We check against the {@see RebasableToOtherWorkspaceInterface} because that is implemented by all - // commands that interact with nodes on a content stream. With that it's likely that we don't have to adjust the - // code if we were to add new commands in the future - if ($command instanceof RebasableToOtherWorkspaceInterface) { - return $this->requireWorkspacePermission($command->getWorkspaceName(), self::WORKSPACE_PERMISSION_WRITE); // @phpstan-ignore-line - } - if ($command instanceof CreateRootWorkspace) { return Privilege::denied('Creation of root workspaces is currently only allowed with disabled authorization checks'); } - if ($command instanceof ChangeBaseWorkspace) { $workspacePermissions = $this->getWorkspacePermissionsForCurrentUser($command->workspaceName); if (!$workspacePermissions->manage) { - return Privilege::denied("Missing 'manage' permissions for workspace '{$command->workspaceName->value}': {$workspacePermissions->getReason()}"); + return Privilege::denied(sprintf('Missing "manage" permissions for workspace "%s": %s', $command->workspaceName->value, $workspacePermissions->getReason())); } $baseWorkspacePermissions = $this->getWorkspacePermissionsForCurrentUser($command->baseWorkspaceName); - if (!$baseWorkspacePermissions->write) { - return Privilege::denied("Missing 'write' permissions for base workspace '{$command->baseWorkspaceName->value}': {$baseWorkspacePermissions->getReason()}"); + if (!$baseWorkspacePermissions->read) { + return Privilege::denied(sprintf('Missing "read" permissions for base workspace "%s": %s', $command->baseWorkspaceName->value, $baseWorkspacePermissions->getReason())); } - return Privilege::granted("User has 'manage' permissions for workspace '{$command->workspaceName->value}' and 'write' permissions for base workspace '{$command->baseWorkspaceName->value}'"); + return Privilege::granted(sprintf('User has "manage" permissions for workspace "%s" and "read" permissions for base workspace "%s"', $command->workspaceName->value, $command->baseWorkspaceName->value)); } return match ($command::class) { - CreateWorkspace::class => $this->requireWorkspacePermission($command->baseWorkspaceName, self::WORKSPACE_PERMISSION_WRITE), - DeleteWorkspace::class => $this->requireWorkspacePermission($command->workspaceName, self::WORKSPACE_PERMISSION_MANAGE), + AddDimensionShineThrough::class, + ChangeNodeAggregateName::class, + ChangeNodeAggregateType::class, + CreateRootNodeAggregateWithNode::class, + MoveDimensionSpacePoint::class, + UpdateRootNodeAggregateDimensions::class, DiscardWorkspace::class, DiscardIndividualNodesFromWorkspace::class, PublishWorkspace::class, PublishIndividualNodesFromWorkspace::class, RebaseWorkspace::class => $this->requireWorkspacePermission($command->workspaceName, self::WORKSPACE_PERMISSION_WRITE), + CreateWorkspace::class => $this->requireWorkspacePermission($command->baseWorkspaceName, self::WORKSPACE_PERMISSION_WRITE), + DeleteWorkspace::class => $this->requireWorkspacePermission($command->workspaceName, self::WORKSPACE_PERMISSION_MANAGE), default => Privilege::granted('Command not restricted'), }; } @@ -165,12 +204,4 @@ private function getNodePermissionsForCurrentUser(Node $node): NodePermissions } return $this->authorizationService->getNodePermissionsForAccount($node, $authenticatedAccount); } - - private function getNode(WorkspaceName $workspaceName, DimensionSpacePoint $dimensionSpacePoint, NodeAggregateId $nodeAggregateId): ?Node - { - return $this->contentRepositoryRegistry->get($this->contentRepositoryId) - ->getContentGraph($workspaceName) - ->getSubgraph($dimensionSpacePoint, VisibilityConstraints::withoutRestrictions()) - ->findNodeById($nodeAggregateId); - } } From 5d17a70946937f35a9b1358f551db070f92b59e2 Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Wed, 30 Oct 2024 16:46:54 +0100 Subject: [PATCH 053/214] Fix workspace permissions check --- .../ContentRepositoryAuthProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php index c12a78ba41d..d2ed098a3ed 100644 --- a/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php +++ b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php @@ -130,7 +130,7 @@ public function canExecuteCommand(CommandInterface $command): Privilege }; if ($nodeThatRequiresEditPrivilege !== null) { $workspacePermissions = $this->getWorkspacePermissionsForCurrentUser($nodeThatRequiresEditPrivilege->workspaceName); - if ($workspacePermissions->write) { + if (!$workspacePermissions->write) { return Privilege::denied(sprintf('No write permissions on workspace "%s": %s', $nodeThatRequiresEditPrivilege->workspaceName->value, $workspacePermissions->getReason())); } $node = $this->contentRepositoryRegistry->get($this->contentRepositoryId) From 4ace290933985ad86261e0ecfa961b7b7c3047f7 Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Fri, 1 Nov 2024 12:55:41 +0100 Subject: [PATCH 054/214] FEATURE: Command Hooks Resolves: #5335 --- .../Classes/CommandHandler/CommandBus.php | 19 +++++++- .../CommandHandler/CommandHookInterface.php | 17 +++++++ .../Classes/CommandHandler/CommandHooks.php | 48 +++++++++++++++++++ .../Factory/CommandHookFactoryInterface.php | 17 +++++++ .../Classes/Factory/CommandHooksFactory.php | 33 +++++++++++++ .../CommandHooksFactoryDependencies.php | 39 +++++++++++++++ .../Factory/ContentRepositoryFactory.php | 7 +++ .../Classes/ContentRepositoryRegistry.php | 27 ++++++++++- 8 files changed, 204 insertions(+), 3 deletions(-) create mode 100644 Neos.ContentRepository.Core/Classes/CommandHandler/CommandHookInterface.php create mode 100644 Neos.ContentRepository.Core/Classes/CommandHandler/CommandHooks.php create mode 100644 Neos.ContentRepository.Core/Classes/Factory/CommandHookFactoryInterface.php create mode 100644 Neos.ContentRepository.Core/Classes/Factory/CommandHooksFactory.php create mode 100644 Neos.ContentRepository.Core/Classes/Factory/CommandHooksFactoryDependencies.php diff --git a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandBus.php b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandBus.php index 92673a00c82..0f9e50c0bbd 100644 --- a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandBus.php +++ b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandBus.php @@ -23,6 +23,7 @@ public function __construct( // todo pass $commandHandlingDependencies in each command handler instead of into the commandBus private CommandHandlingDependencies $commandHandlingDependencies, + private CommandHooks $commandHooks, CommandHandlerInterface ...$handlers ) { $this->handlers = $handlers; @@ -35,9 +36,13 @@ public function handle(CommandInterface $command): EventsToPublish|\Generator { // multiple handlers must not handle the same command foreach ($this->handlers as $handler) { - if ($handler->canHandle($command)) { - return $handler->handle($command, $this->commandHandlingDependencies); + if (!$handler->canHandle($command)) { + continue; } + foreach ($this->commandHooks as $commandHook) { + $command = $commandHook->beforeHandle($command); + } + return $handler->handle($command, $this->commandHandlingDependencies); } throw new \RuntimeException(sprintf('No handler found for Command "%s"', get_debug_type($command)), 1649582778); } @@ -46,8 +51,18 @@ public function withAdditionalHandlers(CommandHandlerInterface ...$handlers): se { return new self( $this->commandHandlingDependencies, + $this->commandHooks, ...$this->handlers, ...$handlers, ); } + + public function withCommandHooks(CommandHooks $commandHooks): self + { + return new self( + $this->commandHandlingDependencies, + $commandHooks, + ...$this->handlers, + ); + } } diff --git a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHookInterface.php b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHookInterface.php new file mode 100644 index 00000000000..f0c0579f98f --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHookInterface.php @@ -0,0 +1,17 @@ + + * @internal + */ +final readonly class CommandHooks implements \IteratorAggregate, \Countable +{ + /** + * @var array + */ + private array $commandHooks; + + private function __construct( + CommandHookInterface ...$commandHooks + ) { + $this->commandHooks = $commandHooks; + } + + /** + * @param array $commandHooks + */ + public static function fromArray(array $commandHooks): self + { + return new self(...$commandHooks); + } + + public static function none(): self + { + return new self(); + } + + public function getIterator(): \Traversable + { + yield from $this->commandHooks; + } + + public function count(): int + { + return count($this->commandHooks); + } +} diff --git a/Neos.ContentRepository.Core/Classes/Factory/CommandHookFactoryInterface.php b/Neos.ContentRepository.Core/Classes/Factory/CommandHookFactoryInterface.php new file mode 100644 index 00000000000..c9eebfba86c --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/Factory/CommandHookFactoryInterface.php @@ -0,0 +1,17 @@ + + */ + private array $commandHookFactories; + + public function __construct( + CommandHookFactoryInterface ...$commandHookFactories, + ) { + $this->commandHookFactories = $commandHookFactories; + } + + public function build( + CommandHooksFactoryDependencies $commandHooksFactoryDependencies, + ): CommandHooks { + return CommandHooks::fromArray(array_map( + static fn (CommandHookFactoryInterface $factory) => $factory->build($commandHooksFactoryDependencies), + $this->commandHookFactories + )); + } +} diff --git a/Neos.ContentRepository.Core/Classes/Factory/CommandHooksFactoryDependencies.php b/Neos.ContentRepository.Core/Classes/Factory/CommandHooksFactoryDependencies.php new file mode 100644 index 00000000000..42ef4256022 --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/Factory/CommandHooksFactoryDependencies.php @@ -0,0 +1,39 @@ +projectionFactoryDependencies->nodeTypeManager, $this->projectionFactoryDependencies->contentDimensionZookeeper, @@ -126,6 +129,10 @@ public function getOrBuild(): ContentRepository $this->projectionFactoryDependencies->eventNormalizer, ) ); + if ($this->commandHooksFactory !== null) { + $commandHooks = $this->commandHooksFactory->build(CommandHooksFactoryDependencies::create($this->contentRepositoryId)); + $publicCommandBus = $publicCommandBus->withCommandHooks($commandHooks); + } return $this->contentRepository = new ContentRepository( $this->contentRepositoryId, diff --git a/Neos.ContentRepositoryRegistry/Classes/ContentRepositoryRegistry.php b/Neos.ContentRepositoryRegistry/Classes/ContentRepositoryRegistry.php index 4b78f99f13d..18ccaeea961 100644 --- a/Neos.ContentRepositoryRegistry/Classes/ContentRepositoryRegistry.php +++ b/Neos.ContentRepositoryRegistry/Classes/ContentRepositoryRegistry.php @@ -6,6 +6,8 @@ use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\Dimension\ContentDimensionSourceInterface; +use Neos\ContentRepository\Core\Factory\CommandHookFactoryInterface; +use Neos\ContentRepository\Core\Factory\CommandHooksFactory; use Neos\ContentRepository\Core\Factory\ContentRepositoryFactory; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceFactoryInterface; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface; @@ -176,7 +178,8 @@ private function buildFactory(ContentRepositoryId $contentRepositoryId): Content $this->buildPropertySerializer($contentRepositoryId, $contentRepositorySettings), $this->buildProjectionsFactory($contentRepositoryId, $contentRepositorySettings), $this->buildUserIdProvider($contentRepositoryId, $contentRepositorySettings), - $clock + $clock, + $this->buildCommandHooksFactory($contentRepositoryId, $contentRepositorySettings), ); } catch (\Exception $exception) { throw InvalidConfigurationException::fromException($contentRepositoryId, $exception); @@ -275,6 +278,28 @@ private function buildProjectionsFactory(ContentRepositoryId $contentRepositoryI return $projectionsAndCatchUpHooksFactory; } + /** @param array $contentRepositorySettings */ + private function buildCommandHooksFactory(ContentRepositoryId $contentRepositoryId, array $contentRepositorySettings): CommandHooksFactory + { + $commandHooksSettings = $contentRepositorySettings['commandHooks'] ?? []; + if (!is_array($commandHooksSettings)) { + throw InvalidConfigurationException::fromMessage('Content repository "%s" does not have the "commandHooks" configured properly. Expected array, got %s.', $contentRepositoryId->value, get_debug_type($commandHooksSettings)); + } + $commandHookFactories = []; + foreach ($commandHooksSettings as $name => $commandHookSettings) { + // Allow to unset/disable command hooks + if ($commandHookSettings === null) { + continue; + } + $commandHookFactory = $this->objectManager->get($commandHookSettings['factoryObjectName']); + if (!$commandHookFactory instanceof CommandHookFactoryInterface) { + throw InvalidConfigurationException::fromMessage('Factory object name for command hook "%s" (content repository "%s") is not an instance of %s but %s.', $name, $contentRepositoryId->value, CommandHookFactoryInterface::class, get_debug_type($commandHookFactory)); + } + $commandHookFactories[] = $commandHookFactory; + } + return new CommandHooksFactory(...$commandHookFactories); + } + /** * @param ProjectionFactoryInterface> $projectionFactory */ From a0c704b20ce86da228d8c6fa32a75cf346ef5f5b Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Fri, 1 Nov 2024 14:56:12 +0100 Subject: [PATCH 055/214] Tweak `CommandHandlerInterface` signature and add doc comments --- .../Classes/CommandHandler/CommandBus.php | 2 +- .../CommandHandler/CommandHookInterface.php | 14 +++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandBus.php b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandBus.php index 0f9e50c0bbd..262a3acee72 100644 --- a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandBus.php +++ b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandBus.php @@ -40,7 +40,7 @@ public function handle(CommandInterface $command): EventsToPublish|\Generator continue; } foreach ($this->commandHooks as $commandHook) { - $command = $commandHook->beforeHandle($command); + $command = $commandHook->onBeforeHandle($command); } return $handler->handle($command, $this->commandHandlingDependencies); } diff --git a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHookInterface.php b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHookInterface.php index f0c0579f98f..2afb7b2ea3f 100644 --- a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHookInterface.php +++ b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHookInterface.php @@ -4,14 +4,22 @@ namespace Neos\ContentRepository\Core\CommandHandler; -use Neos\ContentRepository\Core\EventStore\EventsToPublish; +use Neos\ContentRepository\Core\ContentRepository; /** - * TODO docs + * Contract for a hook that is invoked just before any command is processed via {@see ContentRepository::handle()} + * + * A command hook can be used to replace/alter an incoming command before it is being passed to the corresponding {@see CommandHandlerInterface}. + * This can be used to change or enrich the payload of the command. + * A command hook can also be used to intercept commands based on their type or payload but this is not the intended use case because it can lead to a degraded user experience * * @api */ interface CommandHookInterface { - public function beforeHandle(CommandInterface $command): CommandInterface; + /** + * @param CommandInterface $command The command that is about to be handled + * @return CommandInterface This hook must return a command instance. It can be the unaltered incoming $command or a new instance + */ + public function onBeforeHandle(CommandInterface $command): CommandInterface; } From 3f5c6e94ca5a0262a58717f822f8e4f70b57e37f Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Fri, 1 Nov 2024 14:56:43 +0100 Subject: [PATCH 056/214] Support custom sorting for command hooks a logger hook should be executed last for example --- .../Classes/ContentRepositoryRegistry.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Neos.ContentRepositoryRegistry/Classes/ContentRepositoryRegistry.php b/Neos.ContentRepositoryRegistry/Classes/ContentRepositoryRegistry.php index 18ccaeea961..9261fcc4010 100644 --- a/Neos.ContentRepositoryRegistry/Classes/ContentRepositoryRegistry.php +++ b/Neos.ContentRepositoryRegistry/Classes/ContentRepositoryRegistry.php @@ -286,7 +286,7 @@ private function buildCommandHooksFactory(ContentRepositoryId $contentRepository throw InvalidConfigurationException::fromMessage('Content repository "%s" does not have the "commandHooks" configured properly. Expected array, got %s.', $contentRepositoryId->value, get_debug_type($commandHooksSettings)); } $commandHookFactories = []; - foreach ($commandHooksSettings as $name => $commandHookSettings) { + foreach ((new PositionalArraySorter($commandHooksSettings))->toArray() as $name => $commandHookSettings) { // Allow to unset/disable command hooks if ($commandHookSettings === null) { continue; From f526e739d770a4707150110e72f96c961af41f92 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 2 Nov 2024 17:09:12 +0100 Subject: [PATCH 057/214] TASK: Post merge adjustments to make phpstan happy --- .../Classes/Processors/ProjectionCatchupProcessorFactory.php | 2 +- .../Classes/Processors/ProjectionReplayProcessorFactory.php | 2 +- .../Classes/Domain/Pruning/RoleAndMetadataPruningProcessor.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionCatchupProcessorFactory.php b/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionCatchupProcessorFactory.php index 14436d9dc52..305e8c30172 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionCatchupProcessorFactory.php +++ b/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionCatchupProcessorFactory.php @@ -27,7 +27,7 @@ public function build(ContentRepositoryServiceFactoryDependencies $serviceFactor { return new ProjectionCatchupProcessor( new ProjectionService( - $serviceFactoryDependencies->projections, + $serviceFactoryDependencies->projectionsAndCatchUpHooks->projections, $serviceFactoryDependencies->contentRepository, $serviceFactoryDependencies->eventStore, ) diff --git a/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionReplayProcessorFactory.php b/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionReplayProcessorFactory.php index 4b59f652c5d..6c9c32b6200 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionReplayProcessorFactory.php +++ b/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionReplayProcessorFactory.php @@ -25,7 +25,7 @@ public function build(ContentRepositoryServiceFactoryDependencies $serviceFactor { return new ProjectionReplayProcessor( new ProjectionService( - $serviceFactoryDependencies->projections, + $serviceFactoryDependencies->projectionsAndCatchUpHooks->projections, $serviceFactoryDependencies->contentRepository, $serviceFactoryDependencies->eventStore, ) diff --git a/Neos.Neos/Classes/Domain/Pruning/RoleAndMetadataPruningProcessor.php b/Neos.Neos/Classes/Domain/Pruning/RoleAndMetadataPruningProcessor.php index 81369e66307..f341f825b24 100644 --- a/Neos.Neos/Classes/Domain/Pruning/RoleAndMetadataPruningProcessor.php +++ b/Neos.Neos/Classes/Domain/Pruning/RoleAndMetadataPruningProcessor.php @@ -33,7 +33,7 @@ public function __construct( public function run(ProcessingContext $context): void { - $this->workspaceService->pruneRoleAsssignments($this->contentRepositoryId); + $this->workspaceService->pruneRoleAssignments($this->contentRepositoryId); $this->workspaceService->pruneWorkspaceMetadata($this->contentRepositoryId); } } From 0772b6a8fe724587e4b6820f8b7ea14495d72804 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 2 Nov 2024 17:25:34 +0100 Subject: [PATCH 058/214] TASK: Replace `LiveWorkspaceIsEmptyProcessor` with read model checks --- .../Export/SiteExportProcessorFactory.php | 1 - .../Import/LiveWorkspaceCreationProcessor.php | 9 +++- .../Import/LiveWorkspaceIsEmptyProcessor.php | 54 ------------------- .../LiveWorkspaceIsEmptyProcessorFactory.php | 31 ----------- .../Pruning/SitePruningProcessorFactory.php | 3 -- .../Domain/Service/SiteImportService.php | 6 +-- .../Domain/Service/SitePruningService.php | 14 ----- 7 files changed, 8 insertions(+), 110 deletions(-) delete mode 100644 Neos.Neos/Classes/Domain/Import/LiveWorkspaceIsEmptyProcessor.php delete mode 100644 Neos.Neos/Classes/Domain/Import/LiveWorkspaceIsEmptyProcessorFactory.php diff --git a/Neos.Neos/Classes/Domain/Export/SiteExportProcessorFactory.php b/Neos.Neos/Classes/Domain/Export/SiteExportProcessorFactory.php index 07832016e15..ed313aba1da 100644 --- a/Neos.Neos/Classes/Domain/Export/SiteExportProcessorFactory.php +++ b/Neos.Neos/Classes/Domain/Export/SiteExportProcessorFactory.php @@ -18,7 +18,6 @@ use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceFactoryInterface; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; -use Neos\Neos\Domain\Import\LiveWorkspaceIsEmptyProcessor; use Neos\Neos\Domain\Repository\SiteRepository; /** diff --git a/Neos.Neos/Classes/Domain/Import/LiveWorkspaceCreationProcessor.php b/Neos.Neos/Classes/Domain/Import/LiveWorkspaceCreationProcessor.php index 34a140eb7bd..97824318b99 100644 --- a/Neos.Neos/Classes/Domain/Import/LiveWorkspaceCreationProcessor.php +++ b/Neos.Neos/Classes/Domain/Import/LiveWorkspaceCreationProcessor.php @@ -39,8 +39,13 @@ public function __construct( public function run(ProcessingContext $context): void { $context->dispatch(Severity::NOTICE, 'Creating live workspace'); - $existingWorkspace = $this->contentRepository->findWorkspaceByName(WorkspaceName::forLive()); - if ($existingWorkspace !== null) { + $liveWorkspace = $this->contentRepository->findWorkspaceByName(WorkspaceName::forLive()); + $liveContentStreamVersion = $liveWorkspace ? $this->contentRepository->findContentStreamById($liveWorkspace->currentContentStreamId)?->version : null; + if ($liveWorkspace && $liveContentStreamVersion !== 0) { + // todo we cannot use `hasPublishableChanges` here... maybe introduce `hasChanges`? + throw new \RuntimeException('Live workspace already contains content please run "site:pruneAll" before importing.'); + } + if ($liveWorkspace !== null) { $context->dispatch(Severity::NOTICE, 'Workspace already exists, skipping'); return; } diff --git a/Neos.Neos/Classes/Domain/Import/LiveWorkspaceIsEmptyProcessor.php b/Neos.Neos/Classes/Domain/Import/LiveWorkspaceIsEmptyProcessor.php deleted file mode 100644 index 6d69c4f771e..00000000000 --- a/Neos.Neos/Classes/Domain/Import/LiveWorkspaceIsEmptyProcessor.php +++ /dev/null @@ -1,54 +0,0 @@ -dispatch(Severity::NOTICE, 'Ensures empty live workspace'); - - if ($this->workspaceHasEvents(WorkspaceName::forLive())) { - throw new \RuntimeException('Live workspace already contains events please run "site:pruneAll" before importing.'); - } - } - - private function workspaceHasEvents(WorkspaceName $workspaceName): bool - { - /** @phpstan-ignore neos.cr.internal */ - $workspaceStreamName = WorkspaceEventStreamName::fromWorkspaceName($workspaceName)->getEventStreamName(); - $eventStream = $this->eventStore->load($workspaceStreamName); - foreach ($eventStream as $event) { - return true; - } - return false; - } -} diff --git a/Neos.Neos/Classes/Domain/Import/LiveWorkspaceIsEmptyProcessorFactory.php b/Neos.Neos/Classes/Domain/Import/LiveWorkspaceIsEmptyProcessorFactory.php deleted file mode 100644 index d74eaac3215..00000000000 --- a/Neos.Neos/Classes/Domain/Import/LiveWorkspaceIsEmptyProcessorFactory.php +++ /dev/null @@ -1,31 +0,0 @@ - - */ -final readonly class LiveWorkspaceIsEmptyProcessorFactory implements ContentRepositoryServiceFactoryInterface -{ - public function build(ContentRepositoryServiceFactoryDependencies $serviceFactoryDependencies): ContentRepositoryServiceInterface - { - return new LiveWorkspaceIsEmptyProcessor($serviceFactoryDependencies->eventStore); - } -} diff --git a/Neos.Neos/Classes/Domain/Pruning/SitePruningProcessorFactory.php b/Neos.Neos/Classes/Domain/Pruning/SitePruningProcessorFactory.php index 5c23d8cc895..40c52fa07bd 100644 --- a/Neos.Neos/Classes/Domain/Pruning/SitePruningProcessorFactory.php +++ b/Neos.Neos/Classes/Domain/Pruning/SitePruningProcessorFactory.php @@ -18,10 +18,7 @@ use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceFactoryInterface; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; -use Neos\ContentRepository\Export\Processors\EventExportProcessor; -use Neos\Flow\Persistence\Doctrine\PersistenceManager; use Neos\Flow\Persistence\PersistenceManagerInterface; -use Neos\Neos\Domain\Import\LiveWorkspaceIsEmptyProcessor; use Neos\Neos\Domain\Repository\DomainRepository; use Neos\Neos\Domain\Repository\SiteRepository; diff --git a/Neos.Neos/Classes/Domain/Service/SiteImportService.php b/Neos.Neos/Classes/Domain/Service/SiteImportService.php index 483dd2056df..7cfee7c08a1 100644 --- a/Neos.Neos/Classes/Domain/Service/SiteImportService.php +++ b/Neos.Neos/Classes/Domain/Service/SiteImportService.php @@ -25,9 +25,7 @@ use Neos\ContentRepository\Export\Processors\AssetRepositoryImportProcessor; use Neos\ContentRepository\Export\Severity; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; -use Neos\ContentRepositoryRegistry\Processors\ProjectionCatchupProcessor; use Neos\ContentRepositoryRegistry\Processors\ProjectionCatchupProcessorFactory; -use Neos\ContentRepositoryRegistry\Service\ProjectionServiceFactory; use Neos\Flow\Annotations as Flow; use Neos\Flow\Persistence\Doctrine\Service as DoctrineService; use Neos\Flow\Persistence\PersistenceManagerInterface; @@ -36,7 +34,6 @@ use Neos\Media\Domain\Repository\AssetRepository; use Neos\Neos\Domain\Import\DoctrineMigrateProcessor; use Neos\Neos\Domain\Import\LiveWorkspaceCreationProcessor; -use Neos\Neos\Domain\Import\LiveWorkspaceIsEmptyProcessorFactory; use Neos\Neos\Domain\Import\SiteCreationProcessor; use Neos\Neos\Domain\Repository\DomainRepository; use Neos\Neos\Domain\Repository\SiteRepository; @@ -75,9 +72,8 @@ public function importFromPath(ContentRepositoryId $contentRepositoryId, string $processors = [ 'Run doctrine migrations' => new DoctrineMigrateProcessor($this->doctrineService), 'Setup content repository' => $this->contentRepositoryRegistry->buildService($contentRepositoryId, new ContentRepositorySetupProcessorFactory()), - 'Verify Live workspace does not exist yet' => $this->contentRepositoryRegistry->buildService($contentRepositoryId, new LiveWorkspaceIsEmptyProcessorFactory()), - 'Create Neos sites' => new SiteCreationProcessor($this->siteRepository, $this->domainRepository, $this->persistenceManager), 'Create Live workspace' => new LiveWorkspaceCreationProcessor($contentRepository, $this->workspaceService), + 'Create Neos sites' => new SiteCreationProcessor($this->siteRepository, $this->domainRepository, $this->persistenceManager), 'Import events' => $this->contentRepositoryRegistry->buildService($contentRepositoryId, new EventStoreImportProcessorFactory(WorkspaceName::forLive(), keepEventIds: true)), 'Import assets' => new AssetRepositoryImportProcessor($this->assetRepository, $this->resourceRepository, $this->resourceManager, $this->persistenceManager), 'Catchup all projections' => $this->contentRepositoryRegistry->buildService($contentRepositoryId, $this->projectionCatchupProcessorFactory), diff --git a/Neos.Neos/Classes/Domain/Service/SitePruningService.php b/Neos.Neos/Classes/Domain/Service/SitePruningService.php index 15eabcf7911..ed6ef56c3ff 100644 --- a/Neos.Neos/Classes/Domain/Service/SitePruningService.php +++ b/Neos.Neos/Classes/Domain/Service/SitePruningService.php @@ -18,27 +18,13 @@ use League\Flysystem\Local\LocalFilesystemAdapter; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; -use Neos\ContentRepository\Export\Factory\ContentRepositorySetupProcessorFactory; -use Neos\ContentRepository\Export\Factory\EventStoreImportProcessorFactory; use Neos\ContentRepository\Export\ProcessingContext; use Neos\ContentRepository\Export\ProcessorInterface; -use Neos\ContentRepository\Export\Processors\AssetRepositoryImportProcessor; use Neos\ContentRepository\Export\Severity; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; -use Neos\ContentRepositoryRegistry\Processors\ProjectionCatchupProcessor; -use Neos\ContentRepositoryRegistry\Processors\ProjectionCatchupProcessorFactory; use Neos\ContentRepositoryRegistry\Processors\ProjectionReplayProcessorFactory; -use Neos\ContentRepositoryRegistry\Service\ProjectionServiceFactory; use Neos\Flow\Annotations as Flow; -use Neos\Flow\Persistence\Doctrine\Service as DoctrineService; use Neos\Flow\Persistence\PersistenceManagerInterface; -use Neos\Flow\ResourceManagement\ResourceManager; -use Neos\Flow\ResourceManagement\ResourceRepository; -use Neos\Media\Domain\Repository\AssetRepository; -use Neos\Neos\Domain\Import\DoctrineMigrateProcessor; -use Neos\Neos\Domain\Import\LiveWorkspaceCreationProcessor; -use Neos\Neos\Domain\Import\LiveWorkspaceIsEmptyProcessorFactory; -use Neos\Neos\Domain\Import\SiteCreationProcessor; use Neos\Neos\Domain\Pruning\ContentRepositoryPruningProcessorFactory; use Neos\Neos\Domain\Pruning\RoleAndMetadataPruningProcessorFactory; use Neos\Neos\Domain\Pruning\SitePruningProcessorFactory; From e57ce53ea98ad8b3e72d8452a21c1208516a2ef3 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 2 Nov 2024 17:29:36 +0100 Subject: [PATCH 059/214] TASK: Make `RoleAndMetadataPruningProcessor` a simple processor we dont want to use the internal cr service factory pattern here --- .../RoleAndMetadataPruningProcessor.php | 3 +- ...RoleAndMetadataPruningProcessorFactory.php | 39 ------------------- .../Domain/Service/SitePruningService.php | 9 +---- 3 files changed, 3 insertions(+), 48 deletions(-) delete mode 100644 Neos.Neos/Classes/Domain/Pruning/RoleAndMetadataPruningProcessorFactory.php diff --git a/Neos.Neos/Classes/Domain/Pruning/RoleAndMetadataPruningProcessor.php b/Neos.Neos/Classes/Domain/Pruning/RoleAndMetadataPruningProcessor.php index f341f825b24..063f7e6f4de 100644 --- a/Neos.Neos/Classes/Domain/Pruning/RoleAndMetadataPruningProcessor.php +++ b/Neos.Neos/Classes/Domain/Pruning/RoleAndMetadataPruningProcessor.php @@ -14,7 +14,6 @@ namespace Neos\Neos\Domain\Pruning; -use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Export\ProcessingContext; use Neos\ContentRepository\Export\ProcessorInterface; @@ -23,7 +22,7 @@ /** * Pruning processor that removes role and metadata for a specified content repository */ -final readonly class RoleAndMetadataPruningProcessor implements ProcessorInterface, ContentRepositoryServiceInterface +final readonly class RoleAndMetadataPruningProcessor implements ProcessorInterface { public function __construct( private ContentRepositoryId $contentRepositoryId, diff --git a/Neos.Neos/Classes/Domain/Pruning/RoleAndMetadataPruningProcessorFactory.php b/Neos.Neos/Classes/Domain/Pruning/RoleAndMetadataPruningProcessorFactory.php deleted file mode 100644 index 456e0c10623..00000000000 --- a/Neos.Neos/Classes/Domain/Pruning/RoleAndMetadataPruningProcessorFactory.php +++ /dev/null @@ -1,39 +0,0 @@ - - */ -final readonly class RoleAndMetadataPruningProcessorFactory implements ContentRepositoryServiceFactoryInterface -{ - public function __construct( - private WorkspaceService $workspaceService, - ) { - } - - public function build(ContentRepositoryServiceFactoryDependencies $serviceFactoryDependencies): ContentRepositoryServiceInterface - { - return new RoleAndMetadataPruningProcessor( - $serviceFactoryDependencies->contentRepositoryId, - $this->workspaceService, - ); - } -} diff --git a/Neos.Neos/Classes/Domain/Service/SitePruningService.php b/Neos.Neos/Classes/Domain/Service/SitePruningService.php index ed6ef56c3ff..a77590d0bb4 100644 --- a/Neos.Neos/Classes/Domain/Service/SitePruningService.php +++ b/Neos.Neos/Classes/Domain/Service/SitePruningService.php @@ -26,7 +26,7 @@ use Neos\Flow\Annotations as Flow; use Neos\Flow\Persistence\PersistenceManagerInterface; use Neos\Neos\Domain\Pruning\ContentRepositoryPruningProcessorFactory; -use Neos\Neos\Domain\Pruning\RoleAndMetadataPruningProcessorFactory; +use Neos\Neos\Domain\Pruning\RoleAndMetadataPruningProcessor; use Neos\Neos\Domain\Pruning\SitePruningProcessorFactory; use Neos\Neos\Domain\Repository\DomainRepository; use Neos\Neos\Domain\Repository\SiteRepository; @@ -68,12 +68,7 @@ public function pruneAll(ContentRepositoryId $contentRepositoryId, \Closure $onP $contentRepositoryId, new ContentRepositoryPruningProcessorFactory() ), - 'Prune roles and metadata' => $this->contentRepositoryRegistry->buildService( - $contentRepositoryId, - new RoleAndMetadataPruningProcessorFactory( - $this->workspaceService - ) - ), + 'Prune roles and metadata' => new RoleAndMetadataPruningProcessor($contentRepositoryId, $this->workspaceService), 'Replay all projections' => $this->contentRepositoryRegistry->buildService($contentRepositoryId, $this->projectionReplayProcessorFactory), ]; From ae03530c77ff5021f72e156ce929cfe184b17627 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 2 Nov 2024 17:34:29 +0100 Subject: [PATCH 060/214] TASK: Make `ContentRepositoryPruningProcessor` use the `ContentStreamPruner` --- .../ContentRepositoryPruningProcessor.php | 19 ++-------- ...ntentRepositoryPruningProcessorFactory.php | 37 ------------------- .../Domain/Service/SitePruningService.php | 11 ++++-- 3 files changed, 10 insertions(+), 57 deletions(-) delete mode 100644 Neos.Neos/Classes/Domain/Pruning/ContentRepositoryPruningProcessorFactory.php diff --git a/Neos.Neos/Classes/Domain/Pruning/ContentRepositoryPruningProcessor.php b/Neos.Neos/Classes/Domain/Pruning/ContentRepositoryPruningProcessor.php index d6f6330d373..4660e6aea7e 100644 --- a/Neos.Neos/Classes/Domain/Pruning/ContentRepositoryPruningProcessor.php +++ b/Neos.Neos/Classes/Domain/Pruning/ContentRepositoryPruningProcessor.php @@ -14,13 +14,10 @@ namespace Neos\Neos\Domain\Pruning; -use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface; -use Neos\ContentRepository\Core\Feature\ContentStreamEventStreamName; -use Neos\ContentRepository\Core\Feature\WorkspaceEventStreamName; +use Neos\ContentRepository\Core\Service\ContentStreamPruner; use Neos\ContentRepository\Export\ProcessingContext; use Neos\ContentRepository\Export\ProcessorInterface; -use Neos\EventStore\EventStoreInterface; /** * Pruning processor that removes all events from the given cr @@ -28,22 +25,12 @@ final readonly class ContentRepositoryPruningProcessor implements ProcessorInterface, ContentRepositoryServiceInterface { public function __construct( - private ContentRepository $contentRepository, - private EventStoreInterface $eventStore, + private ContentStreamPruner $contentStreamPruner, ) { } public function run(ProcessingContext $context): void { - foreach ($this->contentRepository->findContentStreams() as $contentStream) { - /** @phpstan-ignore-next-line calling internal method */ - $streamName = ContentStreamEventStreamName::fromContentStreamId($contentStream->id)->getEventStreamName(); - $this->eventStore->deleteStream($streamName); - } - foreach ($this->contentRepository->findWorkspaces() as $workspace) { - /** @phpstan-ignore-next-line calling internal method */ - $streamName = WorkspaceEventStreamName::fromWorkspaceName($workspace->workspaceName)->getEventStreamName(); - $this->eventStore->deleteStream($streamName); - } + $this->contentStreamPruner->pruneAllWorkspacesAndContentStreamsFromEventStream(); } } diff --git a/Neos.Neos/Classes/Domain/Pruning/ContentRepositoryPruningProcessorFactory.php b/Neos.Neos/Classes/Domain/Pruning/ContentRepositoryPruningProcessorFactory.php deleted file mode 100644 index db2299fa348..00000000000 --- a/Neos.Neos/Classes/Domain/Pruning/ContentRepositoryPruningProcessorFactory.php +++ /dev/null @@ -1,37 +0,0 @@ - - */ -final readonly class ContentRepositoryPruningProcessorFactory implements ContentRepositoryServiceFactoryInterface -{ - public function __construct() - { - } - - public function build(ContentRepositoryServiceFactoryDependencies $serviceFactoryDependencies): ContentRepositoryServiceInterface - { - return new ContentRepositoryPruningProcessor( - $serviceFactoryDependencies->contentRepository, - $serviceFactoryDependencies->eventStore, - ); - } -} diff --git a/Neos.Neos/Classes/Domain/Service/SitePruningService.php b/Neos.Neos/Classes/Domain/Service/SitePruningService.php index a77590d0bb4..25fd6e8c2d3 100644 --- a/Neos.Neos/Classes/Domain/Service/SitePruningService.php +++ b/Neos.Neos/Classes/Domain/Service/SitePruningService.php @@ -16,6 +16,7 @@ use League\Flysystem\Filesystem; use League\Flysystem\Local\LocalFilesystemAdapter; +use Neos\ContentRepository\Core\Service\ContentStreamPrunerFactory; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepository\Export\ProcessingContext; @@ -25,7 +26,7 @@ use Neos\ContentRepositoryRegistry\Processors\ProjectionReplayProcessorFactory; use Neos\Flow\Annotations as Flow; use Neos\Flow\Persistence\PersistenceManagerInterface; -use Neos\Neos\Domain\Pruning\ContentRepositoryPruningProcessorFactory; +use Neos\Neos\Domain\Pruning\ContentRepositoryPruningProcessor; use Neos\Neos\Domain\Pruning\RoleAndMetadataPruningProcessor; use Neos\Neos\Domain\Pruning\SitePruningProcessorFactory; use Neos\Neos\Domain\Repository\DomainRepository; @@ -64,9 +65,11 @@ public function pruneAll(ContentRepositoryId $contentRepositoryId, \Closure $onP $this->persistenceManager ) ), - 'Prune content repository' => $this->contentRepositoryRegistry->buildService( - $contentRepositoryId, - new ContentRepositoryPruningProcessorFactory() + 'Prune content repository' => new ContentRepositoryPruningProcessor( + $this->contentRepositoryRegistry->buildService( + $contentRepositoryId, + new ContentStreamPrunerFactory() + ) ), 'Prune roles and metadata' => new RoleAndMetadataPruningProcessor($contentRepositoryId, $this->workspaceService), 'Replay all projections' => $this->contentRepositoryRegistry->buildService($contentRepositoryId, $this->projectionReplayProcessorFactory), From cb82c2d71e5d000a0781a3a84a8b95a49ea4f845 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 2 Nov 2024 17:43:00 +0100 Subject: [PATCH 061/214] TASK: Turn `SitePruningProcessor` into simple processor without factory --- .../Domain/Pruning/SitePruningProcessor.php | 8 +--- .../Pruning/SitePruningProcessorFactory.php | 48 ------------------- .../Domain/Service/SitePruningService.php | 20 ++++---- 3 files changed, 11 insertions(+), 65 deletions(-) delete mode 100644 Neos.Neos/Classes/Domain/Pruning/SitePruningProcessorFactory.php diff --git a/Neos.Neos/Classes/Domain/Pruning/SitePruningProcessor.php b/Neos.Neos/Classes/Domain/Pruning/SitePruningProcessor.php index 7c272774d91..fcb6b9c7a89 100644 --- a/Neos.Neos/Classes/Domain/Pruning/SitePruningProcessor.php +++ b/Neos.Neos/Classes/Domain/Pruning/SitePruningProcessor.php @@ -14,17 +14,11 @@ namespace Neos\Neos\Domain\Pruning; -use JsonException; use Neos\ContentRepository\Core\ContentRepository; -use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface; -use Neos\ContentRepository\Core\SharedModel\Node\NodeName; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; -use Neos\ContentRepository\Export\Event\ValueObject\ExportedEvent; use Neos\ContentRepository\Export\ProcessingContext; use Neos\ContentRepository\Export\ProcessorInterface; -use Neos\ContentRepository\Export\Severity; use Neos\Flow\Persistence\PersistenceManagerInterface; -use Neos\Neos\Domain\Model\Domain; use Neos\Neos\Domain\Model\Site; use Neos\Neos\Domain\Repository\DomainRepository; use Neos\Neos\Domain\Repository\SiteRepository; @@ -33,7 +27,7 @@ /** * Pruning processor that removes all Neos {@see Site} instances referenced by the current content repository */ -final readonly class SitePruningProcessor implements ProcessorInterface, ContentRepositoryServiceInterface +final readonly class SitePruningProcessor implements ProcessorInterface { public function __construct( private ContentRepository $contentRepository, diff --git a/Neos.Neos/Classes/Domain/Pruning/SitePruningProcessorFactory.php b/Neos.Neos/Classes/Domain/Pruning/SitePruningProcessorFactory.php deleted file mode 100644 index 40c52fa07bd..00000000000 --- a/Neos.Neos/Classes/Domain/Pruning/SitePruningProcessorFactory.php +++ /dev/null @@ -1,48 +0,0 @@ - - */ -final readonly class SitePruningProcessorFactory implements ContentRepositoryServiceFactoryInterface -{ - public function __construct( - private WorkspaceName $workspaceName, - private SiteRepository $siteRepository, - private DomainRepository $domainRepository, - private PersistenceManagerInterface $persistenceManager, - ) { - } - - public function build(ContentRepositoryServiceFactoryDependencies $serviceFactoryDependencies): ContentRepositoryServiceInterface - { - return new SitePruningProcessor( - $serviceFactoryDependencies->contentRepository, - $this->workspaceName, - $this->siteRepository, - $this->domainRepository, - $this->persistenceManager - ); - } -} diff --git a/Neos.Neos/Classes/Domain/Service/SitePruningService.php b/Neos.Neos/Classes/Domain/Service/SitePruningService.php index 25fd6e8c2d3..04dd6875923 100644 --- a/Neos.Neos/Classes/Domain/Service/SitePruningService.php +++ b/Neos.Neos/Classes/Domain/Service/SitePruningService.php @@ -28,7 +28,7 @@ use Neos\Flow\Persistence\PersistenceManagerInterface; use Neos\Neos\Domain\Pruning\ContentRepositoryPruningProcessor; use Neos\Neos\Domain\Pruning\RoleAndMetadataPruningProcessor; -use Neos\Neos\Domain\Pruning\SitePruningProcessorFactory; +use Neos\Neos\Domain\Pruning\SitePruningProcessor; use Neos\Neos\Domain\Repository\DomainRepository; use Neos\Neos\Domain\Repository\SiteRepository; @@ -54,16 +54,16 @@ public function pruneAll(ContentRepositoryId $contentRepositoryId, \Closure $onP $filesystem = new Filesystem(new LocalFilesystemAdapter('.')); $context = new ProcessingContext($filesystem, $onMessage); - /** @var array $processors */ + $contentRepository = $this->contentRepositoryRegistry->get($contentRepositoryId); + + /** @var array $processors */ $processors = [ - 'Remove site nodes' => $this->contentRepositoryRegistry->buildService( - $contentRepositoryId, - new SitePruningProcessorFactory( - WorkspaceName::forLive(), - $this->siteRepository, - $this->domainRepository, - $this->persistenceManager - ) + 'Remove site nodes' => new SitePruningProcessor( + $contentRepository, + WorkspaceName::forLive(), + $this->siteRepository, + $this->domainRepository, + $this->persistenceManager ), 'Prune content repository' => new ContentRepositoryPruningProcessor( $this->contentRepositoryRegistry->buildService( From dff6885bd6f92331772a2dbb8148756e243167b4 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 2 Nov 2024 17:45:41 +0100 Subject: [PATCH 062/214] TASK: Turn `SiteExportProcessor` into simple processor without factory --- .../Domain/Export/SiteExportProcessor.php | 3 +- .../Export/SiteExportProcessorFactory.php | 42 ------------------- .../Domain/Service/SiteExportService.php | 12 +++--- 3 files changed, 6 insertions(+), 51 deletions(-) delete mode 100644 Neos.Neos/Classes/Domain/Export/SiteExportProcessorFactory.php diff --git a/Neos.Neos/Classes/Domain/Export/SiteExportProcessor.php b/Neos.Neos/Classes/Domain/Export/SiteExportProcessor.php index 643845d8b49..f71a91f4c65 100644 --- a/Neos.Neos/Classes/Domain/Export/SiteExportProcessor.php +++ b/Neos.Neos/Classes/Domain/Export/SiteExportProcessor.php @@ -15,7 +15,6 @@ namespace Neos\Neos\Domain\Export; use Neos\ContentRepository\Core\ContentRepository; -use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepository\Export\ProcessingContext; use Neos\ContentRepository\Export\ProcessorInterface; @@ -31,7 +30,7 @@ * @phpstan-type SiteShape array{name:string, siteResourcesPackageKey:string, nodeName?: string, online?:bool, domains?: ?DomainShape[] } * */ -final readonly class SiteExportProcessor implements ProcessorInterface, ContentRepositoryServiceInterface +final readonly class SiteExportProcessor implements ProcessorInterface { public function __construct( private ContentRepository $contentRepository, diff --git a/Neos.Neos/Classes/Domain/Export/SiteExportProcessorFactory.php b/Neos.Neos/Classes/Domain/Export/SiteExportProcessorFactory.php deleted file mode 100644 index ed313aba1da..00000000000 --- a/Neos.Neos/Classes/Domain/Export/SiteExportProcessorFactory.php +++ /dev/null @@ -1,42 +0,0 @@ - - */ -final readonly class SiteExportProcessorFactory implements ContentRepositoryServiceFactoryInterface -{ - public function __construct( - private WorkspaceName $workspaceName, - private SiteRepository $siteRepository, - ) { - } - - public function build(ContentRepositoryServiceFactoryDependencies $serviceFactoryDependencies): ContentRepositoryServiceInterface - { - return new SiteExportProcessor( - $serviceFactoryDependencies->contentRepository, - $this->workspaceName, - $this->siteRepository, - ); - } -} diff --git a/Neos.Neos/Classes/Domain/Service/SiteExportService.php b/Neos.Neos/Classes/Domain/Service/SiteExportService.php index 913351c5112..6deee3f87ba 100644 --- a/Neos.Neos/Classes/Domain/Service/SiteExportService.php +++ b/Neos.Neos/Classes/Domain/Service/SiteExportService.php @@ -27,7 +27,7 @@ use Neos\Flow\Annotations as Flow; use Neos\Media\Domain\Repository\AssetRepository; use Neos\Neos\AssetUsage\AssetUsageService; -use Neos\Neos\Domain\Export\SiteExportProcessorFactory; +use Neos\Neos\Domain\Export\SiteExportProcessor; use Neos\Neos\Domain\Repository\SiteRepository; #[Flow\Scope('singleton')] @@ -73,12 +73,10 @@ public function exportToPath(ContentRepositoryId $contentRepositoryId, string $p $liveWorkspace, $this->assetUsageService ), - 'Export sites' => $this->contentRepositoryRegistry->buildService( - $contentRepositoryId, - new SiteExportProcessorFactory( - $liveWorkspace->workspaceName, - $this->siteRepository, - ) + 'Export sites' => new SiteExportProcessor( + $contentRepository, + $liveWorkspace->workspaceName, + $this->siteRepository ), ]; foreach ($processors as $processorLabel => $processor) { From 71c65a2ecebdc335ccd76b31aef8acca036f838d Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 2 Nov 2024 17:53:00 +0100 Subject: [PATCH 063/214] TASK: Turn `ProjectionCatchupProcessor` and `ProjectionReplayProcessor` into simple processor without factory --- .../Processors/ProjectionCatchupProcessor.php | 4 +-- .../ProjectionCatchupProcessorFactory.php | 36 ------------------- .../Processors/ProjectionReplayProcessor.php | 4 +-- .../ProjectionReplayProcessorFactory.php | 34 ------------------ .../ContentRepositoryPruningProcessor.php | 3 +- .../Domain/Service/SiteImportService.php | 6 ++-- .../Domain/Service/SitePruningService.php | 6 ++-- 7 files changed, 9 insertions(+), 84 deletions(-) delete mode 100644 Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionCatchupProcessorFactory.php delete mode 100644 Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionReplayProcessorFactory.php diff --git a/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionCatchupProcessor.php b/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionCatchupProcessor.php index 3f837b91571..c6e66fc2e9e 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionCatchupProcessor.php +++ b/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionCatchupProcessor.php @@ -3,7 +3,6 @@ namespace Neos\ContentRepositoryRegistry\Processors; -use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface; use Neos\ContentRepository\Core\Projection\CatchUpOptions; use Neos\ContentRepository\Export\ProcessingContext; use Neos\ContentRepository\Export\ProcessorInterface; @@ -14,9 +13,8 @@ * * @internal this is currently only used by the {@see SiteImportService} {@see SitePruningService} */ -final class ProjectionCatchupProcessor implements ProcessorInterface, ContentRepositoryServiceInterface +final class ProjectionCatchupProcessor implements ProcessorInterface { - public function __construct( private readonly ProjectionService $projectionservice, ) { diff --git a/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionCatchupProcessorFactory.php b/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionCatchupProcessorFactory.php deleted file mode 100644 index 305e8c30172..00000000000 --- a/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionCatchupProcessorFactory.php +++ /dev/null @@ -1,36 +0,0 @@ - - * @internal this is currently only used by the {@see SiteImportService} {@see SitePruningService} - */ -#[Flow\Scope("singleton")] -final class ProjectionCatchupProcessorFactory implements ContentRepositoryServiceFactoryInterface -{ - - public function build(ContentRepositoryServiceFactoryDependencies $serviceFactoryDependencies): ContentRepositoryServiceInterface - { - return new ProjectionCatchupProcessor( - new ProjectionService( - $serviceFactoryDependencies->projectionsAndCatchUpHooks->projections, - $serviceFactoryDependencies->contentRepository, - $serviceFactoryDependencies->eventStore, - ) - ); - } -} diff --git a/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionReplayProcessor.php b/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionReplayProcessor.php index 91a764eef56..76c6c5b49d4 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionReplayProcessor.php +++ b/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionReplayProcessor.php @@ -3,7 +3,6 @@ namespace Neos\ContentRepositoryRegistry\Processors; -use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface; use Neos\ContentRepository\Core\Projection\CatchUpOptions; use Neos\ContentRepository\Export\ProcessingContext; use Neos\ContentRepository\Export\ProcessorInterface; @@ -15,9 +14,8 @@ * * @internal this is currently only used by the {@see SitePruningService} */ -final class ProjectionReplayProcessor implements ProcessorInterface, ContentRepositoryServiceInterface +final class ProjectionReplayProcessor implements ProcessorInterface { - public function __construct( private readonly ProjectionService $projectionService, ) { diff --git a/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionReplayProcessorFactory.php b/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionReplayProcessorFactory.php deleted file mode 100644 index 6c9c32b6200..00000000000 --- a/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionReplayProcessorFactory.php +++ /dev/null @@ -1,34 +0,0 @@ - - * @internal this is currently only used by the {@see SitePruningService} - */ -#[Flow\Scope("singleton")] -final class ProjectionReplayProcessorFactory implements ContentRepositoryServiceFactoryInterface -{ - public function build(ContentRepositoryServiceFactoryDependencies $serviceFactoryDependencies): ContentRepositoryServiceInterface - { - return new ProjectionReplayProcessor( - new ProjectionService( - $serviceFactoryDependencies->projectionsAndCatchUpHooks->projections, - $serviceFactoryDependencies->contentRepository, - $serviceFactoryDependencies->eventStore, - ) - ); - } -} diff --git a/Neos.Neos/Classes/Domain/Pruning/ContentRepositoryPruningProcessor.php b/Neos.Neos/Classes/Domain/Pruning/ContentRepositoryPruningProcessor.php index 4660e6aea7e..0b94195c9d1 100644 --- a/Neos.Neos/Classes/Domain/Pruning/ContentRepositoryPruningProcessor.php +++ b/Neos.Neos/Classes/Domain/Pruning/ContentRepositoryPruningProcessor.php @@ -14,7 +14,6 @@ namespace Neos\Neos\Domain\Pruning; -use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface; use Neos\ContentRepository\Core\Service\ContentStreamPruner; use Neos\ContentRepository\Export\ProcessingContext; use Neos\ContentRepository\Export\ProcessorInterface; @@ -22,7 +21,7 @@ /** * Pruning processor that removes all events from the given cr */ -final readonly class ContentRepositoryPruningProcessor implements ProcessorInterface, ContentRepositoryServiceInterface +final readonly class ContentRepositoryPruningProcessor implements ProcessorInterface { public function __construct( private ContentStreamPruner $contentStreamPruner, diff --git a/Neos.Neos/Classes/Domain/Service/SiteImportService.php b/Neos.Neos/Classes/Domain/Service/SiteImportService.php index 7cfee7c08a1..b7276a6f8c4 100644 --- a/Neos.Neos/Classes/Domain/Service/SiteImportService.php +++ b/Neos.Neos/Classes/Domain/Service/SiteImportService.php @@ -25,7 +25,8 @@ use Neos\ContentRepository\Export\Processors\AssetRepositoryImportProcessor; use Neos\ContentRepository\Export\Severity; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; -use Neos\ContentRepositoryRegistry\Processors\ProjectionCatchupProcessorFactory; +use Neos\ContentRepositoryRegistry\Processors\ProjectionCatchupProcessor; +use Neos\ContentRepositoryRegistry\Service\ProjectionServiceFactory; use Neos\Flow\Annotations as Flow; use Neos\Flow\Persistence\Doctrine\Service as DoctrineService; use Neos\Flow\Persistence\PersistenceManagerInterface; @@ -51,7 +52,6 @@ public function __construct( private ResourceManager $resourceManager, private PersistenceManagerInterface $persistenceManager, private WorkspaceService $workspaceService, - private ProjectionCatchupProcessorFactory $projectionCatchupProcessorFactory, ) { } @@ -76,7 +76,7 @@ public function importFromPath(ContentRepositoryId $contentRepositoryId, string 'Create Neos sites' => new SiteCreationProcessor($this->siteRepository, $this->domainRepository, $this->persistenceManager), 'Import events' => $this->contentRepositoryRegistry->buildService($contentRepositoryId, new EventStoreImportProcessorFactory(WorkspaceName::forLive(), keepEventIds: true)), 'Import assets' => new AssetRepositoryImportProcessor($this->assetRepository, $this->resourceRepository, $this->resourceManager, $this->persistenceManager), - 'Catchup all projections' => $this->contentRepositoryRegistry->buildService($contentRepositoryId, $this->projectionCatchupProcessorFactory), + 'Catchup all projections' => new ProjectionCatchupProcessor($this->contentRepositoryRegistry->buildService($contentRepositoryId, new ProjectionServiceFactory())), ]; foreach ($processors as $processorLabel => $processor) { diff --git a/Neos.Neos/Classes/Domain/Service/SitePruningService.php b/Neos.Neos/Classes/Domain/Service/SitePruningService.php index 04dd6875923..f427c9fac81 100644 --- a/Neos.Neos/Classes/Domain/Service/SitePruningService.php +++ b/Neos.Neos/Classes/Domain/Service/SitePruningService.php @@ -23,7 +23,8 @@ use Neos\ContentRepository\Export\ProcessorInterface; use Neos\ContentRepository\Export\Severity; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; -use Neos\ContentRepositoryRegistry\Processors\ProjectionReplayProcessorFactory; +use Neos\ContentRepositoryRegistry\Processors\ProjectionCatchupProcessor; +use Neos\ContentRepositoryRegistry\Service\ProjectionServiceFactory; use Neos\Flow\Annotations as Flow; use Neos\Flow\Persistence\PersistenceManagerInterface; use Neos\Neos\Domain\Pruning\ContentRepositoryPruningProcessor; @@ -40,7 +41,6 @@ public function __construct( private SiteRepository $siteRepository, private DomainRepository $domainRepository, private PersistenceManagerInterface $persistenceManager, - private ProjectionReplayProcessorFactory $projectionReplayProcessorFactory, private WorkspaceService $workspaceService, ) { } @@ -72,7 +72,7 @@ public function pruneAll(ContentRepositoryId $contentRepositoryId, \Closure $onP ) ), 'Prune roles and metadata' => new RoleAndMetadataPruningProcessor($contentRepositoryId, $this->workspaceService), - 'Replay all projections' => $this->contentRepositoryRegistry->buildService($contentRepositoryId, $this->projectionReplayProcessorFactory), + 'Replay all projections' => new ProjectionCatchupProcessor($this->contentRepositoryRegistry->buildService($contentRepositoryId, new ProjectionServiceFactory())) ]; foreach ($processors as $processorLabel => $processor) { From 9ba32fd2f3ae14c9a41252fc0bb9baf78499c8ae Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 2 Nov 2024 19:06:23 +0100 Subject: [PATCH 064/214] TASK: Document and improve `extractSitesFromEventStream` --- .../Classes/Domain/Import/SiteCreationProcessor.php | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php b/Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php index d1a1a06d3ba..409809374d3 100644 --- a/Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php +++ b/Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php @@ -26,6 +26,7 @@ use Neos\Neos\Domain\Model\Site; use Neos\Neos\Domain\Repository\DomainRepository; use Neos\Neos\Domain\Repository\SiteRepository; +use Neos\Neos\Domain\Service\NodeTypeNameFactory; /** * Import processor that creates and persists a Neos {@see Site} instance @@ -52,6 +53,7 @@ public function run(ProcessingContext $context): void throw new \RuntimeException("Failed to decode sites.json: {$e->getMessage()}", 1729506117, $e); } } else { + $context->dispatch(Severity::WARNING, 'Deprecated legacy handling: No "sites.json" in export found, attempting to extract neos sites from the events. Please update the export soonish.'); $sites = self::extractSitesFromEventStream($context); } @@ -89,20 +91,21 @@ public function run(ProcessingContext $context): void } /** + * @deprecated with Neos 9 Beta 15 please make sure that exports contain `sites.json` * @return array */ private static function extractSitesFromEventStream(ProcessingContext $context): array { $eventFileResource = $context->files->readStream('events.jsonl'); - $rootNodeAggregateIds = []; + $siteRooNodeAggregateId = null; $sites = []; while (($line = fgets($eventFileResource)) !== false) { $event = ExportedEvent::fromJson($line); - if ($event->type === 'RootNodeAggregateWithNodeWasCreated') { - $rootNodeAggregateIds[] = $event->payload['nodeAggregateId']; + if ($event->type === 'RootNodeAggregateWithNodeWasCreated' && $event->payload['nodeTypeName'] === NodeTypeNameFactory::NAME_SITES) { + $siteRooNodeAggregateId = $event->payload['nodeAggregateId']; continue; } - if ($event->type === 'NodeAggregateWithNodeWasCreated' && in_array($event->payload['parentNodeAggregateId'], $rootNodeAggregateIds, true)) { + if ($event->type === 'NodeAggregateWithNodeWasCreated' && $event->payload['parentNodeAggregateId'] === $siteRooNodeAggregateId) { $sites[] = [ 'siteResourcesPackageKey' => self::extractPackageKeyFromNodeTypeName($event->payload['nodeTypeName']), 'name' => $event->payload['initialPropertyValues']['title']['value'] ?? $event->payload['nodeTypeName'], From 9e7c5c19472ac63e7b1e8b33d3e9a718d366aa2e Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 2 Nov 2024 19:10:41 +0100 Subject: [PATCH 065/214] TASK: Move todo around :) --- Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php | 1 - Neos.Neos/Classes/Domain/Model/Site.php | 5 ++++- Neos.Neos/Classes/Domain/Service/SiteService.php | 1 - 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php b/Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php index 409809374d3..fb6d4222e87 100644 --- a/Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php +++ b/Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php @@ -66,7 +66,6 @@ public function run(ProcessingContext $context): void $context->dispatch(Severity::NOTICE, "Site for node name \"{$siteNodeName->value}\" already exists, skipping"); continue; } - // TODO use node aggregate identifier instead of node name $siteInstance = new Site($siteNodeName->value); $siteInstance->setSiteResourcesPackageKey($site['siteResourcesPackageKey']); $siteInstance->setState(($site['online'] ?? false) ? Site::STATE_OFFLINE : Site::STATE_ONLINE); diff --git a/Neos.Neos/Classes/Domain/Model/Site.php b/Neos.Neos/Classes/Domain/Model/Site.php index d6937a6655d..eb6cedae3c0 100644 --- a/Neos.Neos/Classes/Domain/Model/Site.php +++ b/Neos.Neos/Classes/Domain/Model/Site.php @@ -63,7 +63,10 @@ class Site * Node name of this site in the content repository. * * The first level of nodes of a site can be reached via a path like - * "/Sites/MySite/" where "MySite" is the nodeName. + * "//my-site" where "my-site" is the nodeName. + * + * TODO use node aggregate identifier instead of node name + * see https://github.com/neos/neos-development-collection/issues/4470 * * @var string * @Flow\Identity diff --git a/Neos.Neos/Classes/Domain/Service/SiteService.php b/Neos.Neos/Classes/Domain/Service/SiteService.php index 75dc08b5aea..42f7bea37e6 100644 --- a/Neos.Neos/Classes/Domain/Service/SiteService.php +++ b/Neos.Neos/Classes/Domain/Service/SiteService.php @@ -173,7 +173,6 @@ public function createSite( throw SiteNodeNameIsAlreadyInUseByAnotherSite::butWasAttemptedToBeClaimed($siteNodeName); } - // @todo use node aggregate identifier instead of node name $site = new Site($siteNodeName->value); $site->setSiteResourcesPackageKey($packageKey); $site->setState($inactive ? Site::STATE_OFFLINE : Site::STATE_ONLINE); From 9c9ce5b40bc9dc825f7812ee1063e4b6de80229c Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 2 Nov 2024 19:34:46 +0100 Subject: [PATCH 066/214] TASK: Remove redundant documentation --- .../Classes/Processors/ProjectionCatchupProcessor.php | 4 +--- .../Classes/Processors/ProjectionReplayProcessor.php | 5 +---- .../Classes/Service/ProjectionService.php | 2 +- .../Classes/Service/ProjectionServiceFactory.php | 3 +-- 4 files changed, 4 insertions(+), 10 deletions(-) diff --git a/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionCatchupProcessor.php b/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionCatchupProcessor.php index c6e66fc2e9e..69587bb4806 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionCatchupProcessor.php +++ b/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionCatchupProcessor.php @@ -9,9 +9,7 @@ use Neos\ContentRepositoryRegistry\Service\ProjectionService; /** - * Content Repository service to perform Projection replays - * - * @internal this is currently only used by the {@see SiteImportService} {@see SitePruningService} + * @internal */ final class ProjectionCatchupProcessor implements ProcessorInterface { diff --git a/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionReplayProcessor.php b/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionReplayProcessor.php index 76c6c5b49d4..b77110a42a4 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionReplayProcessor.php +++ b/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionReplayProcessor.php @@ -7,12 +7,9 @@ use Neos\ContentRepository\Export\ProcessingContext; use Neos\ContentRepository\Export\ProcessorInterface; use Neos\ContentRepositoryRegistry\Service\ProjectionService; -use Neos\Neos\Domain\Service\SitePruningService; /** - * Content Repository service to perform Projection replays - * - * @internal this is currently only used by the {@see SitePruningService} + * @internal */ final class ProjectionReplayProcessor implements ProcessorInterface { diff --git a/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionService.php b/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionService.php index 02ae8cd9bc9..06f11984d74 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionService.php +++ b/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionService.php @@ -16,7 +16,7 @@ /** * Content Repository service to perform Projection operations * - * @internal this is currently only used by the {@see SiteCommandController} + * @internal */ final class ProjectionService implements ContentRepositoryServiceInterface { diff --git a/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionServiceFactory.php b/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionServiceFactory.php index 8d167b1c2d8..92114d47f1a 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionServiceFactory.php +++ b/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionServiceFactory.php @@ -6,14 +6,13 @@ use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceFactoryDependencies; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceFactoryInterface; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface; -use Neos\ContentRepositoryRegistry\Command\CrCommandController; use Neos\Flow\Annotations as Flow; /** * Factory for the {@see ProjectionService} * * @implements ContentRepositoryServiceFactoryInterface - * @internal this is currently only used by the {@see CrCommandController} + * @internal */ #[Flow\Scope("singleton")] final class ProjectionServiceFactory implements ContentRepositoryServiceFactoryInterface From f674d5ed13d817e0a62d3bce09153fd2eab2b42a Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 2 Nov 2024 19:39:57 +0100 Subject: [PATCH 067/214] TASK: Reintroduce resetting of projection state via `ProjectionResetProcessor` as in `cr:prune` --- .../Processors/ProjectionResetProcessor.php | 24 +++++++++++++++++++ .../Domain/Service/SitePruningService.php | 8 +++++-- 2 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionResetProcessor.php diff --git a/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionResetProcessor.php b/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionResetProcessor.php new file mode 100644 index 00000000000..7a5a1f9013f --- /dev/null +++ b/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionResetProcessor.php @@ -0,0 +1,24 @@ +projectionService->resetAllProjections(); + } +} diff --git a/Neos.Neos/Classes/Domain/Service/SitePruningService.php b/Neos.Neos/Classes/Domain/Service/SitePruningService.php index f427c9fac81..653f6a45300 100644 --- a/Neos.Neos/Classes/Domain/Service/SitePruningService.php +++ b/Neos.Neos/Classes/Domain/Service/SitePruningService.php @@ -23,7 +23,7 @@ use Neos\ContentRepository\Export\ProcessorInterface; use Neos\ContentRepository\Export\Severity; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; -use Neos\ContentRepositoryRegistry\Processors\ProjectionCatchupProcessor; +use Neos\ContentRepositoryRegistry\Processors\ProjectionResetProcessor; use Neos\ContentRepositoryRegistry\Service\ProjectionServiceFactory; use Neos\Flow\Annotations as Flow; use Neos\Flow\Persistence\PersistenceManagerInterface; @@ -72,7 +72,11 @@ public function pruneAll(ContentRepositoryId $contentRepositoryId, \Closure $onP ) ), 'Prune roles and metadata' => new RoleAndMetadataPruningProcessor($contentRepositoryId, $this->workspaceService), - 'Replay all projections' => new ProjectionCatchupProcessor($this->contentRepositoryRegistry->buildService($contentRepositoryId, new ProjectionServiceFactory())) + 'Reset all projections' => new ProjectionResetProcessor( + $this->contentRepositoryRegistry->buildService( + $contentRepositoryId, + new ProjectionServiceFactory() + )) ]; foreach ($processors as $processorLabel => $processor) { From 7e0021dd15fa8ecfc68a046fcee667aa08ae7865 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 2 Nov 2024 19:44:13 +0100 Subject: [PATCH 068/214] TASK: Use `Processors::fromArray` --- Neos.Neos/Classes/Domain/Service/SiteExportService.php | 7 ++++--- Neos.Neos/Classes/Domain/Service/SiteImportService.php | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Neos.Neos/Classes/Domain/Service/SiteExportService.php b/Neos.Neos/Classes/Domain/Service/SiteExportService.php index 6deee3f87ba..ba305374607 100644 --- a/Neos.Neos/Classes/Domain/Service/SiteExportService.php +++ b/Neos.Neos/Classes/Domain/Service/SiteExportService.php @@ -21,6 +21,7 @@ use Neos\ContentRepository\Export\Factory\EventExportProcessorFactory; use Neos\ContentRepository\Export\ProcessingContext; use Neos\ContentRepository\Export\ProcessorInterface; +use Neos\ContentRepository\Export\Processors; use Neos\ContentRepository\Export\Processors\AssetExportProcessor; use Neos\ContentRepository\Export\Severity; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; @@ -59,8 +60,7 @@ public function exportToPath(ContentRepositoryId $contentRepositoryId, string $p throw new \RuntimeException('Failed to find live workspace', 1716652280); } - /** @var array $processors */ - $processors = [ + $processors = Processors::fromArray([ 'Exporting events' => $this->contentRepositoryRegistry->buildService( $contentRepositoryId, new EventExportProcessorFactory( @@ -78,7 +78,8 @@ public function exportToPath(ContentRepositoryId $contentRepositoryId, string $p $liveWorkspace->workspaceName, $this->siteRepository ), - ]; + ]); + foreach ($processors as $processorLabel => $processor) { ($onProcessor)($processorLabel); $processor->run($context); diff --git a/Neos.Neos/Classes/Domain/Service/SiteImportService.php b/Neos.Neos/Classes/Domain/Service/SiteImportService.php index b7276a6f8c4..1a65686f912 100644 --- a/Neos.Neos/Classes/Domain/Service/SiteImportService.php +++ b/Neos.Neos/Classes/Domain/Service/SiteImportService.php @@ -22,6 +22,7 @@ use Neos\ContentRepository\Export\Factory\EventStoreImportProcessorFactory; use Neos\ContentRepository\Export\ProcessingContext; use Neos\ContentRepository\Export\ProcessorInterface; +use Neos\ContentRepository\Export\Processors; use Neos\ContentRepository\Export\Processors\AssetRepositoryImportProcessor; use Neos\ContentRepository\Export\Severity; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; @@ -68,8 +69,7 @@ public function importFromPath(ContentRepositoryId $contentRepositoryId, string $context = new ProcessingContext($filesystem, $onMessage); $contentRepository = $this->contentRepositoryRegistry->get($contentRepositoryId); - /** @var array $processors */ - $processors = [ + $processors = Processors::fromArray([ 'Run doctrine migrations' => new DoctrineMigrateProcessor($this->doctrineService), 'Setup content repository' => $this->contentRepositoryRegistry->buildService($contentRepositoryId, new ContentRepositorySetupProcessorFactory()), 'Create Live workspace' => new LiveWorkspaceCreationProcessor($contentRepository, $this->workspaceService), @@ -77,7 +77,7 @@ public function importFromPath(ContentRepositoryId $contentRepositoryId, string 'Import events' => $this->contentRepositoryRegistry->buildService($contentRepositoryId, new EventStoreImportProcessorFactory(WorkspaceName::forLive(), keepEventIds: true)), 'Import assets' => new AssetRepositoryImportProcessor($this->assetRepository, $this->resourceRepository, $this->resourceManager, $this->persistenceManager), 'Catchup all projections' => new ProjectionCatchupProcessor($this->contentRepositoryRegistry->buildService($contentRepositoryId, new ProjectionServiceFactory())), - ]; + ]); foreach ($processors as $processorLabel => $processor) { ($onProcessor)($processorLabel); From 7fea08b13b913d080b1ed9bf18f9f076fb0bd849 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 2 Nov 2024 20:01:59 +0100 Subject: [PATCH 069/214] TASK: Fail early if `resource://` paths are used in export see https://github.com/neos/neos-development-collection/issues/4912 --- Neos.Neos/Classes/Command/SiteCommandController.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Neos.Neos/Classes/Command/SiteCommandController.php b/Neos.Neos/Classes/Command/SiteCommandController.php index f0f93259a18..08199679ae6 100644 --- a/Neos.Neos/Classes/Command/SiteCommandController.php +++ b/Neos.Neos/Classes/Command/SiteCommandController.php @@ -346,6 +346,10 @@ protected function determineTargetPath(?string $packageKey, ?string $path): stri $package = $this->packageManager->getPackage($packageKey); $path = Files::concatenatePaths([$package->getPackagePath(), 'Resources/Private/Content']); } + if (str_starts_with($path, 'resource://')) { + $this->outputLine('Resource paths are not allowed, please use --package-key instead or a real path.'); + $this->quit(1); + } return $path; } From c98384d434013e14d8ba56dab32e15b82437668b Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 2 Nov 2024 20:50:56 +0100 Subject: [PATCH 070/214] TASK: Use `Processors::fromArray` --- Neos.Neos/Classes/Domain/Service/SitePruningService.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Neos.Neos/Classes/Domain/Service/SitePruningService.php b/Neos.Neos/Classes/Domain/Service/SitePruningService.php index 653f6a45300..16414ad76bc 100644 --- a/Neos.Neos/Classes/Domain/Service/SitePruningService.php +++ b/Neos.Neos/Classes/Domain/Service/SitePruningService.php @@ -21,6 +21,7 @@ use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepository\Export\ProcessingContext; use Neos\ContentRepository\Export\ProcessorInterface; +use Neos\ContentRepository\Export\Processors; use Neos\ContentRepository\Export\Severity; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\ContentRepositoryRegistry\Processors\ProjectionResetProcessor; @@ -56,8 +57,7 @@ public function pruneAll(ContentRepositoryId $contentRepositoryId, \Closure $onP $contentRepository = $this->contentRepositoryRegistry->get($contentRepositoryId); - /** @var array $processors */ - $processors = [ + $processors = Processors::fromArray([ 'Remove site nodes' => new SitePruningProcessor( $contentRepository, WorkspaceName::forLive(), @@ -77,7 +77,7 @@ public function pruneAll(ContentRepositoryId $contentRepositoryId, \Closure $onP $contentRepositoryId, new ProjectionServiceFactory() )) - ]; + ]); foreach ($processors as $processorLabel => $processor) { ($onProcessor)($processorLabel); From 90f2b8f649fc37cf6169dde45d8ea0ab9c10eb52 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 4 Nov 2024 17:11:11 +0100 Subject: [PATCH 071/214] BUGFIX: Ensure all events are published BEFORE catchup Otherwise, due to failures in projection or catchup-hooks the process would be immediately interrupted leaving a broken state. For example a faulty redirect handler hook - that just listens to live events - would be called during publishing. That means the remaining part to publish is already commited and we know we still have work to do to fork the new user content stream and apply the remaining. But the catchup hook would interrupt immediately when the events were catchup'd live. We would be left with a CLOSED user content stream that contains the "same" events that went live during the rebase. Reopening would not help at that point. This is why we must ensure that all events are published BEFORE we do the first catchup. Further implications: - running catchup only once should be more performant - we cannot refetch the current content stream version for what where previously "subcommans" (`forkContentStream`) but we must pass $expectedVersions around from the outside - we should not run constraint checks after the first `yield` as that would still operate on the old state. Thus all checks are combined above --- .../Classes/CommandHandler/CommandBus.php | 6 +- .../CommandHandlerInterface.php | 4 +- .../Classes/ContentRepository.php | 40 +++- .../Classes/EventStore/EventPersister.php | 14 +- .../Classes/Feature/ContentStreamHandling.php | 113 ++------- .../Feature/WorkspaceCommandHandler.php | 223 ++++++++++-------- .../Classes/Projection/CatchUp.php | 2 +- 7 files changed, 183 insertions(+), 219 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandBus.php b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandBus.php index 92673a00c82..1a69d3d017c 100644 --- a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandBus.php +++ b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandBus.php @@ -11,6 +11,7 @@ * Implementation Detail of {@see ContentRepository::handle}, which does the command dispatching to the different * {@see CommandHandlerInterface} implementation. * + * @phpstan-import-type YieldedEventsToPublish from CommandHandlerInterface * @internal */ final readonly class CommandBus @@ -29,7 +30,10 @@ public function __construct( } /** - * @return EventsToPublish|\Generator + * The handler only calculate which events they want to have published, + * but do not do the publishing themselves + * + * @return EventsToPublish|YieldedEventsToPublish */ public function handle(CommandInterface $command): EventsToPublish|\Generator { diff --git a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHandlerInterface.php b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHandlerInterface.php index b36d5d3ab75..1ff7cee24eb 100644 --- a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHandlerInterface.php +++ b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHandlerInterface.php @@ -5,12 +5,14 @@ namespace Neos\ContentRepository\Core\CommandHandler; use Neos\ContentRepository\Core\EventStore\EventsToPublish; +use Neos\EventStore\Model\EventStore\CommitResult; /** * Common interface for all Content Repository command handlers * * The {@see CommandHandlingDependencies} are available during handling to do soft-constraint checks * + * @phpstan-type YieldedEventsToPublish \Generator * @internal no public API, because commands are no extension points of the CR */ interface CommandHandlerInterface @@ -23,7 +25,7 @@ public function canHandle(CommandInterface $command): bool; * For the case of the workspace command handler that need to publish to many streams and "close" the content-stream directly, * it's allowed to yield the events to interact with the control flow of event publishing. * - * @return EventsToPublish|\Generator + * @return EventsToPublish|YieldedEventsToPublish */ public function handle(CommandInterface $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish|\Generator; } diff --git a/Neos.ContentRepository.Core/Classes/ContentRepository.php b/Neos.ContentRepository.Core/Classes/ContentRepository.php index f474dda8191..592870b3412 100644 --- a/Neos.ContentRepository.Core/Classes/ContentRepository.php +++ b/Neos.ContentRepository.Core/Classes/ContentRepository.php @@ -94,19 +94,35 @@ public function __construct( */ public function handle(CommandInterface $command): void { - // the commands only calculate which events they want to have published, but do not do the - // publishing themselves - $eventsToPublishOrGenerator = $this->commandBus->handle($command); - - if ($eventsToPublishOrGenerator instanceof EventsToPublish) { - $eventsToPublish = $this->enrichEventsToPublishWithMetadata($eventsToPublishOrGenerator); - $this->eventPersister->publishEvents($this, $eventsToPublish); - } else { - foreach ($eventsToPublishOrGenerator as $eventsToPublish) { - assert($eventsToPublish instanceof EventsToPublish); // just for the ide - $eventsToPublish = $this->enrichEventsToPublishWithMetadata($eventsToPublish); - $this->eventPersister->publishEvents($this, $eventsToPublish); + $toPublish = $this->commandBus->handle($command); + + if ($toPublish instanceof EventsToPublish) { + // simple case + $eventsToPublish = $this->enrichEventsToPublishWithMetadata($toPublish); + if ($eventsToPublish->events->isEmpty()) { + return; + } + $this->eventPersister->publishWithoutCatchup($eventsToPublish); + $this->catchupProjections(); + return; + } + + // control-flow aware command handling via generator + try { + $yieldedEventsToPublish = $toPublish->current(); + while ($yieldedEventsToPublish !== null) { + if ($yieldedEventsToPublish->events->isEmpty()) { + $yieldedEventsToPublish = $toPublish->send(null); + continue; + } + $eventsToPublish = $this->enrichEventsToPublishWithMetadata($yieldedEventsToPublish); + $commitResult = $this->eventPersister->publishWithoutCatchup($eventsToPublish); + $yieldedEventsToPublish = $toPublish->send($commitResult); } + } finally { + // We always NEED to catchup even if there was an unexpected ConcurrencyException to make sure previous commits are handled. + // Technically it would be acceptable for the catchup to fail here (due to hook errors) because all the events are already persisted. + $this->catchupProjections(); } } diff --git a/Neos.ContentRepository.Core/Classes/EventStore/EventPersister.php b/Neos.ContentRepository.Core/Classes/EventStore/EventPersister.php index 4909d50e661..59b102e03bd 100644 --- a/Neos.ContentRepository.Core/Classes/EventStore/EventPersister.php +++ b/Neos.ContentRepository.Core/Classes/EventStore/EventPersister.php @@ -8,6 +8,7 @@ use Neos\EventStore\EventStoreInterface; use Neos\EventStore\Exception\ConcurrencyException; use Neos\EventStore\Model\Events; +use Neos\EventStore\Model\EventStore\CommitResult; /** * Internal service to persist {@see EventInterface} with the proper normalization, and triggering the @@ -31,15 +32,22 @@ public function publishEvents(ContentRepository $contentRepository, EventsToPubl if ($eventsToPublish->events->isEmpty()) { return; } + $this->publishWithoutCatchup($eventsToPublish); + $contentRepository->catchUpProjections(); + } + + /** + * @throws ConcurrencyException in case the expectedVersion does not match + */ + public function publishWithoutCatchup(EventsToPublish $eventsToPublish): CommitResult + { $normalizedEvents = Events::fromArray( $eventsToPublish->events->map($this->eventNormalizer->normalize(...)) ); - $this->eventStore->commit( + return $this->eventStore->commit( $eventsToPublish->streamName, $normalizedEvents, $eventsToPublish->expectedVersion ); - - $contentRepository->catchUpProjections(); } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/ContentStreamHandling.php b/Neos.ContentRepository.Core/Classes/Feature/ContentStreamHandling.php index c3027d71147..d71c28f4097 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/ContentStreamHandling.php +++ b/Neos.ContentRepository.Core/Classes/Feature/ContentStreamHandling.php @@ -9,55 +9,25 @@ use Neos\ContentRepository\Core\EventStore\EventsToPublish; use Neos\ContentRepository\Core\Feature\ContentStreamClosing\Event\ContentStreamWasClosed; use Neos\ContentRepository\Core\Feature\ContentStreamClosing\Event\ContentStreamWasReopened; -use Neos\ContentRepository\Core\Feature\ContentStreamCreation\Event\ContentStreamWasCreated; use Neos\ContentRepository\Core\Feature\ContentStreamForking\Event\ContentStreamWasForked; use Neos\ContentRepository\Core\Feature\ContentStreamRemoval\Event\ContentStreamWasRemoved; use Neos\ContentRepository\Core\SharedModel\Exception\ContentStreamAlreadyExists; use Neos\ContentRepository\Core\SharedModel\Exception\ContentStreamDoesNotExistYet; use Neos\ContentRepository\Core\SharedModel\Exception\ContentStreamIsClosed; -use Neos\ContentRepository\Core\SharedModel\Exception\ContentStreamIsNotClosed; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; +use Neos\EventStore\Model\Event\Version; use Neos\EventStore\Model\EventStream\ExpectedVersion; trait ContentStreamHandling { - /** - * @param ContentStreamId $contentStreamId The id of the content stream to create - * @throws ContentStreamAlreadyExists - * @phpstan-pure this method is pure, to persist the events they must be handled outside - */ - private function createContentStream( - ContentStreamId $contentStreamId, - CommandHandlingDependencies $commandHandlingDependencies, - ): EventsToPublish { - $this->requireContentStreamToNotExistYet($contentStreamId, $commandHandlingDependencies); - $streamName = ContentStreamEventStreamName::fromContentStreamId($contentStreamId) - ->getEventStreamName(); - - return new EventsToPublish( - $streamName, - Events::with( - new ContentStreamWasCreated( - $contentStreamId, - ) - ), - ExpectedVersion::NO_STREAM() - ); - } - /** * @param ContentStreamId $contentStreamId The id of the content stream to close - * @param CommandHandlingDependencies $commandHandlingDependencies - * @return EventsToPublish * @phpstan-pure this method is pure, to persist the events they must be handled outside */ private function closeContentStream( ContentStreamId $contentStreamId, - CommandHandlingDependencies $commandHandlingDependencies, + Version $contentStreamVersion, ): EventsToPublish { - $this->requireContentStreamToExist($contentStreamId, $commandHandlingDependencies); - $expectedVersion = $this->getExpectedVersionOfContentStream($contentStreamId, $commandHandlingDependencies); - $this->requireContentStreamToNotBeClosed($contentStreamId, $commandHandlingDependencies); $streamName = ContentStreamEventStreamName::fromContentStreamId($contentStreamId)->getEventStreamName(); return new EventsToPublish( @@ -67,7 +37,7 @@ private function closeContentStream( $contentStreamId, ), ), - $expectedVersion + ExpectedVersion::fromVersion($contentStreamVersion) ); } @@ -75,21 +45,18 @@ private function closeContentStream( * @param ContentStreamId $contentStreamId The id of the content stream to reopen * @phpstan-pure this method is pure, to persist the events they must be handled outside */ - private function reopenContentStream( + private function reopenContentStreamWithoutConstraints( ContentStreamId $contentStreamId, - CommandHandlingDependencies $commandHandlingDependencies, ): EventsToPublish { - $this->requireContentStreamToExist($contentStreamId, $commandHandlingDependencies); - $this->requireContentStreamToBeClosed($contentStreamId, $commandHandlingDependencies); - $streamName = ContentStreamEventStreamName::fromContentStreamId($contentStreamId)->getEventStreamName(); - return new EventsToPublish( - $streamName, + ContentStreamEventStreamName::fromContentStreamId($contentStreamId)->getEventStreamName(), Events::with( new ContentStreamWasReopened( $contentStreamId ), ), + // We operate here without constraints on purpose to ensure this can be commited. + //Constraints have been checked beforehand and its expected that the content stream is closed. ExpectedVersion::ANY() ); } @@ -104,19 +71,10 @@ private function reopenContentStream( private function forkContentStream( ContentStreamId $newContentStreamId, ContentStreamId $sourceContentStreamId, - CommandHandlingDependencies $commandHandlingDependencies + Version $sourceContentStreamVersion ): EventsToPublish { - $this->requireContentStreamToExist($sourceContentStreamId, $commandHandlingDependencies); - $this->requireContentStreamToNotBeClosed($sourceContentStreamId, $commandHandlingDependencies); - $this->requireContentStreamToNotExistYet($newContentStreamId, $commandHandlingDependencies); - - $sourceContentStreamVersion = $commandHandlingDependencies->getContentStreamVersion($sourceContentStreamId); - - $streamName = ContentStreamEventStreamName::fromContentStreamId($newContentStreamId) - ->getEventStreamName(); - return new EventsToPublish( - $streamName, + ContentStreamEventStreamName::fromContentStreamId($newContentStreamId)->getEventStreamName(), Events::with( new ContentStreamWasForked( $newContentStreamId, @@ -133,25 +91,19 @@ private function forkContentStream( * @param ContentStreamId $contentStreamId The id of the content stream to remove * @phpstan-pure this method is pure, to persist the events they must be handled outside */ - private function removeContentStream( + private function removeContentStreamWithoutConstraints( ContentStreamId $contentStreamId, - CommandHandlingDependencies $commandHandlingDependencies ): EventsToPublish { - $this->requireContentStreamToExist($contentStreamId, $commandHandlingDependencies); - $expectedVersion = $this->getExpectedVersionOfContentStream($contentStreamId, $commandHandlingDependencies); - - $streamName = ContentStreamEventStreamName::fromContentStreamId( - $contentStreamId - )->getEventStreamName(); - return new EventsToPublish( - $streamName, + ContentStreamEventStreamName::fromContentStreamId($contentStreamId)->getEventStreamName(), Events::with( new ContentStreamWasRemoved( $contentStreamId, ), ), - $expectedVersion + // We operate here without constraints on purpose to ensure this can be commited. + // Constraints have been checked beforehand and its expected that the content stream is closed. + ExpectedVersion::ANY() ); } @@ -172,23 +124,6 @@ private function requireContentStreamToNotExistYet( } } - /** - * @param ContentStreamId $contentStreamId - * @param CommandHandlingDependencies $commandHandlingDependencies - * @throws ContentStreamDoesNotExistYet - */ - private function requireContentStreamToExist( - ContentStreamId $contentStreamId, - CommandHandlingDependencies $commandHandlingDependencies - ): void { - if (!$commandHandlingDependencies->contentStreamExists($contentStreamId)) { - throw new ContentStreamDoesNotExistYet( - 'Content stream "' . $contentStreamId->value . '" does not exist yet.', - 1521386692 - ); - } - } - private function requireContentStreamToNotBeClosed( ContentStreamId $contentStreamId, CommandHandlingDependencies $commandHandlingDependencies @@ -200,24 +135,4 @@ private function requireContentStreamToNotBeClosed( ); } } - - private function requireContentStreamToBeClosed( - ContentStreamId $contentStreamId, - CommandHandlingDependencies $commandHandlingDependencies - ): void { - if (!$commandHandlingDependencies->isContentStreamClosed($contentStreamId)) { - throw new ContentStreamIsNotClosed( - 'Content stream "' . $contentStreamId->value . '" is not closed.', - 1710405911 - ); - } - } - - private function getExpectedVersionOfContentStream( - ContentStreamId $contentStreamId, - CommandHandlingDependencies $commandHandlingDependencies - ): ExpectedVersion { - $version = $commandHandlingDependencies->getContentStreamVersion($contentStreamId); - return ExpectedVersion::fromVersion($version); - } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php index 20143536272..0be16cd86cb 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php +++ b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php @@ -20,14 +20,14 @@ use Neos\ContentRepository\Core\CommandHandler\CommandSimulatorFactory; use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\EventStore\DecoratedEvent; -use Neos\ContentRepository\Core\EventStore\EventInterface; use Neos\ContentRepository\Core\EventStore\EventNormalizer; use Neos\ContentRepository\Core\EventStore\Events; use Neos\ContentRepository\Core\EventStore\EventsToPublish; use Neos\ContentRepository\Core\Feature\Common\PublishableToWorkspaceInterface; use Neos\ContentRepository\Core\Feature\ContentStreamClosing\Event\ContentStreamWasClosed; use Neos\ContentRepository\Core\Feature\ContentStreamClosing\Event\ContentStreamWasReopened; -use Neos\ContentRepository\Core\Feature\ContentStreamForking\Event\ContentStreamWasForked; +use Neos\ContentRepository\Core\Feature\ContentStreamCreation\Event\ContentStreamWasCreated; +use Neos\ContentRepository\Core\Feature\ContentStreamRemoval\Event\ContentStreamWasRemoved; use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Command\CreateRootWorkspace; use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Command\CreateWorkspace; use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Event\RootWorkspaceWasCreated; @@ -55,6 +55,7 @@ use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Exception\WorkspaceRebaseFailed; use Neos\ContentRepository\Core\SharedModel\Exception\ContentStreamAlreadyExists; use Neos\ContentRepository\Core\SharedModel\Exception\ContentStreamDoesNotExistYet; +use Neos\ContentRepository\Core\SharedModel\Exception\ContentStreamIsClosed; use Neos\ContentRepository\Core\SharedModel\Exception\WorkspaceDoesNotExist; use Neos\ContentRepository\Core\SharedModel\Exception\WorkspaceHasNoBaseWorkspaceName; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; @@ -62,13 +63,13 @@ use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceStatus; use Neos\EventStore\EventStoreInterface; -use Neos\EventStore\Model\Event\EventType; use Neos\EventStore\Model\Event\SequenceNumber; use Neos\EventStore\Model\Event\Version; use Neos\EventStore\Model\EventStream\EventStreamInterface; use Neos\EventStore\Model\EventStream\ExpectedVersion; /** + * @phpstan-import-type YieldedEventsToPublish from CommandHandlerInterface * @internal from userland, you'll use ContentRepository::handle to dispatch commands */ final readonly class WorkspaceCommandHandler implements CommandHandlerInterface @@ -87,6 +88,9 @@ public function canHandle(CommandInterface $command): bool return method_exists($this, 'handle' . (new \ReflectionClass($command))->getShortName()); } + /** + * @return YieldedEventsToPublish + */ public function handle(CommandInterface $command, CommandHandlingDependencies $commandHandlingDependencies): \Generator { /** @phpstan-ignore-next-line */ @@ -115,7 +119,6 @@ private function handleCreateWorkspace( ): \Generator { $this->requireWorkspaceToNotExist($command->workspaceName, $commandHandlingDependencies); $baseWorkspace = $commandHandlingDependencies->findWorkspaceByName($command->baseWorkspaceName); - if ($baseWorkspace === null) { throw new BaseWorkspaceDoesNotExist(sprintf( 'The workspace %s (base workspace of %s) does not exist', @@ -123,12 +126,15 @@ private function handleCreateWorkspace( $command->workspaceName->value ), 1513890708); } + $sourceContentStreamVersion = $commandHandlingDependencies->getContentStreamVersion($baseWorkspace->currentContentStreamId); + $this->requireContentStreamToNotBeClosed($baseWorkspace->currentContentStreamId, $commandHandlingDependencies); + $this->requireContentStreamToNotExistYet($command->newContentStreamId, $commandHandlingDependencies); // When the workspace is created, we first have to fork the content stream yield $this->forkContentStream( $command->newContentStreamId, $baseWorkspace->currentContentStreamId, - $commandHandlingDependencies + $sourceContentStreamVersion ); yield new EventsToPublish( @@ -154,11 +160,16 @@ private function handleCreateRootWorkspace( CommandHandlingDependencies $commandHandlingDependencies, ): \Generator { $this->requireWorkspaceToNotExist($command->workspaceName, $commandHandlingDependencies); + $this->requireContentStreamToNotExistYet($command->newContentStreamId, $commandHandlingDependencies); - $newContentStreamId = $command->newContentStreamId; - yield $this->createContentStream( - $newContentStreamId, - $commandHandlingDependencies + yield new EventsToPublish( + ContentStreamEventStreamName::fromContentStreamId($command->newContentStreamId)->getEventStreamName(), + Events::with( + new ContentStreamWasCreated( + $command->newContentStreamId, + ) + ), + ExpectedVersion::NO_STREAM() ); yield new EventsToPublish( @@ -166,7 +177,7 @@ private function handleCreateRootWorkspace( Events::with( new RootWorkspaceWasCreated( $command->workspaceName, - $newContentStreamId + $command->newContentStreamId ) ), ExpectedVersion::ANY() @@ -183,16 +194,12 @@ private function handlePublishWorkspace( // no-op return; } - - if (!$commandHandlingDependencies->contentStreamExists($workspace->currentContentStreamId)) { - throw new \RuntimeException('Cannot publish nodes on a workspace with a stateless content stream', 1729711258); - } - $this->requireContentStreamToNotBeClosed($baseWorkspace->currentContentStreamId, $commandHandlingDependencies); - $baseContentStreamVersion = $commandHandlingDependencies->getContentStreamVersion($baseWorkspace->currentContentStreamId); + $workspaceContentStreamVersion = $this->requireOpenContentStreamVersion($workspace, $commandHandlingDependencies); + $baseWorkspaceContentStreamVersion = $this->requireOpenContentStreamVersion($baseWorkspace, $commandHandlingDependencies); yield $this->closeContentStream( $workspace->currentContentStreamId, - $commandHandlingDependencies + $workspaceContentStreamVersion ); $rebaseableCommands = RebaseableCommands::extractFromEventStream( @@ -206,15 +213,14 @@ private function handlePublishWorkspace( yield from $this->publishWorkspace( $workspace, $baseWorkspace, + $baseWorkspaceContentStreamVersion, $command->newContentStreamId, - $baseContentStreamVersion, - $rebaseableCommands, - $commandHandlingDependencies + $rebaseableCommands ); - } catch (WorkspaceRebaseFailed $workspaceRebaseFailed) { - yield $this->reopenContentStream( - $workspace->currentContentStreamId, - $commandHandlingDependencies + } catch (WorkspaceRebaseFailed $workspaceRebaseFailed) { // and rethrow in yield + // todo catch all + yield $this->reopenContentStreamWithoutConstraints( + $workspace->currentContentStreamId ); throw $workspaceRebaseFailed; } @@ -223,10 +229,9 @@ private function handlePublishWorkspace( private function publishWorkspace( Workspace $workspace, Workspace $baseWorkspace, + Version $baseWorkspaceContentStreamVersion, ContentStreamId $newContentStreamId, - Version $baseContentStreamVersion, - RebaseableCommands $rebaseableCommands, - CommandHandlingDependencies $commandHandlingDependencies, + RebaseableCommands $rebaseableCommands ): \Generator { $commandSimulator = $this->commandSimulatorFactory->createSimulatorForWorkspace($baseWorkspace->workspaceName); @@ -242,7 +247,7 @@ static function ($handle) use ($rebaseableCommands): void { throw WorkspaceRebaseFailed::duringPublish($commandSimulator->getCommandsThatFailed()); } - yield new EventsToPublish( + $commitResult = yield new EventsToPublish( ContentStreamEventStreamName::fromContentStreamId($baseWorkspace->currentContentStreamId) ->getEventStreamName(), $this->getCopiedEventsOfEventStream( @@ -250,13 +255,14 @@ static function ($handle) use ($rebaseableCommands): void { $baseWorkspace->currentContentStreamId, $commandSimulator->eventStream(), ), - ExpectedVersion::fromVersion($baseContentStreamVersion) + // todo can fail; must reopen!!!!! + ExpectedVersion::fromVersion($baseWorkspaceContentStreamVersion) ); yield $this->forkContentStream( $newContentStreamId, $baseWorkspace->currentContentStreamId, - $commandHandlingDependencies + $commitResult->highestCommittedVersion ); yield new EventsToPublish( @@ -272,19 +278,19 @@ static function ($handle) use ($rebaseableCommands): void { ExpectedVersion::ANY() ); - yield $this->removeContentStream($workspace->currentContentStreamId, $commandHandlingDependencies); + yield $this->removeContentStreamWithoutConstraints($workspace->currentContentStreamId); } private function rebaseWorkspaceWithoutChanges( Workspace $workspace, Workspace $baseWorkspace, - ContentStreamId $newContentStreamId, - CommandHandlingDependencies $commandHandlingDependencies, + Version $baseWorkspaceContentStreamVersion, + ContentStreamId $newContentStreamId ): \Generator { yield $this->forkContentStream( $newContentStreamId, $baseWorkspace->currentContentStreamId, - $commandHandlingDependencies + $baseWorkspaceContentStreamVersion ); yield new EventsToPublish( @@ -299,11 +305,11 @@ private function rebaseWorkspaceWithoutChanges( ExpectedVersion::ANY() ); - yield $this->removeContentStream($workspace->currentContentStreamId, $commandHandlingDependencies); + yield $this->removeContentStreamWithoutConstraints($workspace->currentContentStreamId); } /** - * Copy all events from the passed event stream which implement the {@see PublishableToOtherContentStreamsInterface} + * Copy all events from the passed event stream which implement the {@see PublishableToWorkspaceInterface} */ private function getCopiedEventsOfEventStream( WorkspaceName $targetWorkspaceName, @@ -339,6 +345,8 @@ private function handleRebaseWorkspace( if (!$commandHandlingDependencies->contentStreamExists($workspace->currentContentStreamId)) { throw new \RuntimeException('Cannot rebase a workspace with a stateless content stream', 1711718314); } + $workspaceContentStreamVersion = $this->requireOpenContentStreamVersion($workspace, $commandHandlingDependencies); + $baseWorkspaceContentStreamVersion = $this->requireOpenContentStreamVersion($baseWorkspace, $commandHandlingDependencies); if ( $workspace->status === WorkspaceStatus::UP_TO_DATE @@ -350,7 +358,7 @@ private function handleRebaseWorkspace( yield $this->closeContentStream( $workspace->currentContentStreamId, - $commandHandlingDependencies + $workspaceContentStreamVersion ); if (!$workspace->hasPublishableChanges()) { @@ -358,8 +366,8 @@ private function handleRebaseWorkspace( yield from $this->rebaseWorkspaceWithoutChanges( $workspace, $baseWorkspace, - $command->rebasedContentStreamId, - $commandHandlingDependencies + $baseWorkspaceContentStreamVersion, + $command->rebasedContentStreamId ); return; } @@ -385,9 +393,8 @@ static function ($handle) use ($rebaseableCommands): void { $command->rebaseErrorHandlingStrategy === RebaseErrorHandlingStrategy::STRATEGY_FAIL && $commandSimulator->hasCommandsThatFailed() ) { - yield $this->reopenContentStream( - $workspace->currentContentStreamId, - $commandHandlingDependencies + yield $this->reopenContentStreamWithoutConstraints( + $workspace->currentContentStreamId ); // throw an exception that contains all the information about what exactly failed @@ -398,6 +405,7 @@ static function ($handle) use ($rebaseableCommands): void { yield from $this->forkNewContentStreamAndApplyEvents( $command->rebasedContentStreamId, $baseWorkspace->currentContentStreamId, + $baseWorkspaceContentStreamVersion, new EventsToPublish( WorkspaceEventStreamName::fromWorkspaceName($command->workspaceName)->getEventStreamName(), Events::with( @@ -413,21 +421,16 @@ static function ($handle) use ($rebaseableCommands): void { $command->workspaceName, $command->rebasedContentStreamId, $commandSimulator->eventStream(), - ), - $commandHandlingDependencies + ) ); - yield $this->removeContentStream($workspace->currentContentStreamId, $commandHandlingDependencies); + yield $this->removeContentStreamWithoutConstraints($workspace->currentContentStreamId); } /** * This method is like a combined Rebase and Publish! * - * @throws BaseWorkspaceDoesNotExist - * @throws ContentStreamAlreadyExists - * @throws ContentStreamDoesNotExistYet - * @throws WorkspaceDoesNotExist - * @throws \Exception + * @return YieldedEventsToPublish */ private function handlePublishIndividualNodesFromWorkspace( PublishIndividualNodesFromWorkspace $command, @@ -444,12 +447,12 @@ private function handlePublishIndividualNodesFromWorkspace( if (!$commandHandlingDependencies->contentStreamExists($workspace->currentContentStreamId)) { throw new \RuntimeException('Cannot publish nodes on a workspace with a stateless content stream', 1710410114); } - $this->requireContentStreamToNotBeClosed($baseWorkspace->currentContentStreamId, $commandHandlingDependencies); - $baseContentStreamVersion = $commandHandlingDependencies->getContentStreamVersion($baseWorkspace->currentContentStreamId); + $workspaceContentStreamVersion = $this->requireOpenContentStreamVersion($workspace, $commandHandlingDependencies); + $baseWorkspaceContentStreamVersion = $this->requireOpenContentStreamVersion($baseWorkspace, $commandHandlingDependencies); yield $this->closeContentStream( $workspace->currentContentStreamId, - $commandHandlingDependencies + $workspaceContentStreamVersion ); $rebaseableCommands = RebaseableCommands::extractFromEventStream( @@ -463,9 +466,8 @@ private function handlePublishIndividualNodesFromWorkspace( if ($matchingCommands->isEmpty()) { // almost a noop (e.g. random node ids were specified) ;) - yield $this->reopenContentStream( - $workspace->currentContentStreamId, - $commandHandlingDependencies + yield $this->reopenContentStreamWithoutConstraints( + $workspace->currentContentStreamId ); return; } @@ -476,16 +478,14 @@ private function handlePublishIndividualNodesFromWorkspace( yield from $this->publishWorkspace( $workspace, $baseWorkspace, + $baseWorkspaceContentStreamVersion, $command->contentStreamIdForRemainingPart, - $baseContentStreamVersion, - $matchingCommands, - $commandHandlingDependencies + $matchingCommands ); return; } catch (WorkspaceRebaseFailed $workspaceRebaseFailed) { - yield $this->reopenContentStream( - $workspace->currentContentStreamId, - $commandHandlingDependencies + yield $this->reopenContentStreamWithoutConstraints( + $workspace->currentContentStreamId ); throw $workspaceRebaseFailed; } @@ -507,16 +507,15 @@ static function ($handle) use ($commandSimulator, $matchingCommands, $remainingC ); if ($commandSimulator->hasCommandsThatFailed()) { - yield $this->reopenContentStream( - $workspace->currentContentStreamId, - $commandHandlingDependencies + yield $this->reopenContentStreamWithoutConstraints( + $workspace->currentContentStreamId ); throw WorkspaceRebaseFailed::duringPublish($commandSimulator->getCommandsThatFailed()); } // this could be a no-op for the rare case when a command returns empty events e.g. the node was already tagged with this subtree tag, meaning we actually just rebase - yield new EventsToPublish( + $commitResult = yield new EventsToPublish( ContentStreamEventStreamName::fromContentStreamId($baseWorkspace->currentContentStreamId) ->getEventStreamName(), $this->getCopiedEventsOfEventStream( @@ -524,12 +523,14 @@ static function ($handle) use ($commandSimulator, $matchingCommands, $remainingC $baseWorkspace->currentContentStreamId, $commandSimulator->eventStream()->withMaximumSequenceNumber($highestSequenceNumberForMatching), ), - ExpectedVersion::fromVersion($baseContentStreamVersion) + ExpectedVersion::fromVersion($baseWorkspaceContentStreamVersion) ); yield from $this->forkNewContentStreamAndApplyEvents( $command->contentStreamIdForRemainingPart, $baseWorkspace->currentContentStreamId, + // todo otherwise Features/W8-IndividualNodePublication/03-MoreBasicFeatures.feature:185 fails, see comment about emptiness above ... or should we manually count? + $commitResult?->highestCommittedVersion ?: $baseWorkspaceContentStreamVersion, new EventsToPublish( WorkspaceEventStreamName::fromWorkspaceName($command->workspaceName)->getEventStreamName(), Events::fromArray([ @@ -547,11 +548,10 @@ static function ($handle) use ($commandSimulator, $matchingCommands, $remainingC $command->workspaceName, $command->contentStreamIdForRemainingPart, $commandSimulator->eventStream()->withMinimumSequenceNumber($highestSequenceNumberForMatching->next()) - ), - $commandHandlingDependencies + ) ); - yield $this->removeContentStream($workspace->currentContentStreamId, $commandHandlingDependencies); + yield $this->removeContentStreamWithoutConstraints($workspace->currentContentStreamId); } /** @@ -576,13 +576,12 @@ private function handleDiscardIndividualNodesFromWorkspace( return; } - if (!$commandHandlingDependencies->contentStreamExists($workspace->currentContentStreamId)) { - throw new \RuntimeException('Cannot discard nodes on a workspace with a stateless content stream', 1710408112); - } + $workspaceContentStreamVersion = $this->requireOpenContentStreamVersion($workspace, $commandHandlingDependencies); + $baseWorkspaceContentStreamVersion = $this->requireOpenContentStreamVersion($baseWorkspace, $commandHandlingDependencies); yield $this->closeContentStream( $workspace->currentContentStreamId, - $commandHandlingDependencies + $workspaceContentStreamVersion ); // filter commands, only keeping the ones NOT MATCHING the nodes from the command (i.e. the modifications we want to keep) @@ -596,9 +595,8 @@ private function handleDiscardIndividualNodesFromWorkspace( if ($commandsToDiscard->isEmpty()) { // if we have nothing to discard, we can just keep all. (e.g. random node ids were specified) It's almost a noop ;) - yield $this->reopenContentStream( - $workspace->currentContentStreamId, - $commandHandlingDependencies + yield $this->reopenContentStreamWithoutConstraints( + $workspace->currentContentStreamId ); return; } @@ -607,9 +605,10 @@ private function handleDiscardIndividualNodesFromWorkspace( // quick path everything was discarded yield from $this->discardWorkspace( $workspace, + $workspaceContentStreamVersion, $baseWorkspace, - $command->newContentStreamId, - $commandHandlingDependencies + $baseWorkspaceContentStreamVersion, + $command->newContentStreamId ); return; } @@ -625,9 +624,8 @@ static function ($handle) use ($commandsToKeep): void { ); if ($commandSimulator->hasCommandsThatFailed()) { - yield $this->reopenContentStream( - $workspace->currentContentStreamId, - $commandHandlingDependencies + yield $this->reopenContentStreamWithoutConstraints( + $workspace->currentContentStreamId ); throw WorkspaceRebaseFailed::duringDiscard($commandSimulator->getCommandsThatFailed()); } @@ -635,6 +633,7 @@ static function ($handle) use ($commandsToKeep): void { yield from $this->forkNewContentStreamAndApplyEvents( $command->newContentStreamId, $baseWorkspace->currentContentStreamId, + $baseWorkspaceContentStreamVersion, new EventsToPublish( WorkspaceEventStreamName::fromWorkspaceName($command->workspaceName)->getEventStreamName(), Events::with( @@ -651,11 +650,10 @@ static function ($handle) use ($commandsToKeep): void { $command->workspaceName, $command->newContentStreamId, $commandSimulator->eventStream(), - ), - $commandHandlingDependencies + ) ); - yield $this->removeContentStream($workspace->currentContentStreamId, $commandHandlingDependencies); + yield $this->removeContentStreamWithoutConstraints($workspace->currentContentStreamId); } /** @@ -674,31 +672,32 @@ private function handleDiscardWorkspace( return; } + $workspaceContentStreamVersion = $this->requireOpenContentStreamVersion($workspace, $commandHandlingDependencies); + $baseWorkspaceContentStreamVersion = $this->requireOpenContentStreamVersion($baseWorkspace, $commandHandlingDependencies); + yield from $this->discardWorkspace( $workspace, + $workspaceContentStreamVersion, $baseWorkspace, - $command->newContentStreamId, - $commandHandlingDependencies + $baseWorkspaceContentStreamVersion, + $command->newContentStreamId ); } /** - * @param Workspace $workspace - * @param Workspace $baseWorkspace - * @param ContentStreamId $newContentStream - * @param CommandHandlingDependencies $commandHandlingDependencies * @phpstan-pure this method is pure, to persist the events they must be handled outside */ private function discardWorkspace( Workspace $workspace, + Version $workspaceContentStreamVersion, Workspace $baseWorkspace, - ContentStreamId $newContentStream, - CommandHandlingDependencies $commandHandlingDependencies + Version $baseWorkspaceContentStreamVersion, + ContentStreamId $newContentStream ): \Generator { yield $this->forkContentStream( $newContentStream, $baseWorkspace->currentContentStreamId, - $commandHandlingDependencies + $baseWorkspaceContentStreamVersion ); yield new EventsToPublish( @@ -713,7 +712,7 @@ private function discardWorkspace( ExpectedVersion::ANY() ); - yield $this->removeContentStream($workspace->currentContentStreamId, $commandHandlingDependencies); + yield $this->removeContentStreamWithoutConstraints($workspace->currentContentStreamId); } /** @@ -731,6 +730,8 @@ private function handleChangeBaseWorkspace( $workspace = $this->requireWorkspace($command->workspaceName, $commandHandlingDependencies); $currentBaseWorkspace = $this->requireBaseWorkspace($workspace, $commandHandlingDependencies); + $this->requireContentStreamToNotBeClosed($workspace->currentContentStreamId, $commandHandlingDependencies); + if ($currentBaseWorkspace->workspaceName->equals($command->baseWorkspaceName)) { // no-op return; @@ -738,13 +739,14 @@ private function handleChangeBaseWorkspace( $this->requireEmptyWorkspace($workspace); $newBaseWorkspace = $this->requireWorkspace($command->baseWorkspaceName, $commandHandlingDependencies); - $this->requireNonCircularRelationBetweenWorkspaces($workspace, $newBaseWorkspace, $commandHandlingDependencies); + $newBaseWorkspaceContentStreamVersion = $this->requireOpenContentStreamVersion($newBaseWorkspace, $commandHandlingDependencies); + yield $this->forkContentStream( $command->newContentStreamId, $newBaseWorkspace->currentContentStreamId, - $commandHandlingDependencies + $newBaseWorkspaceContentStreamVersion ); yield new EventsToPublish( @@ -768,10 +770,16 @@ private function handleDeleteWorkspace( CommandHandlingDependencies $commandHandlingDependencies, ): \Generator { $workspace = $this->requireWorkspace($command->workspaceName, $commandHandlingDependencies); + $contentStreamVersion = $commandHandlingDependencies->getContentStreamVersion($workspace->currentContentStreamId); - yield $this->removeContentStream( - $workspace->currentContentStreamId, - $commandHandlingDependencies + yield new EventsToPublish( + ContentStreamEventStreamName::fromContentStreamId($workspace->currentContentStreamId)->getEventStreamName(), + Events::with( + new ContentStreamWasRemoved( + $workspace->currentContentStreamId, + ), + ), + ExpectedVersion::fromVersion($contentStreamVersion) ); yield new EventsToPublish( @@ -788,14 +796,14 @@ private function handleDeleteWorkspace( private function forkNewContentStreamAndApplyEvents( ContentStreamId $newContentStreamId, ContentStreamId $sourceContentStreamId, + Version $sourceContentStreamVersion, EventsToPublish $pointWorkspaceToNewContentStream, Events $eventsToApplyOnNewContentStream, - CommandHandlingDependencies $commandHandlingDependencies, ): \Generator { yield $this->forkContentStream( $newContentStreamId, $sourceContentStreamId, - $commandHandlingDependencies + $sourceContentStreamVersion )->withAppendedEvents(Events::with( new ContentStreamWasClosed( $newContentStreamId @@ -830,6 +838,17 @@ private function requireWorkspaceToNotExist(WorkspaceName $workspaceName, Comman ), 1715341085); } + private function requireOpenContentStreamVersion(Workspace $workspace, CommandHandlingDependencies $commandHandlingDependencies): Version + { + if ($commandHandlingDependencies->isContentStreamClosed($workspace->currentContentStreamId)) { + throw new ContentStreamIsClosed( + 'Content stream "' . $workspace->currentContentStreamId . '" is closed.', + 1730730516 + ); + } + return $commandHandlingDependencies->getContentStreamVersion($workspace->currentContentStreamId); + } + /** * @throws WorkspaceDoesNotExist */ diff --git a/Neos.ContentRepository.Core/Classes/Projection/CatchUp.php b/Neos.ContentRepository.Core/Classes/Projection/CatchUp.php index b997978bfe7..35cd26467a9 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/CatchUp.php +++ b/Neos.ContentRepository.Core/Classes/Projection/CatchUp.php @@ -107,7 +107,7 @@ public function run(EventStreamInterface $eventStream): SequenceNumber try { ($this->eventHandler)($eventEnvelope); } catch (\Exception $e) { - throw new \RuntimeException(sprintf('Exception while catching up to sequence number %d', $eventEnvelope->sequenceNumber->value), 1710707311, $e); + throw new \RuntimeException(sprintf('Exception while catching up to sequence number %d: %s', $eventEnvelope->sequenceNumber->value, $e->getMessage()), 1710707311, $e); } $iteration++; if ($this->batchSize === 1 || $iteration % $this->batchSize === 0) { From 3c6ab69eee4599b1e985ee8c966adbcd237485da Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Tue, 5 Nov 2024 14:37:11 +0100 Subject: [PATCH 072/214] Remove `WorkspaceRoleSubject::__toString()` because it led to [debates](https://github.com/neos/neos-development-collection/pull/5298#discussion_r1818140741) --- Neos.Neos/Classes/Domain/Model/WorkspaceRoleSubject.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Neos.Neos/Classes/Domain/Model/WorkspaceRoleSubject.php b/Neos.Neos/Classes/Domain/Model/WorkspaceRoleSubject.php index c9255bcfe5f..c53388bb23c 100644 --- a/Neos.Neos/Classes/Domain/Model/WorkspaceRoleSubject.php +++ b/Neos.Neos/Classes/Domain/Model/WorkspaceRoleSubject.php @@ -50,9 +50,4 @@ public function equals(self $other): bool { return $this->type === $other->type && $this->value === $other->value; } - - public function __toString(): string - { - return "{$this->type->value}: {$this->value}"; - } } From 0571abf6fec480ef08ae9352cf1875c8dfe3f91e Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Tue, 5 Nov 2024 14:55:56 +0100 Subject: [PATCH 073/214] Split `ContentRepositoryAuthProvider::requireWorkspaceWritePermission()` into two methods --- .../ContentRepositoryAuthProvider.php | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php index d2ed098a3ed..147a6136f9a 100644 --- a/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php +++ b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php @@ -60,9 +60,6 @@ */ final readonly class ContentRepositoryAuthProvider implements AuthProviderInterface { - private const WORKSPACE_PERMISSION_WRITE = 'write'; - private const WORKSPACE_PERMISSION_MANAGE = 'manage'; - public function __construct( private ContentRepositoryId $contentRepositoryId, private UserService $userService, @@ -171,20 +168,29 @@ public function canExecuteCommand(CommandInterface $command): Privilege DiscardIndividualNodesFromWorkspace::class, PublishWorkspace::class, PublishIndividualNodesFromWorkspace::class, - RebaseWorkspace::class => $this->requireWorkspacePermission($command->workspaceName, self::WORKSPACE_PERMISSION_WRITE), - CreateWorkspace::class => $this->requireWorkspacePermission($command->baseWorkspaceName, self::WORKSPACE_PERMISSION_WRITE), - DeleteWorkspace::class => $this->requireWorkspacePermission($command->workspaceName, self::WORKSPACE_PERMISSION_MANAGE), + RebaseWorkspace::class => $this->requireWorkspaceWritePermission($command->workspaceName), + CreateWorkspace::class => $this->requireWorkspaceWritePermission($command->baseWorkspaceName), + DeleteWorkspace::class => $this->requireWorkspaceManagePermission($command->workspaceName), default => Privilege::granted('Command not restricted'), }; } - private function requireWorkspacePermission(WorkspaceName $workspaceName, string $permission): Privilege + private function requireWorkspaceWritePermission(WorkspaceName $workspaceName): Privilege + { + $workspacePermissions = $this->getWorkspacePermissionsForCurrentUser($workspaceName); + if (!$workspacePermissions->write) { + return Privilege::denied("Missing 'write' permissions for workspace '{$workspaceName->value}': {$workspacePermissions->getReason()}"); + } + return Privilege::granted("User has 'write' permissions for workspace '{$workspaceName->value}'"); + } + + private function requireWorkspaceManagePermission(WorkspaceName $workspaceName): Privilege { $workspacePermissions = $this->getWorkspacePermissionsForCurrentUser($workspaceName); - if (!$workspacePermissions->{$permission}) { - return Privilege::denied("Missing '{$permission}' permissions for workspace '{$workspaceName->value}': {$workspacePermissions->getReason()}"); + if (!$workspacePermissions->manage) { + return Privilege::denied("Missing 'manage' permissions for workspace '{$workspaceName->value}': {$workspacePermissions->getReason()}"); } - return Privilege::granted("User has '{$permission}' permissions for workspace '{$workspaceName->value}'"); + return Privilege::granted("User has 'manage' permissions for workspace '{$workspaceName->value}'"); } private function getWorkspacePermissionsForCurrentUser(WorkspaceName $workspaceName): WorkspacePermissions From b4e0c7829b61bc962986a898fcc10d929d0632a6 Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Tue, 5 Nov 2024 17:58:22 +0100 Subject: [PATCH 074/214] Tweak AuthProvider wiring --- .../TestSuite/Behavior/FakeAuthProviderFactory.php | 6 ++---- .../Classes/Factory/ContentRepositoryFactory.php | 7 ++++--- .../Classes/Factory/ProjectionFactoryDependencies.php | 2 -- .../Classes/ContentRepositoryRegistry.php | 6 +++--- .../AuthProvider/AuthProviderFactoryInterface.php | 4 ++-- .../AuthProvider/StaticAuthProviderFactory.php | 4 ++-- .../ContentRepositoryAuthProvider.php | 9 +++------ .../ContentRepositoryAuthProviderFactory.php | 11 ++++------- 8 files changed, 20 insertions(+), 29 deletions(-) diff --git a/Neos.ContentRepository.BehavioralTests/Classes/TestSuite/Behavior/FakeAuthProviderFactory.php b/Neos.ContentRepository.BehavioralTests/Classes/TestSuite/Behavior/FakeAuthProviderFactory.php index f018a5ac958..f239fb410d7 100644 --- a/Neos.ContentRepository.BehavioralTests/Classes/TestSuite/Behavior/FakeAuthProviderFactory.php +++ b/Neos.ContentRepository.BehavioralTests/Classes/TestSuite/Behavior/FakeAuthProviderFactory.php @@ -5,16 +5,14 @@ namespace Neos\ContentRepository\BehavioralTests\TestSuite\Behavior; use Neos\ContentRepository\Core\Feature\Security\AuthProviderInterface; +use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphReadModelInterface; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Helpers\FakeAuthProvider; use Neos\ContentRepositoryRegistry\Factory\AuthProvider\AuthProviderFactoryInterface; final class FakeAuthProviderFactory implements AuthProviderFactoryInterface { - /** - * @param array $options - */ - public function build(ContentRepositoryId $contentRepositoryId, array $options): AuthProviderInterface + public function build(ContentRepositoryId $contentRepositoryId, ContentGraphReadModelInterface $contentGraphReadModel): AuthProviderInterface { return new FakeAuthProvider(); } diff --git a/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php b/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php index a55d94fe826..8517f51c1c3 100644 --- a/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php +++ b/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php @@ -32,6 +32,7 @@ use Neos\ContentRepository\Core\NodeType\NodeTypeManager; use Neos\ContentRepository\Core\Projection\ProjectionsAndCatchUpHooks; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; +use Neos\ContentRepositoryRegistry\Factory\AuthProvider\AuthProviderFactoryInterface; use Neos\EventStore\EventStoreInterface; use Psr\Clock\ClockInterface; use Symfony\Component\Serializer\Serializer; @@ -53,7 +54,7 @@ public function __construct( ContentDimensionSourceInterface $contentDimensionSource, Serializer $propertySerializer, ProjectionsAndCatchUpHooksFactory $projectionsAndCatchUpHooksFactory, - private readonly AuthProviderInterface $authProvider, + private readonly AuthProviderFactoryInterface $authProviderFactory, private readonly ClockInterface $clock, ) { $contentDimensionZookeeper = new ContentDimensionZookeeper($contentDimensionSource); @@ -70,7 +71,6 @@ public function __construct( $contentDimensionZookeeper, $interDimensionalVariationGraph, new PropertyConverter($propertySerializer), - $this->authProvider, ); $this->projectionsAndCatchUpHooks = $projectionsAndCatchUpHooksFactory->build($this->projectionFactoryDependencies); } @@ -128,6 +128,7 @@ public function getOrBuild(): ContentRepository ) ); + $authProvider = $this->authProviderFactory->build($this->contentRepositoryId, $contentGraphReadModel); return $this->contentRepository = new ContentRepository( $this->contentRepositoryId, $publicCommandBus, @@ -138,7 +139,7 @@ public function getOrBuild(): ContentRepository $this->projectionFactoryDependencies->nodeTypeManager, $this->projectionFactoryDependencies->interDimensionalVariationGraph, $this->projectionFactoryDependencies->contentDimensionSource, - $this->authProvider, + $authProvider, $this->clock, $contentGraphReadModel ); diff --git a/Neos.ContentRepository.Core/Classes/Factory/ProjectionFactoryDependencies.php b/Neos.ContentRepository.Core/Classes/Factory/ProjectionFactoryDependencies.php index 0b78e572cc4..9bb2f0cc31f 100644 --- a/Neos.ContentRepository.Core/Classes/Factory/ProjectionFactoryDependencies.php +++ b/Neos.ContentRepository.Core/Classes/Factory/ProjectionFactoryDependencies.php @@ -18,7 +18,6 @@ use Neos\ContentRepository\Core\DimensionSpace\ContentDimensionZookeeper; use Neos\ContentRepository\Core\DimensionSpace\InterDimensionalVariationGraph; use Neos\ContentRepository\Core\EventStore\EventNormalizer; -use Neos\ContentRepository\Core\Feature\Security\AuthProviderInterface; use Neos\ContentRepository\Core\Infrastructure\Property\PropertyConverter; use Neos\ContentRepository\Core\NodeType\NodeTypeManager; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; @@ -38,7 +37,6 @@ public function __construct( public ContentDimensionZookeeper $contentDimensionZookeeper, public InterDimensionalVariationGraph $interDimensionalVariationGraph, public PropertyConverter $propertyConverter, - public AuthProviderInterface $authProvider, ) { } } diff --git a/Neos.ContentRepositoryRegistry/Classes/ContentRepositoryRegistry.php b/Neos.ContentRepositoryRegistry/Classes/ContentRepositoryRegistry.php index b5e952f4270..75b0b2ca151 100644 --- a/Neos.ContentRepositoryRegistry/Classes/ContentRepositoryRegistry.php +++ b/Neos.ContentRepositoryRegistry/Classes/ContentRepositoryRegistry.php @@ -175,7 +175,7 @@ private function buildFactory(ContentRepositoryId $contentRepositoryId): Content $this->buildContentDimensionSource($contentRepositoryId, $contentRepositorySettings), $this->buildPropertySerializer($contentRepositoryId, $contentRepositorySettings), $this->buildProjectionsFactory($contentRepositoryId, $contentRepositorySettings), - $this->buildAuthProvider($contentRepositoryId, $contentRepositorySettings), + $this->buildAuthProviderFactory($contentRepositoryId, $contentRepositorySettings), $clock ); } catch (\Exception $exception) { @@ -293,14 +293,14 @@ private function registerCatchupHookForProjection(mixed $projectionOptions, Proj } /** @param array $contentRepositorySettings */ - private function buildAuthProvider(ContentRepositoryId $contentRepositoryId, array $contentRepositorySettings): AuthProviderInterface + private function buildAuthProviderFactory(ContentRepositoryId $contentRepositoryId, array $contentRepositorySettings): AuthProviderFactoryInterface { isset($contentRepositorySettings['authProvider']['factoryObjectName']) || throw InvalidConfigurationException::fromMessage('Content repository "%s" does not have authProvider.factoryObjectName configured.', $contentRepositoryId->value); $authProviderFactory = $this->objectManager->get($contentRepositorySettings['authProvider']['factoryObjectName']); if (!$authProviderFactory instanceof AuthProviderFactoryInterface) { throw InvalidConfigurationException::fromMessage('authProvider.factoryObjectName for content repository "%s" is not an instance of %s but %s.', $contentRepositoryId->value, AuthProviderFactoryInterface::class, get_debug_type($authProviderFactory)); } - return $authProviderFactory->build($contentRepositoryId, $contentRepositorySettings['authProvider']['options'] ?? []); + return $authProviderFactory; } /** @param array $contentRepositorySettings */ diff --git a/Neos.ContentRepositoryRegistry/Classes/Factory/AuthProvider/AuthProviderFactoryInterface.php b/Neos.ContentRepositoryRegistry/Classes/Factory/AuthProvider/AuthProviderFactoryInterface.php index 809efdf65db..5b8593b7954 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Factory/AuthProvider/AuthProviderFactoryInterface.php +++ b/Neos.ContentRepositoryRegistry/Classes/Factory/AuthProvider/AuthProviderFactoryInterface.php @@ -5,6 +5,7 @@ namespace Neos\ContentRepositoryRegistry\Factory\AuthProvider; use Neos\ContentRepository\Core\Feature\Security\AuthProviderInterface; +use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphReadModelInterface; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; /** @@ -12,6 +13,5 @@ */ interface AuthProviderFactoryInterface { - /** @param array $options */ - public function build(ContentRepositoryId $contentRepositoryId, array $options): AuthProviderInterface; + public function build(ContentRepositoryId $contentRepositoryId, ContentGraphReadModelInterface $contentGraphReadModel): AuthProviderInterface; } diff --git a/Neos.ContentRepositoryRegistry/Classes/Factory/AuthProvider/StaticAuthProviderFactory.php b/Neos.ContentRepositoryRegistry/Classes/Factory/AuthProvider/StaticAuthProviderFactory.php index 870f2e5ace2..e8ff314c9a2 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Factory/AuthProvider/StaticAuthProviderFactory.php +++ b/Neos.ContentRepositoryRegistry/Classes/Factory/AuthProvider/StaticAuthProviderFactory.php @@ -5,6 +5,7 @@ use Neos\ContentRepository\Core\Feature\Security\AuthProviderInterface; use Neos\ContentRepository\Core\Feature\Security\Dto\UserId; use Neos\ContentRepository\Core\Feature\Security\StaticAuthProvider; +use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphReadModelInterface; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; /** @@ -12,8 +13,7 @@ */ final class StaticAuthProviderFactory implements AuthProviderFactoryInterface { - /** @param array $options */ - public function build(ContentRepositoryId $contentRepositoryId, array $options): AuthProviderInterface + public function build(ContentRepositoryId $contentRepositoryId, ContentGraphReadModelInterface $contentGraphReadModel): AuthProviderInterface { return new StaticAuthProvider(UserId::forSystemUser()); } diff --git a/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php index 147a6136f9a..c4bae9036a4 100644 --- a/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php +++ b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php @@ -5,8 +5,6 @@ namespace Neos\Neos\Security\ContentRepositoryAuthProvider; use Neos\ContentRepository\Core\CommandHandler\CommandInterface; -use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; -use Neos\ContentRepository\Core\Feature\Common\RebasableToOtherWorkspaceInterface; use Neos\ContentRepository\Core\Feature\DimensionSpaceAdjustment\Command\AddDimensionShineThrough; use Neos\ContentRepository\Core\Feature\DimensionSpaceAdjustment\Command\MoveDimensionSpacePoint; use Neos\ContentRepository\Core\Feature\NodeCreation\Command\CreateNodeAggregateWithNode; @@ -39,13 +37,12 @@ use Neos\ContentRepository\Core\Feature\WorkspacePublication\Command\PublishIndividualNodesFromWorkspace; use Neos\ContentRepository\Core\Feature\WorkspacePublication\Command\PublishWorkspace; use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Command\RebaseWorkspace; +use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphReadModelInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress; -use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; -use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Security\Context as SecurityContext; use Neos\Neos\Domain\Model\NodePermissions; use Neos\Neos\Domain\Model\WorkspacePermissions; @@ -63,7 +60,7 @@ public function __construct( private ContentRepositoryId $contentRepositoryId, private UserService $userService, - private ContentRepositoryRegistry $contentRepositoryRegistry, + private ContentGraphReadModelInterface $contentGraphReadModel, private ContentRepositoryAuthorizationService $authorizationService, private SecurityContext $securityContext, ) { @@ -130,7 +127,7 @@ public function canExecuteCommand(CommandInterface $command): Privilege if (!$workspacePermissions->write) { return Privilege::denied(sprintf('No write permissions on workspace "%s": %s', $nodeThatRequiresEditPrivilege->workspaceName->value, $workspacePermissions->getReason())); } - $node = $this->contentRepositoryRegistry->get($this->contentRepositoryId) + $node = $this->contentGraphReadModel ->getContentGraph($nodeThatRequiresEditPrivilege->workspaceName) ->getSubgraph($nodeThatRequiresEditPrivilege->dimensionSpacePoint, VisibilityConstraints::withoutRestrictions()) ->findNodeById($nodeThatRequiresEditPrivilege->aggregateId); diff --git a/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProviderFactory.php b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProviderFactory.php index 54db08a2c36..cf3d2fb9ac5 100644 --- a/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProviderFactory.php +++ b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProviderFactory.php @@ -4,8 +4,9 @@ namespace Neos\Neos\Security\ContentRepositoryAuthProvider; +use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphProjectionInterface; +use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphReadModelInterface; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; -use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\ContentRepositoryRegistry\Factory\AuthProvider\AuthProviderFactoryInterface; use Neos\Flow\Annotations as Flow; use Neos\Flow\Security\Context as SecurityContext; @@ -22,17 +23,13 @@ { public function __construct( private UserService $userService, - private ContentRepositoryRegistry $contentRepositoryRegistry, private ContentRepositoryAuthorizationService $contentRepositoryAuthorizationService, private SecurityContext $securityContext, ) { } - /** - * @param array $options - */ - public function build(ContentRepositoryId $contentRepositoryId, array $options): ContentRepositoryAuthProvider + public function build(ContentRepositoryId $contentRepositoryId, ContentGraphReadModelInterface $contentGraphReadModel): ContentRepositoryAuthProvider { - return new ContentRepositoryAuthProvider($contentRepositoryId, $this->userService, $this->contentRepositoryRegistry, $this->contentRepositoryAuthorizationService, $this->securityContext); + return new ContentRepositoryAuthProvider($contentRepositoryId, $this->userService, $contentGraphReadModel, $this->contentRepositoryAuthorizationService, $this->securityContext); } } From 61848a2202cc2a83606230f288c1dab12718c700 Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Wed, 6 Nov 2024 11:19:42 +0100 Subject: [PATCH 075/214] Make `Privilege::reason` private to be in sync with `WorkspacePermissions` --- Neos.ContentRepository.Core/Classes/ContentRepository.php | 4 ++-- .../Classes/Feature/Security/Dto/Privilege.php | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/ContentRepository.php b/Neos.ContentRepository.Core/Classes/ContentRepository.php index 90005f03169..3d584e056b7 100644 --- a/Neos.ContentRepository.Core/Classes/ContentRepository.php +++ b/Neos.ContentRepository.Core/Classes/ContentRepository.php @@ -101,7 +101,7 @@ public function handle(CommandInterface $command): void { $privilege = $this->authProvider->canExecuteCommand($command); if (!$privilege->granted) { - throw AccessDenied::becauseCommandIsNotGranted($command, $privilege->reason); + throw AccessDenied::becauseCommandIsNotGranted($command, $privilege->getReason()); } // the commands only calculate which events they want to have published, but do not do the // publishing themselves @@ -250,7 +250,7 @@ public function getContentGraph(WorkspaceName $workspaceName): ContentGraphInter { $privilege = $this->authProvider->canReadNodesFromWorkspace($workspaceName); if (!$privilege->granted) { - throw AccessDenied::becauseWorkspaceCantBeRead($workspaceName, $privilege->reason); + throw AccessDenied::becauseWorkspaceCantBeRead($workspaceName, $privilege->getReason()); } return $this->contentGraphReadModel->getContentGraph($workspaceName); } diff --git a/Neos.ContentRepository.Core/Classes/Feature/Security/Dto/Privilege.php b/Neos.ContentRepository.Core/Classes/Feature/Security/Dto/Privilege.php index 0fcef1552fe..6712ab9279f 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/Security/Dto/Privilege.php +++ b/Neos.ContentRepository.Core/Classes/Feature/Security/Dto/Privilege.php @@ -22,7 +22,7 @@ { private function __construct( public bool $granted, - public string $reason, + private string $reason, ) { } @@ -35,4 +35,9 @@ public static function denied(string $reason): self { return new self(false, $reason); } + + public function getReason(): string + { + return $this->reason; + } } From 643ce497df50c1f37ab711c35f81442639579c80 Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Wed, 6 Nov 2024 11:21:46 +0100 Subject: [PATCH 076/214] Remove `SubtreeTagPrivilegeSubject::__toString()` --- .../Privilege/SubtreeTagPrivilegeSubject.php | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/Neos.Neos/Classes/Security/Authorization/Privilege/SubtreeTagPrivilegeSubject.php b/Neos.Neos/Classes/Security/Authorization/Privilege/SubtreeTagPrivilegeSubject.php index fb78ee75883..2a4c9eeb247 100644 --- a/Neos.Neos/Classes/Security/Authorization/Privilege/SubtreeTagPrivilegeSubject.php +++ b/Neos.Neos/Classes/Security/Authorization/Privilege/SubtreeTagPrivilegeSubject.php @@ -14,7 +14,6 @@ namespace Neos\Neos\Security\Authorization\Privilege; -use Neos\ContentRepository\Core\Feature\SubtreeTagging\Dto\SubtreeTag; use Neos\ContentRepository\Core\Feature\SubtreeTagging\Dto\SubtreeTags; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\Flow\Security\Authorization\Privilege\PrivilegeSubjectInterface; @@ -29,13 +28,4 @@ public function __construct( public ContentRepositoryId|null $contentRepositoryId = null, ) { } - - public function __toString(): string - { - $label = 'tag' . ($this->subTreeTags->count() > 1 ? 's' : '') . ' "' . implode('", "', $this->subTreeTags->toStringArray()) . '"'; - if ($this->contentRepositoryId !== null) { - $label .= ' in Content Repository "' . $this->contentRepositoryId->value . '"'; - } - return $label; - } } From da297ece279536aa9911711d55ffdfce2034c400 Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Wed, 6 Nov 2024 11:25:48 +0100 Subject: [PATCH 077/214] Replace PHP assert by phpstan assert in `AbstractSubtreeTagBasedPrivilege` --- .../Privilege/AbstractSubtreeTagBasedPrivilege.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Neos.Neos/Classes/Security/Authorization/Privilege/AbstractSubtreeTagBasedPrivilege.php b/Neos.Neos/Classes/Security/Authorization/Privilege/AbstractSubtreeTagBasedPrivilege.php index 8aaea9f1d35..ee5e41f0f66 100644 --- a/Neos.Neos/Classes/Security/Authorization/Privilege/AbstractSubtreeTagBasedPrivilege.php +++ b/Neos.Neos/Classes/Security/Authorization/Privilege/AbstractSubtreeTagBasedPrivilege.php @@ -30,6 +30,7 @@ abstract class AbstractSubtreeTagBasedPrivilege extends AbstractPrivilege private SubtreeTags|null $subtreeTagsRuntimeCache = null; private ContentRepositoryId|null $contentRepositoryIdRuntimeCache = null; + /** @phpstan-assert !null $this->subtreeTagsRuntimeCache */ private function initialize(): void { if ($this->initialized) { @@ -66,7 +67,6 @@ public function matchesSubject(PrivilegeSubjectInterface $subject): bool public function getSubtreeTags(): SubtreeTags { $this->initialize(); - assert($this->subtreeTagsRuntimeCache !== null); return $this->subtreeTagsRuntimeCache; } From 187b80ea94840e5e75cc5162c22f6cd50738ec20 Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Wed, 6 Nov 2024 11:31:39 +0100 Subject: [PATCH 078/214] Add inline docs --- .../Privilege/AbstractSubtreeTagBasedPrivilege.php | 4 ++-- .../Authorization/Privilege/EditNodePrivilege.php | 9 ++------- .../Authorization/Privilege/ReadNodePrivilege.php | 9 ++------- 3 files changed, 6 insertions(+), 16 deletions(-) diff --git a/Neos.Neos/Classes/Security/Authorization/Privilege/AbstractSubtreeTagBasedPrivilege.php b/Neos.Neos/Classes/Security/Authorization/Privilege/AbstractSubtreeTagBasedPrivilege.php index ee5e41f0f66..e42051d4bc9 100644 --- a/Neos.Neos/Classes/Security/Authorization/Privilege/AbstractSubtreeTagBasedPrivilege.php +++ b/Neos.Neos/Classes/Security/Authorization/Privilege/AbstractSubtreeTagBasedPrivilege.php @@ -14,7 +14,6 @@ namespace Neos\Neos\Security\Authorization\Privilege; -use Neos\ContentRepository\Core\Feature\SubtreeTagging\Dto\SubtreeTag; use Neos\ContentRepository\Core\Feature\SubtreeTagging\Dto\SubtreeTags; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\Flow\Security\Authorization\Privilege\AbstractPrivilege; @@ -22,7 +21,8 @@ use Neos\Flow\Security\Exception\InvalidPrivilegeTypeException; /** - * TODO docs + * Common base class for privileges that evaluate {@see SubtreeTagPrivilegeSubject}s + * @see ReadNodePrivilege, EditNodePrivilege */ abstract class AbstractSubtreeTagBasedPrivilege extends AbstractPrivilege { diff --git a/Neos.Neos/Classes/Security/Authorization/Privilege/EditNodePrivilege.php b/Neos.Neos/Classes/Security/Authorization/Privilege/EditNodePrivilege.php index 52d94c0516b..83c9f53b88a 100644 --- a/Neos.Neos/Classes/Security/Authorization/Privilege/EditNodePrivilege.php +++ b/Neos.Neos/Classes/Security/Authorization/Privilege/EditNodePrivilege.php @@ -14,14 +14,9 @@ namespace Neos\Neos\Security\Authorization\Privilege; -use Neos\ContentRepository\Core\Feature\SubtreeTagging\Dto\SubtreeTag; -use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; -use Neos\Flow\Security\Authorization\Privilege\AbstractPrivilege; -use Neos\Flow\Security\Authorization\Privilege\PrivilegeSubjectInterface; -use Neos\Flow\Security\Exception\InvalidPrivilegeTypeException; - /** - * TODO docs + * The privilege to edit any matching node in the Content Repository. + * This includes creation, setting properties or references, disabling/enabling, tagging and moving corresponding nodes */ class EditNodePrivilege extends AbstractSubtreeTagBasedPrivilege { diff --git a/Neos.Neos/Classes/Security/Authorization/Privilege/ReadNodePrivilege.php b/Neos.Neos/Classes/Security/Authorization/Privilege/ReadNodePrivilege.php index f96f1a0ddaa..dfbe9f138f3 100644 --- a/Neos.Neos/Classes/Security/Authorization/Privilege/ReadNodePrivilege.php +++ b/Neos.Neos/Classes/Security/Authorization/Privilege/ReadNodePrivilege.php @@ -14,14 +14,9 @@ namespace Neos\Neos\Security\Authorization\Privilege; -use Neos\ContentRepository\Core\Feature\SubtreeTagging\Dto\SubtreeTag; -use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; -use Neos\Flow\Security\Authorization\Privilege\AbstractPrivilege; -use Neos\Flow\Security\Authorization\Privilege\PrivilegeSubjectInterface; -use Neos\Flow\Security\Exception\InvalidPrivilegeTypeException; - /** - * TODO docs + * The privilege to read any matching node from the Content Repository. + * This includes all properties, references and metadata */ class ReadNodePrivilege extends AbstractSubtreeTagBasedPrivilege { From 41e6b57ba8917ef394e97e4ab511db37453032d6 Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Wed, 6 Nov 2024 11:37:18 +0100 Subject: [PATCH 079/214] Extract `ContentRepositoryAuthProvider::nodeThatRequiresEditPrivilegeForCommand()` --- .../ContentRepositoryAuthProvider.php | 45 +++++++++++-------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php index c4bae9036a4..95ff0a58ece 100644 --- a/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php +++ b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php @@ -48,6 +48,7 @@ use Neos\Neos\Domain\Model\WorkspacePermissions; use Neos\Neos\Domain\Service\UserService; use Neos\Neos\Security\Authorization\ContentRepositoryAuthorizationService; +use Neos\Neos\Security\Authorization\Privilege\EditNodePrivilege; /** * Implementation of Content Repository {@see AuthProviderInterface} which ties the authorization @@ -103,25 +104,7 @@ public function canExecuteCommand(CommandInterface $command): Privilege if ($this->securityContext->areAuthorizationChecksDisabled()) { return Privilege::granted('Authorization checks are disabled'); } - - /** @var NodeAddress|null $nodeThatRequiresEditPrivilege */ - $nodeThatRequiresEditPrivilege = match ($command::class) { - CopyNodesRecursively::class => NodeAddress::create($this->contentRepositoryId, $command->workspaceName, $command->targetDimensionSpacePoint->toDimensionSpacePoint(), $command->targetParentNodeAggregateId), - CreateNodeAggregateWithNode::class, - CreateNodeAggregateWithNodeAndSerializedProperties::class => NodeAddress::create($this->contentRepositoryId, $command->workspaceName, $command->originDimensionSpacePoint->toDimensionSpacePoint(), $command->parentNodeAggregateId), - CreateNodeVariant::class => NodeAddress::create($this->contentRepositoryId, $command->workspaceName, $command->sourceOrigin->toDimensionSpacePoint(), $command->nodeAggregateId), - DisableNodeAggregate::class, - EnableNodeAggregate::class, - RemoveNodeAggregate::class, - TagSubtree::class, - UntagSubtree::class => NodeAddress::create($this->contentRepositoryId, $command->workspaceName, $command->coveredDimensionSpacePoint, $command->nodeAggregateId), - MoveNodeAggregate::class => NodeAddress::create($this->contentRepositoryId, $command->workspaceName, $command->dimensionSpacePoint, $command->nodeAggregateId), - SetNodeProperties::class, - SetSerializedNodeProperties::class => NodeAddress::create($this->contentRepositoryId, $command->workspaceName, $command->originDimensionSpacePoint->toDimensionSpacePoint(), $command->nodeAggregateId), - SetNodeReferences::class, - SetSerializedNodeReferences::class => NodeAddress::create($this->contentRepositoryId, $command->workspaceName, $command->sourceOriginDimensionSpacePoint->toDimensionSpacePoint(), $command->sourceNodeAggregateId), - default => null, - }; + $nodeThatRequiresEditPrivilege = $this->nodeThatRequiresEditPrivilegeForCommand($command); if ($nodeThatRequiresEditPrivilege !== null) { $workspacePermissions = $this->getWorkspacePermissionsForCurrentUser($nodeThatRequiresEditPrivilege->workspaceName); if (!$workspacePermissions->write) { @@ -172,6 +155,30 @@ public function canExecuteCommand(CommandInterface $command): Privilege }; } + /** + * For a given command, determine the node (represented as {@see NodeAddress}) that needs {@see EditNodePrivilege} to be granted + */ + private function nodeThatRequiresEditPrivilegeForCommand(CommandInterface $command): ?NodeAddress + { + return match ($command::class) { + CopyNodesRecursively::class => NodeAddress::create($this->contentRepositoryId, $command->workspaceName, $command->targetDimensionSpacePoint->toDimensionSpacePoint(), $command->targetParentNodeAggregateId), + CreateNodeAggregateWithNode::class, + CreateNodeAggregateWithNodeAndSerializedProperties::class => NodeAddress::create($this->contentRepositoryId, $command->workspaceName, $command->originDimensionSpacePoint->toDimensionSpacePoint(), $command->parentNodeAggregateId), + CreateNodeVariant::class => NodeAddress::create($this->contentRepositoryId, $command->workspaceName, $command->sourceOrigin->toDimensionSpacePoint(), $command->nodeAggregateId), + DisableNodeAggregate::class, + EnableNodeAggregate::class, + RemoveNodeAggregate::class, + TagSubtree::class, + UntagSubtree::class => NodeAddress::create($this->contentRepositoryId, $command->workspaceName, $command->coveredDimensionSpacePoint, $command->nodeAggregateId), + MoveNodeAggregate::class => NodeAddress::create($this->contentRepositoryId, $command->workspaceName, $command->dimensionSpacePoint, $command->nodeAggregateId), + SetNodeProperties::class, + SetSerializedNodeProperties::class => NodeAddress::create($this->contentRepositoryId, $command->workspaceName, $command->originDimensionSpacePoint->toDimensionSpacePoint(), $command->nodeAggregateId), + SetNodeReferences::class, + SetSerializedNodeReferences::class => NodeAddress::create($this->contentRepositoryId, $command->workspaceName, $command->sourceOriginDimensionSpacePoint->toDimensionSpacePoint(), $command->sourceNodeAggregateId), + default => null, + }; + } + private function requireWorkspaceWritePermission(WorkspaceName $workspaceName): Privilege { $workspacePermissions = $this->getWorkspacePermissionsForCurrentUser($workspaceName); From 7c64c2c6c3f9c4a12cb1104ddd8f74645bfd5115 Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Wed, 6 Nov 2024 16:50:45 +0100 Subject: [PATCH 080/214] TASK: Tests for "Content Repository Privileges" Related: #3732 Related: #5298 --- ...ory.php => TestingAuthProviderFactory.php} | 9 +- .../Settings.ContentRepositoryRegistry.yaml | 2 +- .../Bootstrap/CRTestSuiteRuntimeVariables.php | 4 +- .../Bootstrap/Helpers/FakeAuthProvider.php | 42 ---- .../Bootstrap/Helpers/TestingAuthProvider.php | 70 ++++++ .../ContentRepositorySecurityTrait.php | 206 ++++++++++++++++++ .../Features/Bootstrap/ExceptionsTrait.php | 9 + .../Features/Bootstrap/FeatureContext.php | 1 + .../Features/Bootstrap/UserServiceTrait.php | 2 + .../ContentRepository/Security.feature | 75 +++++++ .../Security/NodeTreePrivilege.__feature | 148 ------------- 11 files changed, 370 insertions(+), 198 deletions(-) rename Neos.ContentRepository.BehavioralTests/Classes/TestSuite/Behavior/{FakeAuthProviderFactory.php => TestingAuthProviderFactory.php} (63%) delete mode 100644 Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Helpers/FakeAuthProvider.php create mode 100644 Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Helpers/TestingAuthProvider.php create mode 100644 Neos.Neos/Tests/Behavior/Features/Bootstrap/ContentRepositorySecurityTrait.php create mode 100644 Neos.Neos/Tests/Behavior/Features/ContentRepository/Security.feature delete mode 100644 Neos.Neos/Tests/Behavior/Features/Security/NodeTreePrivilege.__feature diff --git a/Neos.ContentRepository.BehavioralTests/Classes/TestSuite/Behavior/FakeAuthProviderFactory.php b/Neos.ContentRepository.BehavioralTests/Classes/TestSuite/Behavior/TestingAuthProviderFactory.php similarity index 63% rename from Neos.ContentRepository.BehavioralTests/Classes/TestSuite/Behavior/FakeAuthProviderFactory.php rename to Neos.ContentRepository.BehavioralTests/Classes/TestSuite/Behavior/TestingAuthProviderFactory.php index f239fb410d7..0fb820d5f03 100644 --- a/Neos.ContentRepository.BehavioralTests/Classes/TestSuite/Behavior/FakeAuthProviderFactory.php +++ b/Neos.ContentRepository.BehavioralTests/Classes/TestSuite/Behavior/TestingAuthProviderFactory.php @@ -4,16 +4,15 @@ namespace Neos\ContentRepository\BehavioralTests\TestSuite\Behavior; -use Neos\ContentRepository\Core\Feature\Security\AuthProviderInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphReadModelInterface; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; -use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Helpers\FakeAuthProvider; +use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Helpers\TestingAuthProvider; use Neos\ContentRepositoryRegistry\Factory\AuthProvider\AuthProviderFactoryInterface; -final class FakeAuthProviderFactory implements AuthProviderFactoryInterface +final class TestingAuthProviderFactory implements AuthProviderFactoryInterface { - public function build(ContentRepositoryId $contentRepositoryId, ContentGraphReadModelInterface $contentGraphReadModel): AuthProviderInterface + public function build(ContentRepositoryId $contentRepositoryId, ContentGraphReadModelInterface $contentGraphReadModel): TestingAuthProvider { - return new FakeAuthProvider(); + return new TestingAuthProvider(); } } diff --git a/Neos.ContentRepository.BehavioralTests/Configuration/Testing/Behat/Settings.ContentRepositoryRegistry.yaml b/Neos.ContentRepository.BehavioralTests/Configuration/Testing/Behat/Settings.ContentRepositoryRegistry.yaml index 74387df65ff..c7bd8303a73 100644 --- a/Neos.ContentRepository.BehavioralTests/Configuration/Testing/Behat/Settings.ContentRepositoryRegistry.yaml +++ b/Neos.ContentRepository.BehavioralTests/Configuration/Testing/Behat/Settings.ContentRepositoryRegistry.yaml @@ -3,7 +3,7 @@ Neos: presets: default: authProvider: - factoryObjectName: 'Neos\ContentRepository\BehavioralTests\TestSuite\Behavior\FakeAuthProviderFactory' + factoryObjectName: 'Neos\ContentRepository\BehavioralTests\TestSuite\Behavior\TestingAuthProviderFactory' clock: factoryObjectName: 'Neos\ContentRepository\BehavioralTests\TestSuite\Behavior\FakeClockFactory' nodeTypeManager: diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteRuntimeVariables.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteRuntimeVariables.php index b6ca95a29b6..d8ff69457b2 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteRuntimeVariables.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteRuntimeVariables.php @@ -26,7 +26,7 @@ use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; -use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Helpers\FakeAuthProvider; +use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Helpers\TestingAuthProvider; use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Helpers\FakeClock; /** @@ -73,7 +73,7 @@ abstract protected function getContentRepository(ContentRepositoryId $id): Conte */ public function iAmUserIdentifiedBy(string $userId): void { - FakeAuthProvider::setUserId(UserId::fromString($userId)); + TestingAuthProvider::setDefaultUserId(UserId::fromString($userId)); } /** diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Helpers/FakeAuthProvider.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Helpers/FakeAuthProvider.php deleted file mode 100644 index 916e98ffee3..00000000000 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Helpers/FakeAuthProvider.php +++ /dev/null @@ -1,42 +0,0 @@ -getAuthenticatedUserId(); + } + return self::$userId ?? null; + } + + public function getVisibilityConstraints(WorkspaceName $workspaceName): VisibilityConstraints + { + if (self::$contentRepositoryAuthProvider !== null) { + return self::$contentRepositoryAuthProvider->getVisibilityConstraints($workspaceName); + } + return VisibilityConstraints::withoutRestrictions(); + } + + public function canReadNodesFromWorkspace(WorkspaceName $workspaceName): Privilege + { + if (self::$contentRepositoryAuthProvider !== null) { + return self::$contentRepositoryAuthProvider->canReadNodesFromWorkspace($workspaceName); + } + return Privilege::granted(self::class . ' always grants privileges'); + } + + public function canExecuteCommand(CommandInterface $command): Privilege + { + if (self::$contentRepositoryAuthProvider !== null) { + return self::$contentRepositoryAuthProvider->canExecuteCommand($command); + } + return Privilege::granted(self::class . ' always grants privileges'); + } +} diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/ContentRepositorySecurityTrait.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/ContentRepositorySecurityTrait.php new file mode 100644 index 00000000000..6d950404a80 --- /dev/null +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/ContentRepositorySecurityTrait.php @@ -0,0 +1,206 @@ + $className + * @return T + */ + abstract private function getObject(string $className): object; + + #[BeforeScenario] + public function resetContentRepositorySecurity(): void + { + TestingAuthProvider::resetAuthProvider(); + $this->contentRepositorySecurityEnabled = false; + } + + #[BeforeFeature] + #[AfterFeature] + public static function resetPolicies(): void + { + if (self::$testingPolicyPathAndFilename !== null && file_exists(self::$testingPolicyPathAndFilename)) { + unlink(self::$testingPolicyPathAndFilename); + } + } + + private function enableFlowSecurity(): void + { + if ($this->flowSecurityEnabled === true) { + return; + } + $this->getObject(PrivilegeManagerInterface::class)->reset(); + + $tokenAndProviderFactory = $this->getObject(TokenAndProviderFactoryInterface::class); + + $this->testingProvider = $tokenAndProviderFactory->getProviders()['TestingProvider']; + + $securityContext = $this->getObject(SecurityContext::class); + $securityContext->clearContext(); + $httpRequest = $this->getObject(ServerRequestFactoryInterface::class)->createServerRequest('GET', 'http://localhost/'); + $this->mockActionRequest = ActionRequest::fromHttpRequest($httpRequest); + $securityContext->setRequest($this->mockActionRequest); + $this->flowSecurityEnabled = true; + } + + private function enableContentRepositorySecurity(): void + { + if ($this->contentRepositorySecurityEnabled === true) { + return; + } + $contentRepositoryAuthProviderFactory = $this->getObject(ContentRepositoryAuthProviderFactory::class); + $contentGraphProjection = $this->getContentRepositoryService(new class implements ContentRepositoryServiceFactoryInterface { + public function build(ContentRepositoryServiceFactoryDependencies $serviceFactoryDependencies): ContentRepositoryServiceInterface + { + $contentGraphProjection = $serviceFactoryDependencies->projectionsAndCatchUpHooks->contentGraphProjection; + return new class ($contentGraphProjection) implements ContentRepositoryServiceInterface { + public function __construct( + public ContentGraphProjectionInterface $contentGraphProjection, + ) { + } + }; + } + })->contentGraphProjection; + $contentRepositoryAuthProvider = $contentRepositoryAuthProviderFactory->build($this->currentContentRepository->id, $contentGraphProjection->getState()); + + TestingAuthProvider::replaceAuthProvider($contentRepositoryAuthProvider); + $this->contentRepositorySecurityEnabled = true; + } + + private function authenticateAccount(Account $account): void + { + $this->enableFlowSecurity(); + $this->testingProvider->setAuthenticationStatus(TokenInterface::AUTHENTICATION_SUCCESSFUL); + $this->testingProvider->setAccount($account); + + $securityContext = $this->getObject(SecurityContext::class); + $securityContext->clearContext(); + $securityContext->setRequest($this->mockActionRequest); + $this->getObject(AuthenticationProviderManager::class)->authenticate(); + } + + /** + * @Given content repository security is enabled + */ + public function contentRepositorySecurityIsEnabled(): void + { + $this->enableContentRepositorySecurity(); + } + + + /** + * @Given The following additional policies are configured: + */ + public function theFollowingAdditionalPoliciesAreConfigured(PyStringNode $policies): void + { + $policyService = $this->getObject(PolicyService::class); + $policyService->getRoles(); // force initialization + $policyConfiguration = ObjectAccess::getProperty($policyService, 'policyConfiguration', true); + $mergedPolicyConfiguration = Arrays::arrayMergeRecursiveOverrule($policyConfiguration, Yaml::parse($policies->getRaw())); + + self::$testingPolicyPathAndFilename = $this->getObject(Environment::class)->getPathToTemporaryDirectory() . 'Policy.yaml'; + file_put_contents(self::$testingPolicyPathAndFilename, Yaml::dump($mergedPolicyConfiguration)); + + ObjectAccess::setProperty($policyService, 'initialized', false, true); + $this->getObject(ConfigurationManager::class)->flushConfigurationCache(); + } + + /** + * @When the user :username accesses the content graph for workspace :workspaceName + */ + public function theUserAccessesTheContentGraphForWorkspace(string $username, string $workspaceName): void + { + $this->enableContentRepositorySecurity(); + $user = $this->getObject(UserService::class)->getUser($username); + $this->authenticateAccount($user->getAccounts()->first()); + $this->tryCatchingExceptions(fn () => $this->currentContentRepository->getContentGraph(WorkspaceName::fromString($workspaceName))); + } + + /** + * @Then The user :username should not be able to read node :nodeAggregateId + */ + public function theUserShouldNotBeAbleToReadNode(string $username, string $nodeAggregateId): void + { + $user = $this->getObject(UserService::class)->getUser($username); + $this->authenticateAccount($user->getAccounts()->first()); + $node = $this->currentContentRepository->getContentSubgraph($this->currentWorkspaceName, $this->currentDimensionSpacePoint)->findNodeById(NodeAggregateId::fromString($nodeAggregateId)); + if ($node !== null) { + Assert::fail(sprintf('Expected node "%s" to be inaccessible to user "%s" but it was loaded', $nodeAggregateId, $username)); + } + } + + /** + * @Then The user :username should be able to read node :nodeAggregateId + */ + public function theUserShouldBeAbleToReadNode(string $username, string $nodeAggregateId): void + { + $user = $this->getObject(UserService::class)->getUser($username); + $this->authenticateAccount($user->getAccounts()->first()); + $node = $this->currentContentRepository->getContentSubgraph($this->currentWorkspaceName, $this->currentDimensionSpacePoint)->findNodeById(NodeAggregateId::fromString($nodeAggregateId)); + if ($node === null) { + Assert::fail(sprintf('Expected node "%s" to be accessible to user "%s" but it could not be loaded', $nodeAggregateId, $username)); + } + } +} diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/ExceptionsTrait.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/ExceptionsTrait.php index fabfcd12608..228d4594bdc 100644 --- a/Neos.Neos/Tests/Behavior/Features/Bootstrap/ExceptionsTrait.php +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/ExceptionsTrait.php @@ -46,6 +46,15 @@ public function anExceptionShouldBeThrown(string $exceptionMessage): void $this->lastCaughtException = null; } + /** + * @Then no exception should be thrown + */ + public function noExceptionShouldBeThrown(): void + { + Assert::assertNull($this->lastCaughtException, 'Expected no exception but one was thrown'); + $this->lastCaughtException = null; + } + /** * @BeforeScenario * @AfterScenario diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/FeatureContext.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/FeatureContext.php index 9d06ce491eb..a522afdd352 100644 --- a/Neos.Neos/Tests/Behavior/Features/Bootstrap/FeatureContext.php +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/FeatureContext.php @@ -46,6 +46,7 @@ class FeatureContext implements BehatContext use AssetTrait; use WorkspaceServiceTrait; + use ContentRepositorySecurityTrait; use UserServiceTrait; protected Environment $environment; diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/UserServiceTrait.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/UserServiceTrait.php index 4d8d153f64e..4c4118f8f57 100644 --- a/Neos.Neos/Tests/Behavior/Features/Bootstrap/UserServiceTrait.php +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/UserServiceTrait.php @@ -16,8 +16,10 @@ use Neos\Flow\Persistence\PersistenceManagerInterface; use Neos\Flow\Security\AccountFactory; use Neos\Flow\Security\Cryptography\HashService; +use Neos\Flow\Security\Policy\PolicyService; use Neos\Neos\Domain\Model\User; use Neos\Neos\Domain\Service\UserService; +use Neos\Neos\Security\Authorization\Privilege\ReadNodePrivilege; use Neos\Party\Domain\Model\PersonName; use Neos\Utility\ObjectAccess; diff --git a/Neos.Neos/Tests/Behavior/Features/ContentRepository/Security.feature b/Neos.Neos/Tests/Behavior/Features/ContentRepository/Security.feature new file mode 100644 index 00000000000..9b4e721c733 --- /dev/null +++ b/Neos.Neos/Tests/Behavior/Features/ContentRepository/Security.feature @@ -0,0 +1,75 @@ +@flowEntities +Feature: TODO + + Background: + Given The following additional policies are configured: + """ + privilegeTargets: + 'Neos\Neos\Security\Authorization\Privilege\ReadNodePrivilege': + 'Neos.Neos:ReadBlog': + matcher: 'blog' + roles: + + 'Neos.Neos:Administrator': + privileges: + - privilegeTarget: 'Neos.Neos:ReadBlog' + permission: GRANT + """ + And using no content dimensions + And using the following node types: + """yaml + 'Neos.ContentRepository.Testing:Document': {} + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + And I am user identified by "initiating-user-identifier" + And the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | newContentStreamId | "cs-identifier" | + And I am in workspace "live" and dimension space point {} + And the command CreateRootNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "root" | + | nodeTypeName | "Neos.ContentRepository:Root" | + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeTypeName | parentNodeAggregateId | nodeName | + | a | Neos.ContentRepository.Testing:Document | root | a | + | a1 | Neos.ContentRepository.Testing:Document | a | a1 | + | a1a | Neos.ContentRepository.Testing:Document | a1 | a1a | + | a1a1 | Neos.ContentRepository.Testing:Document | a1a | a1a1 | + | a1a1a | Neos.ContentRepository.Testing:Document | a1a1 | a1a1a | + | a1a1b | Neos.ContentRepository.Testing:Document | a1a1 | a1a1b | + | a1a2 | Neos.ContentRepository.Testing:Document | a1a | a1a2 | + | a1b | Neos.ContentRepository.Testing:Document | a1 | a1b | + | a2 | Neos.ContentRepository.Testing:Document | a | a2 | + | b | Neos.ContentRepository.Testing:Document | root | b | + | b1 | Neos.ContentRepository.Testing:Document | b | b1 | + And the following Neos users exist: + | Id | Username | First name | Last name | Roles | + | janedoe | jane.doe | Jane | Doe | Neos.Neos:Administrator | + | johndoe | john.doe | John | Doe | Neos.Neos:RestrictedEditor,Neos.Neos:UserManager | + | editor | editor | Edward | Editor | Neos.Neos:Editor | + + Scenario: Access content graph for root workspace without role assignments + Given I am in workspace "live" + And the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "a" | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "blog" | + And the role VIEWER is assigned to workspace "live" for group "Neos.Flow:Everybody" + When content repository security is enabled + Then The user "john.doe" should not be able to read node "a1" + Then The user "jane.doe" should be able to read node "a1" + + Scenario: TODO + When content repository security is enabled + And the user "jane.doe" accesses the content graph for workspace "live" + Then an exception 'Read access denied for workspace "live": Account "jane.doe" is a Neos Administrator without explicit role for workspace "live"' should be thrown + + Scenario: TODO + Given the role MANAGER is assigned to workspace "live" for user "janedoe" + When content repository security is enabled + And the user "jane.doe" accesses the content graph for workspace "live" + Then no exception should be thrown diff --git a/Neos.Neos/Tests/Behavior/Features/Security/NodeTreePrivilege.__feature b/Neos.Neos/Tests/Behavior/Features/Security/NodeTreePrivilege.__feature deleted file mode 100644 index 2fa92e93581..00000000000 --- a/Neos.Neos/Tests/Behavior/Features/Security/NodeTreePrivilege.__feature +++ /dev/null @@ -1,148 +0,0 @@ -# TODO rewrite test after https://github.com/neos/neos-development-collection/issues/3732 - -Feature: Privilege to restrict nodes shown in the node tree - - Background: - Given I have the following policies: - """ - privilegeTargets: - - 'Neos\Neos\Security\Authorization\Privilege\NodeTreePrivilege': - 'Neos.ContentRepository:CompanySubtree': - matcher: 'isDescendantNodeOf("/sites/content-repository/company")' - 'Neos.ContentRepository:ServiceSubtree': - matcher: 'isDescendantNodeOf("/sites/content-repository/service")' - - 'Neos.ContentRepository:NeosSite': - matcher: 'isDescendantNodeOf("/sites/neos")' - 'Neos.ContentRepository:NeosTeams': - matcher: 'isAncestorOrDescendantNodeOf("/sites/neos/community/teams")' - - 'Neos\ContentRepository\Security\Authorization\Privilege\Node\EditNodePrivilege': - 'Neos.ContentRepository:EditNeosTeamsPath': - matcher: 'isAncestorNodeOf("/sites/neos/community/teams")' - - roles: - 'Neos.Flow:Everybody': - privileges: [] - - 'Neos.Flow:Anonymous': - privileges: [] - - 'Neos.Flow:AuthenticatedUser': - privileges: [] - - 'Neos.Neos:Editor': - privileges: - - - privilegeTarget: 'Neos.ContentRepository:CompanySubtree' - permission: GRANT - - 'Neos.Neos:Administrator': - parentRoles: ['Neos.Neos:Editor'] - privileges: - - - privilegeTarget: 'Neos.ContentRepository:ServiceSubtree' - permission: GRANT - - - privilegeTarget: 'Neos.ContentRepository:NeosTeams' - permission: GRANT - - - privilegeTarget: 'Neos.ContentRepository:EditNeosTeamsPath' - permission: DENY - - """ - - And I have the following nodes: - | Identifier | Path | Node Type | Properties | Workspace | - | ecf40ad1-3119-0a43-d02e-55f8b5aa3c70 | /sites | unstructured | | live | - | fd5ba6e1-4313-b145-1004-dad2f1173a35 | /sites/content-repository | Neos.ContentRepository.Testing:Document | {"title": "Home"} | live | - | 68ca0dcd-2afb-ef0e-1106-a5301e65b8a0 | /sites/content-repository/company | Neos.ContentRepository.Testing:Document | {"title": "Company"} | live | - | 52540602-b417-11e3-9358-14109fd7a2dd | /sites/content-repository/service | Neos.ContentRepository.Testing:Document | {"title": "Service"} | live | - | 3223481d-e11c-4db7-95de-b371411a2431 | /sites/content-repository/service/newsletter | Neos.ContentRepository.Testing:Document | {"title": "Newsletter"} | live | - | 544e14a3-b21d-429a-9fdd-cbeccc8d2b0f | /sites/content-repository/about-us | Neos.ContentRepository.Testing:Document | {"title": "About us"} | live | - | 56217c92-07e9-4554-ac35-03f86d278870 | /sites/neos | Neos.ContentRepository.Testing:Document | {"title": "Neos"} | live | - | 4be072fe-0738-4892-8a27-342a6ac96075 | /sites/neos/community | Neos.ContentRepository.Testing:Document | {"title": "Community"} | live | - | c56d66e7-9c55-4eef-a2b1-c263b3261996 | /sites/neos/community/teams | Neos.ContentRepository.Testing:Document | {"title": "Teams"} | live | - | 07902b2e-61d9-4ce4-9b90-1cf338830d2f | /sites/neos/community/teams/member| Neos.ContentRepository.Testing:Document | {"title": "Johannes"} | live | - - @Isolated @fixtures - Scenario: Editors are granted to set properties on company node - Given I am authenticated with role "Neos.Neos:Editor" - And I get a node by path "/sites/content-repository/company" with the following context: - | Workspace | - | user-admin | - Then I should be granted to set the "title" property to "The company" - And I should get true when asking the node authorization service if editing this node is granted - - @Isolated @fixtures - Scenario: Editors are not granted to set properties on service node - Given I am authenticated with role "Neos.Neos:Editor" - And I get a node by path "/sites/content-repository/service" with the following context: - | Workspace | - | user-admin | - Then I should not be granted to set the "title" property to "Our services" - And I should get false when asking the node authorization service if editing this node is granted - - @Isolated @fixtures - Scenario: Editors are not granted to set properties on service sub node - Given I am authenticated with role "Neos.Neos:Editor" - And I get a node by path "/sites/content-repository/service/newsletter" with the following context: - | Workspace | - | user-admin | - Then I should not be granted to set the "title" property to "Our newsletter" - And I should get false when asking the node authorization service if editing this node is granted - - @Isolated @fixtures - Scenario: Administrators are granted to set properties on company node - Given I am authenticated with role "Neos.Neos:Administrator" - And I get a node by path "/sites/content-repository/company" with the following context: - | Workspace | - | user-admin | - Then I should be granted to set the "title" property to "The company" - And I should get true when asking the node authorization service if editing this node is granted - - @Isolated @fixtures - Scenario: Administrators are granted to set properties on service node - Given I am authenticated with role "Neos.Neos:Administrator" - And I get a node by path "/sites/content-repository/service" with the following context: - | Workspace | - | user-admin | - Then I should be granted to set the "title" property to "Our services" - And I should get true when asking the node authorization service if editing this node is granted - - @Isolated @fixtures - Scenario: Administrators are granted to set properties on service sub node - Given I am authenticated with role "Neos.Neos:Administrator" - And I get a node by path "/sites/content-repository/service/newsletter" with the following context: - | Workspace | - | user-admin | - Then I should be granted to set the "title" property to "Our newsletter" - And I should get true when asking the node authorization service if editing this node is granted - - @Isolated @fixtures - Scenario: Editors are not granted to set properties on a neos sub node - Given I am authenticated with role "Neos.Neos:Editor" - And I get a node by path "/sites/neos/community/teams" with the following context: - | Workspace | - | user-admin | - Then I should not be granted to set the "title" property to "The Teams" - And I should get false when asking the node authorization service if editing this node is granted - - @Isolated @fixtures - Scenario: Administrators are granted to set properties on a neos sub node - Given I am authenticated with role "Neos.Neos:Administrator" - And I get a node by path "/sites/neos/community/teams/member" with the following context: - | Workspace | - | user-admin | - Then I should be granted to set the "title" property to "Basti" - And I should get true when asking the node authorization service if editing this node is granted - - @Isolated @fixtures - Scenario: Administrators are not granted to set properties on an ancestor node of teams - Given I am authenticated with role "Neos.Neos:Administrator" - And I get a node by path "/sites/neos/community" with the following context: - | Workspace | - | user-admin | - Then I should not be granted to set the "title" property to "The Community" - And I should get false when asking the node authorization service if editing this node is granted From dec5667d4d6fce4974a45689879cc71ea63630ed Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Thu, 7 Nov 2024 08:38:40 +0100 Subject: [PATCH 081/214] moar tests --- .../ContentRepositorySecurityTrait.php | 30 +++++---- .../Bootstrap/WorkspaceServiceTrait.php | 31 +++++++-- .../Security/EditNodePrivilege.feature | 65 ++++++++++++++++++ .../ReadNodePrivilege.feature} | 53 +++++++-------- .../Security/WorkspaceAccess.feature | 66 +++++++++++++++++++ 5 files changed, 196 insertions(+), 49 deletions(-) create mode 100644 Neos.Neos/Tests/Behavior/Features/ContentRepository/Security/EditNodePrivilege.feature rename Neos.Neos/Tests/Behavior/Features/ContentRepository/{Security.feature => Security/ReadNodePrivilege.feature} (51%) create mode 100644 Neos.Neos/Tests/Behavior/Features/ContentRepository/Security/WorkspaceAccess.feature diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/ContentRepositorySecurityTrait.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/ContentRepositorySecurityTrait.php index 6d950404a80..76c71de0e68 100644 --- a/Neos.Neos/Tests/Behavior/Features/Bootstrap/ContentRepositorySecurityTrait.php +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/ContentRepositorySecurityTrait.php @@ -146,6 +146,7 @@ private function authenticateAccount(Account $account): void */ public function contentRepositorySecurityIsEnabled(): void { + $this->enableFlowSecurity(); $this->enableContentRepositorySecurity(); } @@ -168,39 +169,42 @@ public function theFollowingAdditionalPoliciesAreConfigured(PyStringNode $polici } /** - * @When the user :username accesses the content graph for workspace :workspaceName + * @When the user :username is authenticated */ - public function theUserAccessesTheContentGraphForWorkspace(string $username, string $workspaceName): void + public function theUserIsAuthenticated(string $username): void { - $this->enableContentRepositorySecurity(); $user = $this->getObject(UserService::class)->getUser($username); $this->authenticateAccount($user->getAccounts()->first()); + } + + + /** + * @When the current user accesses the content graph for workspace :workspaceName + */ + public function theCurrentUserAccessesTheContentGraphForWorkspace(string $workspaceName): void + { $this->tryCatchingExceptions(fn () => $this->currentContentRepository->getContentGraph(WorkspaceName::fromString($workspaceName))); } /** - * @Then The user :username should not be able to read node :nodeAggregateId + * @Then The current user should not be able to read node :nodeAggregateId */ - public function theUserShouldNotBeAbleToReadNode(string $username, string $nodeAggregateId): void + public function theCurrentUserShouldNotBeAbleToReadNode(string $nodeAggregateId): void { - $user = $this->getObject(UserService::class)->getUser($username); - $this->authenticateAccount($user->getAccounts()->first()); $node = $this->currentContentRepository->getContentSubgraph($this->currentWorkspaceName, $this->currentDimensionSpacePoint)->findNodeById(NodeAggregateId::fromString($nodeAggregateId)); if ($node !== null) { - Assert::fail(sprintf('Expected node "%s" to be inaccessible to user "%s" but it was loaded', $nodeAggregateId, $username)); + Assert::fail(sprintf('Expected node "%s" to be inaccessible but it was loaded', $nodeAggregateId)); } } /** - * @Then The user :username should be able to read node :nodeAggregateId + * @Then The current user should be able to read node :nodeAggregateId */ - public function theUserShouldBeAbleToReadNode(string $username, string $nodeAggregateId): void + public function theCurrentUserShouldBeAbleToReadNode(string $nodeAggregateId): void { - $user = $this->getObject(UserService::class)->getUser($username); - $this->authenticateAccount($user->getAccounts()->first()); $node = $this->currentContentRepository->getContentSubgraph($this->currentWorkspaceName, $this->currentDimensionSpacePoint)->findNodeById(NodeAggregateId::fromString($nodeAggregateId)); if ($node === null) { - Assert::fail(sprintf('Expected node "%s" to be accessible to user "%s" but it could not be loaded', $nodeAggregateId, $username)); + Assert::fail(sprintf('Expected node "%s" to be accessible but it could not be loaded', $nodeAggregateId)); } } } diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/WorkspaceServiceTrait.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/WorkspaceServiceTrait.php index 416b6d4a8d3..28fbdba1a00 100644 --- a/Neos.Neos/Tests/Behavior/Features/Bootstrap/WorkspaceServiceTrait.php +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/WorkspaceServiceTrait.php @@ -14,10 +14,12 @@ use Behat\Gherkin\Node\TableNode; use Neos\ContentRepository\BehavioralTests\TestSuite\Behavior\CRBehavioralTestsSubjectProvider; +use Neos\ContentRepository\Core\CommandHandler\CommandInterface; use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Command\CreateRootWorkspace; use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Command\CreateWorkspace; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; +use Neos\Flow\Reflection\ReflectionService; use Neos\Neos\Domain\Model\UserId; use Neos\Neos\Domain\Model\WorkspaceDescription; use Neos\Neos\Domain\Model\WorkspaceRole; @@ -166,15 +168,20 @@ public function theWorkspaceShouldHaveTheFollowingMetadata($workspaceName, Table /** * @When the role :role is assigned to workspace :workspaceName for group :groupName - * @When the role :role is assigned to workspace :workspaceName for user :userId + * @When the role :role is assigned to workspace :workspaceName for user :username */ - public function theRoleIsAssignedToWorkspaceForGroupOrUser(string $role, string $workspaceName, string $groupName = null, string $userId = null): void + public function theRoleIsAssignedToWorkspaceForGroupOrUser(string $role, string $workspaceName, string $groupName = null, string $username = null): void { + if ($groupName !== null) { + $subject = WorkspaceRoleSubject::createForGroup($groupName); + } else { + $subject = WorkspaceRoleSubject::createForUser($this->userIdForUsername($username)); + } $this->tryCatchingExceptions(fn () => $this->getObject(WorkspaceService::class)->assignWorkspaceRole( $this->currentContentRepository->id, WorkspaceName::fromString($workspaceName), WorkspaceRoleAssignment::create( - $groupName !== null ? WorkspaceRoleSubject::createForGroup($groupName) : WorkspaceRoleSubject::createForUser(UserId::fromString($userId)), + $subject, WorkspaceRole::from($role) ) )); @@ -182,14 +189,19 @@ public function theRoleIsAssignedToWorkspaceForGroupOrUser(string $role, string /** * @When the role for group :groupName is unassigned from workspace :workspaceName - * @When the role for user :userId is unassigned from workspace :workspaceName + * @When the role for user :username is unassigned from workspace :workspaceName */ - public function theRoleIsUnassignedFromWorkspace(string $workspaceName, string $groupName = null, string $userId = null): void + public function theRoleIsUnassignedFromWorkspace(string $workspaceName, string $groupName = null, string $username = null): void { + if ($groupName !== null) { + $subject = WorkspaceRoleSubject::createForGroup($groupName); + } else { + $subject = WorkspaceRoleSubject::createForUser($this->userIdForUsername($username)); + } $this->tryCatchingExceptions(fn () => $this->getObject(WorkspaceService::class)->unassignWorkspaceRole( $this->currentContentRepository->id, WorkspaceName::fromString($workspaceName), - $groupName !== null ? WorkspaceRoleSubject::createForGroup($groupName) : WorkspaceRoleSubject::createForUser(UserId::fromString($userId)), + $subject, )); } @@ -242,4 +254,11 @@ public function theNeosUserShouldHaveNoPermissionsForWorkspace(string $username, Assert::assertFalse($permissions->write); Assert::assertFalse($permissions->manage); } + + private function userIdForUsername(string $username): UserId + { + $user = $this->getObject(UserService::class)->getUser($username); + Assert::assertNotNull($user); + return $user->getId(); + } } diff --git a/Neos.Neos/Tests/Behavior/Features/ContentRepository/Security/EditNodePrivilege.feature b/Neos.Neos/Tests/Behavior/Features/ContentRepository/Security/EditNodePrivilege.feature new file mode 100644 index 00000000000..3f86ee05114 --- /dev/null +++ b/Neos.Neos/Tests/Behavior/Features/ContentRepository/Security/EditNodePrivilege.feature @@ -0,0 +1,65 @@ +@flowEntities +Feature: EditNodePrivilege related features + + Background: + Given The following additional policies are configured: + """ + privilegeTargets: + 'Neos\Neos\Security\Authorization\Privilege\EditNodePrivilege': + 'Neos.Neos:EditBlog': + matcher: 'blog' + """ + And using the following content dimensions: + | Identifier | Values | Generalizations | + | language | mul, de, en, gsw, ltz | ltz->de->mul, gsw->de->mul, en->mul | + And using the following node types: + """yaml + 'Neos.Neos:Document': {} + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + And I am user identified by "initiating-user-identifier" + And the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | newContentStreamId | "cs-identifier" | + And I am in workspace "live" and dimension space point {} + And the command CreateRootNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "root" | + | nodeTypeName | "Neos.ContentRepository:Root" | + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeTypeName | parentNodeAggregateId | nodeName | originDimensionSpacePoint | + | a | Neos.Neos:Document | root | a | {"language":"mul"} | + | a1 | Neos.Neos:Document | a | a1 | {"language":"de"} | + | a1a | Neos.Neos:Document | a1 | a1a | {"language":"de"} | + | a1a1 | Neos.Neos:Document | a1a | a1a1 | {"language":"de"} | + | a1a1a | Neos.Neos:Document | a1a1 | a1a1a | {"language":"de"} | + | a1a1b | Neos.Neos:Document | a1a1 | a1a1b | {"language":"de"} | + | a1a2 | Neos.Neos:Document | a1a | a1a2 | {"language":"de"} | + | a1b | Neos.Neos:Document | a1 | a1b | {"language":"de"} | + | a2 | Neos.Neos:Document | a | a2 | {"language":"de"} | + | b | Neos.Neos:Document | root | b | {"language":"de"} | + | b1 | Neos.Neos:Document | b | b1 | {"language":"de"} | + And the following Neos users exist: + | Id | Username | First name | Last name | Roles | + | janedoe | jane.doe | Jane | Doe | Neos.Neos:Administrator | + | johndoe | john.doe | John | Doe | Neos.Neos:RestrictedEditor,Neos.Neos:UserManager | + | editor | editor | Edward | Editor | Neos.Neos:Editor | + + Scenario: TODO + Given I am in workspace "live" + And I am in dimension space point {"language":"de"} + And the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "a" | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "blog" | + And the role MANAGER is assigned to workspace "live" for user "jane.doe" + When content repository security is enabled + And the user "jane.doe" is authenticated + When the command DisableNodeAggregate is executed with payload and exceptions are caught: + | Key | Value | + | nodeAggregateId | "a1a" | + | nodeVariantSelectionStrategy | "allVariants" | + Then the last command should have thrown an exception of type "AccessDenied" diff --git a/Neos.Neos/Tests/Behavior/Features/ContentRepository/Security.feature b/Neos.Neos/Tests/Behavior/Features/ContentRepository/Security/ReadNodePrivilege.feature similarity index 51% rename from Neos.Neos/Tests/Behavior/Features/ContentRepository/Security.feature rename to Neos.Neos/Tests/Behavior/Features/ContentRepository/Security/ReadNodePrivilege.feature index 9b4e721c733..886f619599c 100644 --- a/Neos.Neos/Tests/Behavior/Features/ContentRepository/Security.feature +++ b/Neos.Neos/Tests/Behavior/Features/ContentRepository/Security/ReadNodePrivilege.feature @@ -1,5 +1,5 @@ @flowEntities -Feature: TODO +Feature: ReadNodePrivilege related features Background: Given The following additional policies are configured: @@ -9,16 +9,17 @@ Feature: TODO 'Neos.Neos:ReadBlog': matcher: 'blog' roles: - 'Neos.Neos:Administrator': privileges: - privilegeTarget: 'Neos.Neos:ReadBlog' permission: GRANT """ - And using no content dimensions + And using the following content dimensions: + | Identifier | Values | Generalizations | + | language | mul, de, en, gsw, ltz | ltz->de->mul, gsw->de->mul, en->mul | And using the following node types: """yaml - 'Neos.ContentRepository.Testing:Document': {} + 'Neos.Neos:Document': {} """ And using identifier "default", I define a content repository And I am in content repository "default" @@ -33,26 +34,27 @@ Feature: TODO | nodeAggregateId | "root" | | nodeTypeName | "Neos.ContentRepository:Root" | And the following CreateNodeAggregateWithNode commands are executed: - | nodeAggregateId | nodeTypeName | parentNodeAggregateId | nodeName | - | a | Neos.ContentRepository.Testing:Document | root | a | - | a1 | Neos.ContentRepository.Testing:Document | a | a1 | - | a1a | Neos.ContentRepository.Testing:Document | a1 | a1a | - | a1a1 | Neos.ContentRepository.Testing:Document | a1a | a1a1 | - | a1a1a | Neos.ContentRepository.Testing:Document | a1a1 | a1a1a | - | a1a1b | Neos.ContentRepository.Testing:Document | a1a1 | a1a1b | - | a1a2 | Neos.ContentRepository.Testing:Document | a1a | a1a2 | - | a1b | Neos.ContentRepository.Testing:Document | a1 | a1b | - | a2 | Neos.ContentRepository.Testing:Document | a | a2 | - | b | Neos.ContentRepository.Testing:Document | root | b | - | b1 | Neos.ContentRepository.Testing:Document | b | b1 | + | nodeAggregateId | nodeTypeName | parentNodeAggregateId | nodeName | originDimensionSpacePoint | + | a | Neos.Neos:Document | root | a | {"language":"mul"} | + | a1 | Neos.Neos:Document | a | a1 | {"language":"de"} | + | a1a | Neos.Neos:Document | a1 | a1a | {"language":"de"} | + | a1a1 | Neos.Neos:Document | a1a | a1a1 | {"language":"de"} | + | a1a1a | Neos.Neos:Document | a1a1 | a1a1a | {"language":"de"} | + | a1a1b | Neos.Neos:Document | a1a1 | a1a1b | {"language":"de"} | + | a1a2 | Neos.Neos:Document | a1a | a1a2 | {"language":"de"} | + | a1b | Neos.Neos:Document | a1 | a1b | {"language":"de"} | + | a2 | Neos.Neos:Document | a | a2 | {"language":"de"} | + | b | Neos.Neos:Document | root | b | {"language":"de"} | + | b1 | Neos.Neos:Document | b | b1 | {"language":"de"} | And the following Neos users exist: | Id | Username | First name | Last name | Roles | | janedoe | jane.doe | Jane | Doe | Neos.Neos:Administrator | | johndoe | john.doe | John | Doe | Neos.Neos:RestrictedEditor,Neos.Neos:UserManager | | editor | editor | Edward | Editor | Neos.Neos:Editor | - Scenario: Access content graph for root workspace without role assignments + Scenario: TODO Given I am in workspace "live" + And I am in dimension space point {"language":"de"} And the command TagSubtree is executed with payload: | Key | Value | | nodeAggregateId | "a" | @@ -60,16 +62,7 @@ Feature: TODO | tag | "blog" | And the role VIEWER is assigned to workspace "live" for group "Neos.Flow:Everybody" When content repository security is enabled - Then The user "john.doe" should not be able to read node "a1" - Then The user "jane.doe" should be able to read node "a1" - - Scenario: TODO - When content repository security is enabled - And the user "jane.doe" accesses the content graph for workspace "live" - Then an exception 'Read access denied for workspace "live": Account "jane.doe" is a Neos Administrator without explicit role for workspace "live"' should be thrown - - Scenario: TODO - Given the role MANAGER is assigned to workspace "live" for user "janedoe" - When content repository security is enabled - And the user "jane.doe" accesses the content graph for workspace "live" - Then no exception should be thrown + And the user "john.doe" is authenticated + Then The current user should not be able to read node "a1" + When the user "jane.doe" is authenticated + Then The current user should be able to read node "a1" diff --git a/Neos.Neos/Tests/Behavior/Features/ContentRepository/Security/WorkspaceAccess.feature b/Neos.Neos/Tests/Behavior/Features/ContentRepository/Security/WorkspaceAccess.feature new file mode 100644 index 00000000000..eaf3d7ea4dd --- /dev/null +++ b/Neos.Neos/Tests/Behavior/Features/ContentRepository/Security/WorkspaceAccess.feature @@ -0,0 +1,66 @@ +@flowEntities +Feature: Workspace access related features + + Background: + Given The following additional policies are configured: + """ + privilegeTargets: + 'Neos\Neos\Security\Authorization\Privilege\ReadNodePrivilege': + 'Neos.Neos:ReadBlog': + matcher: 'blog' + roles: + 'Neos.Neos:Administrator': + privileges: + - privilegeTarget: 'Neos.Neos:ReadBlog' + permission: GRANT + """ + And using the following content dimensions: + | Identifier | Values | Generalizations | + | language | mul, de, en, gsw, ltz | ltz->de->mul, gsw->de->mul, en->mul | + And using the following node types: + """yaml + 'Neos.Neos:Document': {} + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + And I am user identified by "initiating-user-identifier" + And the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | newContentStreamId | "cs-identifier" | + And I am in workspace "live" and dimension space point {} + And the command CreateRootNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "root" | + | nodeTypeName | "Neos.ContentRepository:Root" | + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeTypeName | parentNodeAggregateId | nodeName | originDimensionSpacePoint | + | a | Neos.Neos:Document | root | a | {"language":"mul"} | + | a1 | Neos.Neos:Document | a | a1 | {"language":"de"} | + | a1a | Neos.Neos:Document | a1 | a1a | {"language":"de"} | + | a1a1 | Neos.Neos:Document | a1a | a1a1 | {"language":"de"} | + | a1a1a | Neos.Neos:Document | a1a1 | a1a1a | {"language":"de"} | + | a1a1b | Neos.Neos:Document | a1a1 | a1a1b | {"language":"de"} | + | a1a2 | Neos.Neos:Document | a1a | a1a2 | {"language":"de"} | + | a1b | Neos.Neos:Document | a1 | a1b | {"language":"de"} | + | a2 | Neos.Neos:Document | a | a2 | {"language":"de"} | + | b | Neos.Neos:Document | root | b | {"language":"de"} | + | b1 | Neos.Neos:Document | b | b1 | {"language":"de"} | + And the following Neos users exist: + | Id | Username | First name | Last name | Roles | + | janedoe | jane.doe | Jane | Doe | Neos.Neos:Administrator | + | johndoe | john.doe | John | Doe | Neos.Neos:RestrictedEditor,Neos.Neos:UserManager | + | editor | editor | Edward | Editor | Neos.Neos:Editor | + + Scenario: TODO + When content repository security is enabled + And the user "jane.doe" is authenticated + And the current user accesses the content graph for workspace "live" + Then an exception 'Read access denied for workspace "live": Account "jane.doe" is a Neos Administrator without explicit role for workspace "live"' should be thrown + + Scenario: TODO + Given the role MANAGER is assigned to workspace "live" for user "jane.doe" + When content repository security is enabled + And the user "jane.doe" is authenticated + And the current user accesses the content graph for workspace "live" + Then no exception should be thrown From b268f2d485588613b19c3a2fff07d015a499d17b Mon Sep 17 00:00:00 2001 From: Denny Lubitz Date: Thu, 7 Nov 2024 09:39:52 +0100 Subject: [PATCH 082/214] TASK: Improve removal of asset usages on node removal --- .../CatchUpHook/AssetUsageCatchUpHook.php | 20 ++++++++++++------- .../Service/AssetUsageIndexingService.php | 12 ----------- 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/Neos.Neos/Classes/AssetUsage/CatchUpHook/AssetUsageCatchUpHook.php b/Neos.Neos/Classes/AssetUsage/CatchUpHook/AssetUsageCatchUpHook.php index e52b55bb99c..59e2c733a99 100644 --- a/Neos.Neos/Classes/AssetUsage/CatchUpHook/AssetUsageCatchUpHook.php +++ b/Neos.Neos/Classes/AssetUsage/CatchUpHook/AssetUsageCatchUpHook.php @@ -118,17 +118,23 @@ private function removeNodes(WorkspaceName $workspaceName, NodeAggregateId $node $contentGraph = $this->contentGraphReadModel->getContentGraph($workspaceName); foreach ($dimensionSpacePoints as $dimensionSpacePoint) { + $this->assetUsageIndexingService->removeIndexForWorkspaceNameNodeAggregateIdAndDimensionSpacePoint( + $this->contentRepositoryId, + $workspaceName, + $nodeAggregateId, + $dimensionSpacePoint + ); + $subgraph = $contentGraph->getSubgraph($dimensionSpacePoint, VisibilityConstraints::withoutRestrictions()); - $node = $subgraph->findNodeById($nodeAggregateId); $descendants = $subgraph->findDescendantNodes($nodeAggregateId, FindDescendantNodesFilter::create()); - $nodes = array_merge([$node], iterator_to_array($descendants)); - - /** @var Node $node */ - foreach ($nodes as $node) { - $this->assetUsageIndexingService->removeIndexForNode( + /** @var Node $descendant */ + foreach ($descendants as $descendant) { + $this->assetUsageIndexingService->removeIndexForWorkspaceNameNodeAggregateIdAndDimensionSpacePoint( $this->contentRepositoryId, - $node + $descendant->workspaceName, + $descendant->aggregateId, + $descendant->dimensionSpacePoint ); } } diff --git a/Neos.Neos/Classes/AssetUsage/Service/AssetUsageIndexingService.php b/Neos.Neos/Classes/AssetUsage/Service/AssetUsageIndexingService.php index a49c301a24d..f284ccf34b0 100644 --- a/Neos.Neos/Classes/AssetUsage/Service/AssetUsageIndexingService.php +++ b/Neos.Neos/Classes/AssetUsage/Service/AssetUsageIndexingService.php @@ -171,18 +171,6 @@ public function removeIndexForWorkspaceNameNodeAggregateIdAndDimensionSpacePoint ); } - public function removeIndexForNode( - ContentRepositoryId $contentRepositoryId, - Node $node - ): void { - $this->removeIndexForWorkspaceNameNodeAggregateIdAndDimensionSpacePoint( - $contentRepositoryId, - $node->workspaceName, - $node->aggregateId, - $node->dimensionSpacePoint - ); - } - public function removeIndexForWorkspace( ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName From b4815ecf38567993f7d2e4320f0188a652e1b47d Mon Sep 17 00:00:00 2001 From: Denny Lubitz Date: Thu, 7 Nov 2024 10:00:39 +0100 Subject: [PATCH 083/214] TASK: Fix PHPStan --- .../src/Event/ValueObject/ExportedEvent.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/Neos.ContentRepository.Export/src/Event/ValueObject/ExportedEvent.php b/Neos.ContentRepository.Export/src/Event/ValueObject/ExportedEvent.php index 8c50097a96e..7b565a8dece 100644 --- a/Neos.ContentRepository.Export/src/Event/ValueObject/ExportedEvent.php +++ b/Neos.ContentRepository.Export/src/Event/ValueObject/ExportedEvent.php @@ -55,7 +55,6 @@ public function withIdentifier(string $identifier): self /** * @param \Closure(array): array $processor - * @return $this */ public function processPayload(\Closure $processor): self { @@ -64,7 +63,6 @@ public function processPayload(\Closure $processor): self /** * @param \Closure(array): array $processor - * @return $this */ public function processMetadata(\Closure $processor): self { From 8a6e03efbaddec4af1a5ef302e2c74008c3d3b74 Mon Sep 17 00:00:00 2001 From: Denny Lubitz Date: Thu, 7 Nov 2024 15:39:19 +0100 Subject: [PATCH 084/214] TASK: Add example configuration in settings --- Neos.ContentRepositoryRegistry/Configuration/Settings.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Neos.ContentRepositoryRegistry/Configuration/Settings.yaml b/Neos.ContentRepositoryRegistry/Configuration/Settings.yaml index 83b207597eb..2b3300c773a 100644 --- a/Neos.ContentRepositoryRegistry/Configuration/Settings.yaml +++ b/Neos.ContentRepositoryRegistry/Configuration/Settings.yaml @@ -80,3 +80,9 @@ Neos: # factoryObjectName: My\Package\Projection\SomeProjectionFactory # options: {} # catchUpHooks: {} + + # Command Hooks + # + # commandHooks: + # 'My.Package:SomeProjection': # just a name + # factoryObjectName: My\Package\CommandHook\SomeCommandHookFactory \ No newline at end of file From 8110fe3c1395937cae5ba818c8f284a1f08c4792 Mon Sep 17 00:00:00 2001 From: Denny Lubitz Date: Thu, 7 Nov 2024 15:39:44 +0100 Subject: [PATCH 085/214] TASK: Add example configuration in settings --- Neos.ContentRepositoryRegistry/Configuration/Settings.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Neos.ContentRepositoryRegistry/Configuration/Settings.yaml b/Neos.ContentRepositoryRegistry/Configuration/Settings.yaml index 2b3300c773a..ce1104753b5 100644 --- a/Neos.ContentRepositoryRegistry/Configuration/Settings.yaml +++ b/Neos.ContentRepositoryRegistry/Configuration/Settings.yaml @@ -85,4 +85,4 @@ Neos: # # commandHooks: # 'My.Package:SomeProjection': # just a name - # factoryObjectName: My\Package\CommandHook\SomeCommandHookFactory \ No newline at end of file + # factoryObjectName: My\Package\CommandHook\SomeCommandHookFactory From e4abeb19bc465ab9ad081b04c935d3c5e48c6827 Mon Sep 17 00:00:00 2001 From: Denny Lubitz Date: Thu, 7 Nov 2024 15:40:05 +0100 Subject: [PATCH 086/214] TASK: Add example configuration in settings --- Neos.ContentRepositoryRegistry/Configuration/Settings.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Neos.ContentRepositoryRegistry/Configuration/Settings.yaml b/Neos.ContentRepositoryRegistry/Configuration/Settings.yaml index ce1104753b5..060affe4041 100644 --- a/Neos.ContentRepositoryRegistry/Configuration/Settings.yaml +++ b/Neos.ContentRepositoryRegistry/Configuration/Settings.yaml @@ -84,5 +84,5 @@ Neos: # Command Hooks # # commandHooks: - # 'My.Package:SomeProjection': # just a name + # 'My.Package:SomeCommandHook': # just a name # factoryObjectName: My\Package\CommandHook\SomeCommandHookFactory From 8624c0900f01166787515e13df98ddc55e284701 Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Thu, 7 Nov 2024 17:39:57 +0100 Subject: [PATCH 087/214] Update Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php Co-authored-by: Marc Henry Schultz <85400359+mhsdesign@users.noreply.github.com> --- .../Classes/Factory/ContentRepositoryFactory.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php b/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php index c2661e6e566..2295249bbf7 100644 --- a/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php +++ b/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php @@ -56,7 +56,7 @@ public function __construct( ProjectionsAndCatchUpHooksFactory $projectionsAndCatchUpHooksFactory, private readonly UserIdProviderInterface $userIdProvider, private readonly ClockInterface $clock, - private readonly CommandHooksFactory|null $commandHooksFactory = null, + private readonly CommandHooksFactory|null $commandHooksFactory, ) { $contentDimensionZookeeper = new ContentDimensionZookeeper($contentDimensionSource); $interDimensionalVariationGraph = new InterDimensionalVariationGraph( From 9714f472198370929947fcab08e12815640db201 Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Thu, 7 Nov 2024 18:46:39 +0100 Subject: [PATCH 088/214] WIP: PoC: Serializable Commands --- .../Classes/CommandHandler/CommandBus.php | 2 +- .../CommandHandler/CommandHandlerInterface.php | 4 ++-- .../CommandHandler/SerializedCommandInterface.php | 12 ++++++++++++ .../Common/RebasableToOtherWorkspaceInterface.php | 5 +++-- .../DimensionSpaceCommandHandler.php | 5 +++-- .../Classes/Feature/NodeAggregateCommandHandler.php | 5 +++-- ...eNodeAggregateWithNodeAndSerializedProperties.php | 4 ++-- .../NodeDuplicationCommandHandler.php | 5 +++-- .../Command/SetSerializedNodeProperties.php | 5 ++--- .../Command/SetSerializedNodeReferences.php | 4 ++-- .../Classes/Feature/RebaseableCommand.php | 6 ++++-- .../Classes/Feature/WorkspaceCommandHandler.php | 5 +++-- .../CommandThatFailedDuringRebase.php | 5 +++-- 13 files changed, 43 insertions(+), 24 deletions(-) create mode 100644 Neos.ContentRepository.Core/Classes/CommandHandler/SerializedCommandInterface.php diff --git a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandBus.php b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandBus.php index 92673a00c82..3cbdec086b7 100644 --- a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandBus.php +++ b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandBus.php @@ -31,7 +31,7 @@ public function __construct( /** * @return EventsToPublish|\Generator */ - public function handle(CommandInterface $command): EventsToPublish|\Generator + public function handle(CommandInterface|SerializedCommandInterface $command): EventsToPublish|\Generator { // multiple handlers must not handle the same command foreach ($this->handlers as $handler) { diff --git a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHandlerInterface.php b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHandlerInterface.php index b36d5d3ab75..4ad1e53919b 100644 --- a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHandlerInterface.php +++ b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHandlerInterface.php @@ -15,7 +15,7 @@ */ interface CommandHandlerInterface { - public function canHandle(CommandInterface $command): bool; + public function canHandle(CommandInterface|SerializedCommandInterface $command): bool; /** * "simple" command handlers return EventsToPublish directly @@ -25,5 +25,5 @@ public function canHandle(CommandInterface $command): bool; * * @return EventsToPublish|\Generator */ - public function handle(CommandInterface $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish|\Generator; + public function handle(CommandInterface|SerializedCommandInterface $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish|\Generator; } diff --git a/Neos.ContentRepository.Core/Classes/CommandHandler/SerializedCommandInterface.php b/Neos.ContentRepository.Core/Classes/CommandHandler/SerializedCommandInterface.php new file mode 100644 index 00000000000..342de7d5d5b --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/CommandHandler/SerializedCommandInterface.php @@ -0,0 +1,12 @@ +getShortName()); } - public function handle(CommandInterface $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish + public function handle(CommandInterface|SerializedCommandInterface $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish { /** @phpstan-ignore-next-line */ return match ($command::class) { diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeAggregateCommandHandler.php b/Neos.ContentRepository.Core/Classes/Feature/NodeAggregateCommandHandler.php index eea2319dd3a..a54aefa9cae 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeAggregateCommandHandler.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeAggregateCommandHandler.php @@ -17,6 +17,7 @@ use Neos\ContentRepository\Core\CommandHandler\CommandHandlerInterface; use Neos\ContentRepository\Core\CommandHandler\CommandInterface; use Neos\ContentRepository\Core\CommandHandler\CommandHandlingDependencies; +use Neos\ContentRepository\Core\CommandHandler\SerializedCommandInterface; use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\DimensionSpace; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; @@ -86,12 +87,12 @@ public function __construct( ) { } - public function canHandle(CommandInterface $command): bool + public function canHandle(CommandInterface|SerializedCommandInterface $command): bool { return method_exists($this, 'handle' . (new \ReflectionClass($command))->getShortName()); } - public function handle(CommandInterface $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish + public function handle(CommandInterface|SerializedCommandInterface $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish { /** @phpstan-ignore-next-line */ return match ($command::class) { diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/Command/CreateNodeAggregateWithNodeAndSerializedProperties.php b/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/Command/CreateNodeAggregateWithNodeAndSerializedProperties.php index 958b1a30590..239120db1f0 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/Command/CreateNodeAggregateWithNodeAndSerializedProperties.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/Command/CreateNodeAggregateWithNodeAndSerializedProperties.php @@ -14,7 +14,7 @@ namespace Neos\ContentRepository\Core\Feature\NodeCreation\Command; -use Neos\ContentRepository\Core\CommandHandler\CommandInterface; +use Neos\ContentRepository\Core\CommandHandler\SerializedCommandInterface; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; use Neos\ContentRepository\Core\Feature\Common\MatchableWithNodeIdToPublishOrDiscardInterface; use Neos\ContentRepository\Core\Feature\Common\RebasableToOtherWorkspaceInterface; @@ -34,7 +34,7 @@ * @internal implementation detail, use {@see CreateNodeAggregateWithNode} instead. */ final readonly class CreateNodeAggregateWithNodeAndSerializedProperties implements - CommandInterface, + SerializedCommandInterface, \JsonSerializable, MatchableWithNodeIdToPublishOrDiscardInterface, RebasableToOtherWorkspaceInterface diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/NodeDuplicationCommandHandler.php b/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/NodeDuplicationCommandHandler.php index edffe38ad5c..31a5a897af8 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/NodeDuplicationCommandHandler.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/NodeDuplicationCommandHandler.php @@ -15,6 +15,7 @@ namespace Neos\ContentRepository\Core\Feature\NodeDuplication; use Neos\ContentRepository\Core\CommandHandler\CommandHandlingDependencies; +use Neos\ContentRepository\Core\CommandHandler\SerializedCommandInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphInterface; use Neos\ContentRepository\Core\CommandHandler\CommandHandlerInterface; use Neos\ContentRepository\Core\CommandHandler\CommandInterface; @@ -64,12 +65,12 @@ protected function getAllowedDimensionSubspace(): DimensionSpacePointSet return $this->contentDimensionZookeeper->getAllowedDimensionSubspace(); } - public function canHandle(CommandInterface $command): bool + public function canHandle(CommandInterface|SerializedCommandInterface $command): bool { return method_exists($this, 'handle' . (new \ReflectionClass($command))->getShortName()); } - public function handle(CommandInterface $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish + public function handle(CommandInterface|SerializedCommandInterface $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish { /** @phpstan-ignore-next-line */ return match ($command::class) { diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Command/SetSerializedNodeProperties.php b/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Command/SetSerializedNodeProperties.php index a2d684f6035..f5d382f0387 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Command/SetSerializedNodeProperties.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Command/SetSerializedNodeProperties.php @@ -14,7 +14,7 @@ namespace Neos\ContentRepository\Core\Feature\NodeModification\Command; -use Neos\ContentRepository\Core\CommandHandler\CommandInterface; +use Neos\ContentRepository\Core\CommandHandler\SerializedCommandInterface; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; use Neos\ContentRepository\Core\Feature\Common\MatchableWithNodeIdToPublishOrDiscardInterface; use Neos\ContentRepository\Core\Feature\Common\RebasableToOtherWorkspaceInterface; @@ -22,7 +22,6 @@ use Neos\ContentRepository\Core\Feature\WorkspacePublication\Dto\NodeIdToPublishOrDiscard; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Node\PropertyNames; -use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; /** @@ -33,7 +32,7 @@ * @internal implementation detail, use {@see SetNodeProperties} instead. */ final readonly class SetSerializedNodeProperties implements - CommandInterface, + SerializedCommandInterface, \JsonSerializable, MatchableWithNodeIdToPublishOrDiscardInterface, RebasableToOtherWorkspaceInterface diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Command/SetSerializedNodeReferences.php b/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Command/SetSerializedNodeReferences.php index ae96cc5af13..dd075e4e547 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Command/SetSerializedNodeReferences.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Command/SetSerializedNodeReferences.php @@ -14,7 +14,7 @@ namespace Neos\ContentRepository\Core\Feature\NodeReferencing\Command; -use Neos\ContentRepository\Core\CommandHandler\CommandInterface; +use Neos\ContentRepository\Core\CommandHandler\SerializedCommandInterface; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; use Neos\ContentRepository\Core\Feature\Common\MatchableWithNodeIdToPublishOrDiscardInterface; use Neos\ContentRepository\Core\Feature\Common\RebasableToOtherWorkspaceInterface; @@ -31,7 +31,7 @@ * @internal implementation detail, use {@see SetNodeReferences} instead. */ final readonly class SetSerializedNodeReferences implements - CommandInterface, + SerializedCommandInterface, \JsonSerializable, MatchableWithNodeIdToPublishOrDiscardInterface, RebasableToOtherWorkspaceInterface diff --git a/Neos.ContentRepository.Core/Classes/Feature/RebaseableCommand.php b/Neos.ContentRepository.Core/Classes/Feature/RebaseableCommand.php index 210f91b8b8a..93aa84c89cc 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/RebaseableCommand.php +++ b/Neos.ContentRepository.Core/Classes/Feature/RebaseableCommand.php @@ -4,6 +4,8 @@ namespace Neos\ContentRepository\Core\Feature; +use Neos\ContentRepository\Core\CommandHandler\CommandInterface; +use Neos\ContentRepository\Core\CommandHandler\SerializedCommandInterface; use Neos\ContentRepository\Core\EventStore\DecoratedEvent; use Neos\ContentRepository\Core\EventStore\Events; use Neos\ContentRepository\Core\EventStore\InitiatingEventMetadata; @@ -19,7 +21,7 @@ final readonly class RebaseableCommand { public function __construct( - public RebasableToOtherWorkspaceInterface $originalCommand, + public (RebasableToOtherWorkspaceInterface&CommandInterface)|(RebasableToOtherWorkspaceInterface&SerializedCommandInterface) $originalCommand, public EventMetadata $initiatingMetaData, public SequenceNumber $originalSequenceNumber ) { @@ -42,7 +44,7 @@ public static function extractFromEventMetaData(EventMetadata $eventMetadata, Se ), 1547815341); } /** @var class-string $commandToRebaseClass */ - /** @var RebasableToOtherWorkspaceInterface $commandInstance */ + /** @var (RebasableToOtherWorkspaceInterface&CommandInterface)|(RebasableToOtherWorkspaceInterface&SerializedCommandInterface) $commandInstance */ $commandInstance = $commandToRebaseClass::fromArray($commandToRebasePayload); return new self( $commandInstance, diff --git a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php index 20143536272..dc8c12c7dca 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php +++ b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php @@ -18,6 +18,7 @@ use Neos\ContentRepository\Core\CommandHandler\CommandHandlingDependencies; use Neos\ContentRepository\Core\CommandHandler\CommandInterface; use Neos\ContentRepository\Core\CommandHandler\CommandSimulatorFactory; +use Neos\ContentRepository\Core\CommandHandler\SerializedCommandInterface; use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\EventStore\DecoratedEvent; use Neos\ContentRepository\Core\EventStore\EventInterface; @@ -82,12 +83,12 @@ public function __construct( ) { } - public function canHandle(CommandInterface $command): bool + public function canHandle(CommandInterface|SerializedCommandInterface $command): bool { return method_exists($this, 'handle' . (new \ReflectionClass($command))->getShortName()); } - public function handle(CommandInterface $command, CommandHandlingDependencies $commandHandlingDependencies): \Generator + public function handle(CommandInterface|SerializedCommandInterface $command, CommandHandlingDependencies $commandHandlingDependencies): \Generator { /** @phpstan-ignore-next-line */ return match ($command::class) { diff --git a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/CommandThatFailedDuringRebase.php b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/CommandThatFailedDuringRebase.php index bcfd9627256..f8b4d579732 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/CommandThatFailedDuringRebase.php +++ b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/CommandThatFailedDuringRebase.php @@ -15,6 +15,7 @@ namespace Neos\ContentRepository\Core\Feature\WorkspaceRebase; use Neos\ContentRepository\Core\CommandHandler\CommandInterface; +use Neos\ContentRepository\Core\CommandHandler\SerializedCommandInterface; use Neos\EventStore\Model\Event\SequenceNumber; /** @@ -23,12 +24,12 @@ final readonly class CommandThatFailedDuringRebase { /** - * @param CommandInterface $command the command that failed + * @param CommandInterface|SerializedCommandInterface $command the command that failed * @param \Throwable $exception how the command failed * @param SequenceNumber $sequenceNumber the event store sequence number of the event containing the command to be rebased */ public function __construct( - public CommandInterface $command, + public CommandInterface|SerializedCommandInterface $command, public \Throwable $exception, private SequenceNumber $sequenceNumber, ) { From adf7b38f26966ae5440578102e2700d0d3b7be33 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Fri, 8 Nov 2024 10:46:57 +0100 Subject: [PATCH 089/214] TASK: Remove automatic schema creation from import and redirect user to `./flow cr:setup` `./flow doctrine:migrate` does not work without hacks in the same process and its also an expensive task and should not delay the import and obscure errors and swallow output if done as part of the import. --- ...ContentRepositorySetupProcessorFactory.php | 22 ----------- .../ContentRepositorySetupProcessor.php | 28 -------------- .../Import/DoctrineMigrateProcessor.php | 32 ---------------- .../Domain/Service/SiteImportService.php | 37 ++++++++++++++++--- 4 files changed, 32 insertions(+), 87 deletions(-) delete mode 100644 Neos.ContentRepository.Export/src/Factory/ContentRepositorySetupProcessorFactory.php delete mode 100644 Neos.ContentRepository.Export/src/Processors/ContentRepositorySetupProcessor.php delete mode 100644 Neos.Neos/Classes/Domain/Import/DoctrineMigrateProcessor.php diff --git a/Neos.ContentRepository.Export/src/Factory/ContentRepositorySetupProcessorFactory.php b/Neos.ContentRepository.Export/src/Factory/ContentRepositorySetupProcessorFactory.php deleted file mode 100644 index 945ef993da7..00000000000 --- a/Neos.ContentRepository.Export/src/Factory/ContentRepositorySetupProcessorFactory.php +++ /dev/null @@ -1,22 +0,0 @@ - - */ -final readonly class ContentRepositorySetupProcessorFactory implements ContentRepositoryServiceFactoryInterface -{ - public function build(ContentRepositoryServiceFactoryDependencies $serviceFactoryDependencies): ContentRepositorySetupProcessor - { - return new ContentRepositorySetupProcessor( - $serviceFactoryDependencies->contentRepository, - ); - } -} diff --git a/Neos.ContentRepository.Export/src/Processors/ContentRepositorySetupProcessor.php b/Neos.ContentRepository.Export/src/Processors/ContentRepositorySetupProcessor.php deleted file mode 100644 index 57d322921da..00000000000 --- a/Neos.ContentRepository.Export/src/Processors/ContentRepositorySetupProcessor.php +++ /dev/null @@ -1,28 +0,0 @@ -dispatch(Severity::NOTICE, "Setting up content repository \"{$this->contentRepository->id->value}\""); - $this->contentRepository->setUp(); - } -} diff --git a/Neos.Neos/Classes/Domain/Import/DoctrineMigrateProcessor.php b/Neos.Neos/Classes/Domain/Import/DoctrineMigrateProcessor.php deleted file mode 100644 index 632cdc8a6b5..00000000000 --- a/Neos.Neos/Classes/Domain/Import/DoctrineMigrateProcessor.php +++ /dev/null @@ -1,32 +0,0 @@ -doctrineService->executeMigrations(); - } -} diff --git a/Neos.Neos/Classes/Domain/Service/SiteImportService.php b/Neos.Neos/Classes/Domain/Service/SiteImportService.php index 1a65686f912..741424a02e2 100644 --- a/Neos.Neos/Classes/Domain/Service/SiteImportService.php +++ b/Neos.Neos/Classes/Domain/Service/SiteImportService.php @@ -14,11 +14,12 @@ namespace Neos\Neos\Domain\Service; +use Doctrine\DBAL\Exception as DBALException; use League\Flysystem\Filesystem; use League\Flysystem\Local\LocalFilesystemAdapter; +use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; -use Neos\ContentRepository\Export\Factory\ContentRepositorySetupProcessorFactory; use Neos\ContentRepository\Export\Factory\EventStoreImportProcessorFactory; use Neos\ContentRepository\Export\ProcessingContext; use Neos\ContentRepository\Export\ProcessorInterface; @@ -34,7 +35,6 @@ use Neos\Flow\ResourceManagement\ResourceManager; use Neos\Flow\ResourceManagement\ResourceRepository; use Neos\Media\Domain\Repository\AssetRepository; -use Neos\Neos\Domain\Import\DoctrineMigrateProcessor; use Neos\Neos\Domain\Import\LiveWorkspaceCreationProcessor; use Neos\Neos\Domain\Import\SiteCreationProcessor; use Neos\Neos\Domain\Repository\DomainRepository; @@ -65,13 +65,15 @@ public function importFromPath(ContentRepositoryId $contentRepositoryId, string if (!is_dir($path)) { throw new \InvalidArgumentException(sprintf('Path "%s" is not a directory', $path), 1729593802); } + $contentRepository = $this->contentRepositoryRegistry->get($contentRepositoryId); + + $this->requireDataBaseSchemaToBeSetup(); + $this->requireContentRepositoryToBeSetup($contentRepository); + $filesystem = new Filesystem(new LocalFilesystemAdapter($path)); $context = new ProcessingContext($filesystem, $onMessage); - $contentRepository = $this->contentRepositoryRegistry->get($contentRepositoryId); $processors = Processors::fromArray([ - 'Run doctrine migrations' => new DoctrineMigrateProcessor($this->doctrineService), - 'Setup content repository' => $this->contentRepositoryRegistry->buildService($contentRepositoryId, new ContentRepositorySetupProcessorFactory()), 'Create Live workspace' => new LiveWorkspaceCreationProcessor($contentRepository, $this->workspaceService), 'Create Neos sites' => new SiteCreationProcessor($this->siteRepository, $this->domainRepository, $this->persistenceManager), 'Import events' => $this->contentRepositoryRegistry->buildService($contentRepositoryId, new EventStoreImportProcessorFactory(WorkspaceName::forLive(), keepEventIds: true)), @@ -84,4 +86,29 @@ public function importFromPath(ContentRepositoryId $contentRepositoryId, string $processor->run($context); } } + + private function requireContentRepositoryToBeSetup(ContentRepository $contentRepository): void + { + $status = $contentRepository->status(); + if (!$status->isOk()) { + throw new \RuntimeException(sprintf('Content repository %s is not setup correctly, please run `./flow cr:setup`', $contentRepository->id->value)); + } + } + + private function requireDataBaseSchemaToBeSetup(): void + { + try { + [ + 'new' => $_newMigrationCount, + 'executed' => $executedMigrationCount, + 'available' => $availableMigrationCount + ] = $this->doctrineService->getMigrationStatus(); + } catch (DBALException | \PDOException) { + throw new \RuntimeException('Not database connected. Please check your database connection settings or run `./flow setup` for further information.', 1684075689386); + } + + if ($executedMigrationCount === 0 && $availableMigrationCount > 0) { + throw new \RuntimeException('No doctrine migrations have been executed. Please run `./flow doctrine:migrate`'); + } + } } From b701b17d1b7a7584bc7a708a4ab3c3f0b32436fe Mon Sep 17 00:00:00 2001 From: Denny Lubitz Date: Fri, 8 Nov 2024 14:16:47 +0100 Subject: [PATCH 090/214] TASK: Post-merge fix of tests --- .../Tests/Behavior/Bootstrap/FeatureContext.php | 7 +++---- .../Behavior/Features/RootNodeTypeMapping.feature | 12 ++++++------ 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Bootstrap/FeatureContext.php b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Bootstrap/FeatureContext.php index 18a49dbdee0..1812c6cbeb7 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Bootstrap/FeatureContext.php +++ b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Bootstrap/FeatureContext.php @@ -86,13 +86,12 @@ public function iHaveTheFollowingNodeDataRows(TableNode $nodeDataRows): void } /** - * @When /^I run the event migration for content stream (.*) with rootNode mapping (.*)$/ + * @When /^I run the event migration with rootNode mapping (.*)$/ */ - public function iRunTheEventMigrationForContentStreamWithRootnodeMapping(string $contentStream = null, string $rootNodeMapping): void + public function iRunTheEventMigrationWithRootnodeMapping(string $rootNodeMapping): void { - $contentStream = trim($contentStream, '"'); $rootNodeTypeMapping = RootNodeTypeMapping::fromArray(json_decode($rootNodeMapping, true)); - $this->iRunTheEventMigration($contentStream, $rootNodeTypeMapping); + $this->iRunTheEventMigration(null, $rootNodeTypeMapping); } /** diff --git a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/RootNodeTypeMapping.feature b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/RootNodeTypeMapping.feature index 2834b27a237..6fd919dbf24 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/RootNodeTypeMapping.feature +++ b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/RootNodeTypeMapping.feature @@ -24,7 +24,7 @@ Feature: Simple migrations without content dimensions but other root nodetype na | site-node-id | /sites/test-site | Some.Package:Homepage | {"text": "foo"} | | test-root-node-id | /test | unstructured | | | test-node-id | /test/test-site | Some.Package:Homepage | {"text": "foo"} | - And I run the event migration for content stream "cs-id" + And I run the event migration Then I expect the following errors to be logged | Failed to find parent node for node with id "test-root-node-id" and dimensions: []. Please ensure that the new content repository has a valid content dimension configuration. Also note that the old CR can sometimes have orphaned nodes. | | Failed to find parent node for node with id "test-node-id" and dimensions: []. Please ensure that the new content repository has a valid content dimension configuration. Also note that the old CR can sometimes have orphaned nodes. | @@ -37,10 +37,10 @@ Feature: Simple migrations without content dimensions but other root nodetype na | site-node-id | /sites/test-site | Some.Package:Homepage | {"text": "foo"} | | test-root-node-id | /test | unstructured | | | test-node-id | /test/test-site | Some.Package:Homepage | {"text": "foo"} | - And I run the event migration for content stream "cs-id" with rootNode mapping {"/sites": "Neos.Neos:Sites", "/test": "Neos.ContentRepository.LegacyNodeMigration:TestRoot"} + And I run the event migration with rootNode mapping {"/sites": "Neos.Neos:Sites", "/test": "Neos.ContentRepository.LegacyNodeMigration:TestRoot"} Then I expect the following events to be exported | Type | Payload | - | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | - | NodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} | - | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "test-root-node-id", "nodeTypeName": "Neos.ContentRepository.LegacyNodeMigration:TestRoot", "nodeAggregateClassification": "root"} | - | NodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "test-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "test-root-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} | + | RootNodeAggregateWithNodeWasCreated | {"nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | + | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} | + | RootNodeAggregateWithNodeWasCreated | {"nodeAggregateId": "test-root-node-id", "nodeTypeName": "Neos.ContentRepository.LegacyNodeMigration:TestRoot", "nodeAggregateClassification": "root"} | + | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "test-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "test-root-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} | From 2efbf3a5d72e2c60ec2d05e6580bc1cacf8b5a7e Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Fri, 8 Nov 2024 14:26:48 +0100 Subject: [PATCH 091/214] moar better tests --- ...ricCommandExecutionAndEventPublication.php | 15 ++- .../ContentRepositorySecurityTrait.php | 16 ++-- .../Features/Bootstrap/ExceptionsTrait.php | 12 ++- .../Features/Bootstrap/UserServiceTrait.php | 2 +- .../Bootstrap/WorkspaceServiceTrait.php | 9 +- .../Security/EditNodePrivilege.feature | 23 +++-- .../Security/ReadNodePrivilege.feature | 8 +- .../Security/WorkspaceAccess.feature | 96 +++++++++++++------ .../WorkspaceService.feature | 60 ++++++++---- 9 files changed, 162 insertions(+), 79 deletions(-) diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php index dd899ee4e24..5ecc9d7d546 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php @@ -14,6 +14,7 @@ namespace Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap; +use Behat\Gherkin\Node\PyStringNode; use Behat\Gherkin\Node\TableNode; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; use Neos\ContentRepository\Core\EventStore\EventNormalizer; @@ -147,14 +148,17 @@ protected function publishEvent(string $eventType, StreamName $streamName, array } /** - * @Then /^the last command should have thrown an exception of type "([^"]*)"(?: with code (\d*))?$/ + * @Then the last command should have thrown an exception of type :shortExceptionName with code :expectedCode and message: + * @Then the last command should have thrown an exception of type :shortExceptionName with code :expectedCode + * @Then the last command should have thrown an exception of type :shortExceptionName with message: + * @Then the last command should have thrown an exception of type :shortExceptionName */ - public function theLastCommandShouldHaveThrown(string $shortExceptionName, ?int $expectedCode = null): void + public function theLastCommandShouldHaveThrown(string $shortExceptionName, ?int $expectedCode = null, PyStringNode $expectedMessage = null): void { Assert::assertNotNull($this->lastCommandException, 'Command did not throw exception'); $lastCommandExceptionShortName = (new \ReflectionClass($this->lastCommandException))->getShortName(); - Assert::assertSame($shortExceptionName, $lastCommandExceptionShortName, sprintf('Actual exception: %s (%s): %s', get_class($this->lastCommandException), $this->lastCommandException->getCode(), $this->lastCommandException->getMessage())); - if (!is_null($expectedCode)) { + Assert::assertSame($shortExceptionName, $lastCommandExceptionShortName, sprintf('Actual exception: %s (%s): %s', get_debug_type($this->lastCommandException), $this->lastCommandException->getCode(), $this->lastCommandException->getMessage())); + if ($expectedCode !== null) { Assert::assertSame($expectedCode, $this->lastCommandException->getCode(), sprintf( 'Expected exception code %s, got exception code %s instead; Message: %s', $expectedCode, @@ -162,6 +166,9 @@ public function theLastCommandShouldHaveThrown(string $shortExceptionName, ?int $this->lastCommandException->getMessage() )); } + if ($expectedMessage !== null) { + Assert::assertSame($expectedMessage->getRaw(), $this->lastCommandException->getMessage()); + } } /** diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/ContentRepositorySecurityTrait.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/ContentRepositorySecurityTrait.php index 76c71de0e68..c8bc89a542e 100644 --- a/Neos.Neos/Tests/Behavior/Features/Bootstrap/ContentRepositorySecurityTrait.php +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/ContentRepositorySecurityTrait.php @@ -169,9 +169,9 @@ public function theFollowingAdditionalPoliciesAreConfigured(PyStringNode $polici } /** - * @When the user :username is authenticated + * @When I am authenticated as :username */ - public function theUserIsAuthenticated(string $username): void + public function iAmAuthenticatedAs(string $username): void { $user = $this->getObject(UserService::class)->getUser($username); $this->authenticateAccount($user->getAccounts()->first()); @@ -179,17 +179,17 @@ public function theUserIsAuthenticated(string $username): void /** - * @When the current user accesses the content graph for workspace :workspaceName + * @When I access the content graph for workspace :workspaceName */ - public function theCurrentUserAccessesTheContentGraphForWorkspace(string $workspaceName): void + public function iAccessesTheContentGraphForWorkspace(string $workspaceName): void { $this->tryCatchingExceptions(fn () => $this->currentContentRepository->getContentGraph(WorkspaceName::fromString($workspaceName))); } /** - * @Then The current user should not be able to read node :nodeAggregateId + * @Then I should not be able to read node :nodeAggregateId */ - public function theCurrentUserShouldNotBeAbleToReadNode(string $nodeAggregateId): void + public function iShouldNotBeAbleToReadNode(string $nodeAggregateId): void { $node = $this->currentContentRepository->getContentSubgraph($this->currentWorkspaceName, $this->currentDimensionSpacePoint)->findNodeById(NodeAggregateId::fromString($nodeAggregateId)); if ($node !== null) { @@ -198,9 +198,9 @@ public function theCurrentUserShouldNotBeAbleToReadNode(string $nodeAggregateId) } /** - * @Then The current user should be able to read node :nodeAggregateId + * @Then I should be able to read node :nodeAggregateId */ - public function theCurrentUserShouldBeAbleToReadNode(string $nodeAggregateId): void + public function iShouldBeAbleToReadNode(string $nodeAggregateId): void { $node = $this->currentContentRepository->getContentSubgraph($this->currentWorkspaceName, $this->currentDimensionSpacePoint)->findNodeById(NodeAggregateId::fromString($nodeAggregateId)); if ($node === null) { diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/ExceptionsTrait.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/ExceptionsTrait.php index 228d4594bdc..5a87e107249 100644 --- a/Neos.Neos/Tests/Behavior/Features/Bootstrap/ExceptionsTrait.php +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/ExceptionsTrait.php @@ -12,6 +12,7 @@ * source code. */ +use Behat\Gherkin\Node\PyStringNode; use PHPUnit\Framework\Assert; /** @@ -37,12 +38,17 @@ private function tryCatchingExceptions(\Closure $callback): mixed } /** - * @Then an exception :exceptionMessage should be thrown + * @Then an exception of type :expectedShortExceptionName should be thrown with message: + * @Then an exception of type :expectedShortExceptionName should be thrown */ - public function anExceptionShouldBeThrown(string $exceptionMessage): void + public function anExceptionShouldBeThrown(string $expectedShortExceptionName, PyStringNode $expectedExceptionMessage = null): void { Assert::assertNotNull($this->lastCaughtException, 'Expected an exception but none was thrown'); - Assert::assertSame($exceptionMessage, $this->lastCaughtException->getMessage()); + $lastCaughtExceptionShortName = (new \ReflectionClass($this->lastCaughtException))->getShortName(); + Assert::assertSame($expectedShortExceptionName, $lastCaughtExceptionShortName, sprintf('Actual exception: %s (%s): %s', get_debug_type($this->lastCaughtException), $this->lastCaughtException->getCode(), $this->lastCaughtException->getMessage())); + if ($expectedExceptionMessage !== null) { + Assert::assertSame($expectedExceptionMessage->getRaw(), $this->lastCaughtException->getMessage()); + } $this->lastCaughtException = null; } diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/UserServiceTrait.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/UserServiceTrait.php index 4c4118f8f57..da251d624fb 100644 --- a/Neos.Neos/Tests/Behavior/Features/Bootstrap/UserServiceTrait.php +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/UserServiceTrait.php @@ -65,7 +65,7 @@ public function theFollowingNeosUsersExist(TableNode $usersTable): void username: $userData['Username'], firstName: $userData['First name'] ?? null, lastName: $userData['Last name'] ?? null, - roleIdentifiers: isset($userData['Roles']) ? explode(',', $userData['Roles']) : null, + roleIdentifiers: !empty($userData['Roles']) ? explode(',', $userData['Roles']) : null, id: $userData['Id'] ?? null, ); } diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/WorkspaceServiceTrait.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/WorkspaceServiceTrait.php index 28fbdba1a00..5aeae6d712b 100644 --- a/Neos.Neos/Tests/Behavior/Features/Bootstrap/WorkspaceServiceTrait.php +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/WorkspaceServiceTrait.php @@ -65,17 +65,18 @@ public function theRootWorkspaceIsCreated(string $workspaceName, string $title = } /** - * @When the personal workspace :workspaceName is created with the target workspace :targetWorkspace for user :ownerUserId + * @When the personal workspace :workspaceName is created with the target workspace :targetWorkspace for user :username */ - public function thePersonalWorkspaceIsCreatedWithTheTargetWorkspace(string $workspaceName, string $targetWorkspace, string $ownerUserId): void + public function thePersonalWorkspaceIsCreatedWithTheTargetWorkspace(string $workspaceName, string $targetWorkspace, string $username): void { + $ownerUserId = $this->userIdForUsername($username); $this->tryCatchingExceptions(fn () => $this->getObject(WorkspaceService::class)->createPersonalWorkspace( $this->currentContentRepository->id, WorkspaceName::fromString($workspaceName), WorkspaceTitle::fromString($workspaceName), WorkspaceDescription::fromString(''), WorkspaceName::fromString($targetWorkspace), - UserId::fromString($ownerUserId), + $ownerUserId, )); } @@ -258,7 +259,7 @@ public function theNeosUserShouldHaveNoPermissionsForWorkspace(string $username, private function userIdForUsername(string $username): UserId { $user = $this->getObject(UserService::class)->getUser($username); - Assert::assertNotNull($user); + Assert::assertNotNull($user, sprintf('The user "%s" does not exist', $username)); return $user->getId(); } } diff --git a/Neos.Neos/Tests/Behavior/Features/ContentRepository/Security/EditNodePrivilege.feature b/Neos.Neos/Tests/Behavior/Features/ContentRepository/Security/EditNodePrivilege.feature index 3f86ee05114..d90d4448763 100644 --- a/Neos.Neos/Tests/Behavior/Features/ContentRepository/Security/EditNodePrivilege.feature +++ b/Neos.Neos/Tests/Behavior/Features/ContentRepository/Security/EditNodePrivilege.feature @@ -18,7 +18,6 @@ Feature: EditNodePrivilege related features """ And using identifier "default", I define a content repository And I am in content repository "default" - And I am user identified by "initiating-user-identifier" And the command CreateRootWorkspace is executed with payload: | Key | Value | | workspaceName | "live" | @@ -42,10 +41,10 @@ Feature: EditNodePrivilege related features | b | Neos.Neos:Document | root | b | {"language":"de"} | | b1 | Neos.Neos:Document | b | b1 | {"language":"de"} | And the following Neos users exist: - | Id | Username | First name | Last name | Roles | - | janedoe | jane.doe | Jane | Doe | Neos.Neos:Administrator | - | johndoe | john.doe | John | Doe | Neos.Neos:RestrictedEditor,Neos.Neos:UserManager | - | editor | editor | Edward | Editor | Neos.Neos:Editor | + | Username | First name | Last name | Roles | + | jane.doe | Jane | Doe | Neos.Neos:Administrator | + | john.doe | John | Doe | Neos.Neos:RestrictedEditor,Neos.Neos:UserManager | + | editor | Edward | Editor | Neos.Neos:Editor | Scenario: TODO Given I am in workspace "live" @@ -57,9 +56,19 @@ Feature: EditNodePrivilege related features | tag | "blog" | And the role MANAGER is assigned to workspace "live" for user "jane.doe" When content repository security is enabled - And the user "jane.doe" is authenticated + And I am authenticated as "jane.doe" When the command DisableNodeAggregate is executed with payload and exceptions are caught: | Key | Value | | nodeAggregateId | "a1a" | | nodeVariantSelectionStrategy | "allVariants" | - Then the last command should have thrown an exception of type "AccessDenied" + Then the last command should have thrown an exception of type "AccessDenied" with code 1729086686 +# Then the last command should have thrown an exception of type "AccessDenied" with message: +# """ +# Command "Neos\ContentRepository\Core\Feature\NodeDisabling\Command\DisableNodeAggregate" was denied: No edit permissions for node "a1a" in workspace "live": Evaluated following 2 privilege target(s): +# "Neos.Neos:ReadBlog": ABSTAIN +# "Neos.Neos:ReadBlog": GRANT +# (1 granted, 0 denied, 1 abstained) +# Evaluated following 1 privilege target(s): +# "Neos.Neos:EditBlog": ABSTAIN +# (0 granted, 0 denied, 1 abstained) +# """ diff --git a/Neos.Neos/Tests/Behavior/Features/ContentRepository/Security/ReadNodePrivilege.feature b/Neos.Neos/Tests/Behavior/Features/ContentRepository/Security/ReadNodePrivilege.feature index 886f619599c..f30fc37739f 100644 --- a/Neos.Neos/Tests/Behavior/Features/ContentRepository/Security/ReadNodePrivilege.feature +++ b/Neos.Neos/Tests/Behavior/Features/ContentRepository/Security/ReadNodePrivilege.feature @@ -62,7 +62,7 @@ Feature: ReadNodePrivilege related features | tag | "blog" | And the role VIEWER is assigned to workspace "live" for group "Neos.Flow:Everybody" When content repository security is enabled - And the user "john.doe" is authenticated - Then The current user should not be able to read node "a1" - When the user "jane.doe" is authenticated - Then The current user should be able to read node "a1" + And I am authenticated as "john.doe" + Then I should not be able to read node "a1" + When I am authenticated as "jane.doe" + Then I should be able to read node "a1" diff --git a/Neos.Neos/Tests/Behavior/Features/ContentRepository/Security/WorkspaceAccess.feature b/Neos.Neos/Tests/Behavior/Features/ContentRepository/Security/WorkspaceAccess.feature index eaf3d7ea4dd..473901a678f 100644 --- a/Neos.Neos/Tests/Behavior/Features/ContentRepository/Security/WorkspaceAccess.feature +++ b/Neos.Neos/Tests/Behavior/Features/ContentRepository/Security/WorkspaceAccess.feature @@ -29,38 +29,80 @@ Feature: Workspace access related features | workspaceName | "live" | | newContentStreamId | "cs-identifier" | And I am in workspace "live" and dimension space point {} - And the command CreateRootNodeAggregateWithNode is executed with payload: - | Key | Value | - | nodeAggregateId | "root" | - | nodeTypeName | "Neos.ContentRepository:Root" | - And the following CreateNodeAggregateWithNode commands are executed: - | nodeAggregateId | nodeTypeName | parentNodeAggregateId | nodeName | originDimensionSpacePoint | - | a | Neos.Neos:Document | root | a | {"language":"mul"} | - | a1 | Neos.Neos:Document | a | a1 | {"language":"de"} | - | a1a | Neos.Neos:Document | a1 | a1a | {"language":"de"} | - | a1a1 | Neos.Neos:Document | a1a | a1a1 | {"language":"de"} | - | a1a1a | Neos.Neos:Document | a1a1 | a1a1a | {"language":"de"} | - | a1a1b | Neos.Neos:Document | a1a1 | a1a1b | {"language":"de"} | - | a1a2 | Neos.Neos:Document | a1a | a1a2 | {"language":"de"} | - | a1b | Neos.Neos:Document | a1 | a1b | {"language":"de"} | - | a2 | Neos.Neos:Document | a | a2 | {"language":"de"} | - | b | Neos.Neos:Document | root | b | {"language":"de"} | - | b1 | Neos.Neos:Document | b | b1 | {"language":"de"} | And the following Neos users exist: - | Id | Username | First name | Last name | Roles | - | janedoe | jane.doe | Jane | Doe | Neos.Neos:Administrator | - | johndoe | john.doe | John | Doe | Neos.Neos:RestrictedEditor,Neos.Neos:UserManager | - | editor | editor | Edward | Editor | Neos.Neos:Editor | + | Username | Roles | + | admin | Neos.Neos:Administrator | + | editor | Neos.Neos:Editor | + | restricted_editor | Neos.Neos:RestrictedEditor,Neos.Neos:UserManager | + | no_editor | | Scenario: TODO When content repository security is enabled - And the user "jane.doe" is authenticated - And the current user accesses the content graph for workspace "live" - Then an exception 'Read access denied for workspace "live": Account "jane.doe" is a Neos Administrator without explicit role for workspace "live"' should be thrown + And I am authenticated as "admin" + And I access the content graph for workspace "live" + Then an exception of type "AccessDenied" should be thrown with message: + """ + Read access denied for workspace "live": Account "admin" is a Neos Administrator without explicit role for workspace "live" + """ Scenario: TODO - Given the role MANAGER is assigned to workspace "live" for user "jane.doe" + Given the role MANAGER is assigned to workspace "live" for user "admin" + When content repository security is enabled + And I am authenticated as "admin" + And I access the content graph for workspace "live" + Then no exception should be thrown + + Scenario Outline: Accessing content graph for explicitly assigned workspace role to the authenticated user + Given the role is assigned to workspace "live" for user "" + When content repository security is enabled + And I am authenticated as "" + And I access the content graph for workspace "live" + Then no exception should be thrown + + Examples: + | user | workspace role | + | admin | VIEWER | + | editor | COLLABORATOR | + | editor | VIEWER | + | restricted_editor | MANAGER | + | restricted_editor | VIEWER | + + Scenario Outline: Accessing content graph for workspace role assigned to group of the authenticated user + Given the role is assigned to workspace "live" for group "" When content repository security is enabled - And the user "jane.doe" is authenticated - And the current user accesses the content graph for workspace "live" + And I am authenticated as "" + And I access the content graph for workspace "live" Then no exception should be thrown + + Examples: + | user | group | workspace role | + | admin | Neos.Neos:Editor | COLLABORATOR | + | editor | Neos.Neos:Editor | COLLABORATOR | + | restricted_editor | Neos.Neos:RestrictedEditor | VIEWER | + | no_editor | Neos.Flow:Everybody | VIEWER | + + Scenario Outline: Accessing content graph for workspace role assigned to group the authenticated user is not part of + Given the role is assigned to workspace "live" for group "" + When content repository security is enabled + And I am authenticated as "" + And I access the content graph for workspace "live" + Then an exception of type "AccessDenied" should be thrown + + Examples: + | user | group | workspace role | + | admin | Neos.Flow:Anonymous | COLLABORATOR | + | editor | Neos.Neos:Administrator | MANAGER | + | restricted_editor | Neos.Neos:Editor | VIEWER | + + Scenario Outline: Accessing content graph for workspace that is owned by the authenticated user + Given the personal workspace "user-workspace" is created with the target workspace "live" for user "" + When content repository security is enabled + And I am authenticated as "" + And I access the content graph for workspace "user-workspace" + Then no exception should be thrown + + Examples: + | user | + | admin | + | editor | + | restricted_editor | diff --git a/Neos.Neos/Tests/Behavior/Features/ContentRepository/WorkspaceService.feature b/Neos.Neos/Tests/Behavior/Features/ContentRepository/WorkspaceService.feature index ea0a90995a2..22b3ff17e04 100644 --- a/Neos.Neos/Tests/Behavior/Features/ContentRepository/WorkspaceService.feature +++ b/Neos.Neos/Tests/Behavior/Features/ContentRepository/WorkspaceService.feature @@ -29,12 +29,15 @@ Feature: Neos WorkspaceService related features Scenario: Create root workspace with a name that exceeds the workspace name max length When the root workspace "some-name-that-exceeds-the-max-allowed-length" is created - Then an exception 'Invalid workspace name "some-name-that-exceeds-the-max-allowed-length" given. A workspace name has to consist of at most 36 lower case characters' should be thrown + Then an exception of type "InvalidArgumentException" should be thrown with message: + """ + Invalid workspace name "some-name-that-exceeds-the-max-allowed-length" given. A workspace name has to consist of at most 36 lower case characters + """ Scenario: Create root workspace with a name that is already used Given the root workspace "some-root-workspace" is created When the root workspace "some-root-workspace" is created - Then an exception "The workspace some-root-workspace already exists" should be thrown + Then an exception of type "WorkspaceAlreadyExists" should be thrown Scenario: Get metadata of non-existing root workspace When a root workspace "some-root-workspace" exists without metadata @@ -73,10 +76,10 @@ Feature: Neos WorkspaceService related features Scenario: Create a single personal workspace When the root workspace "some-root-workspace" is created - And the personal workspace "some-user-workspace" is created with the target workspace "some-root-workspace" for user "some-user-id" + And the personal workspace "some-user-workspace" is created with the target workspace "some-root-workspace" for user "jane.doe" Then the workspace "some-user-workspace" should have the following metadata: | Title | Description | Classification | Owner user id | - | some-user-workspace | | PERSONAL | some-user-id | + | some-user-workspace | | PERSONAL | janedoe | Scenario: Create a single shared workspace When the root workspace "some-root-workspace" is created @@ -94,7 +97,10 @@ Feature: Neos WorkspaceService related features Scenario: Assign role to non-existing workspace When the role COLLABORATOR is assigned to workspace "some-workspace" for group "Neos.Neos:AbstractEditor" - Then an exception 'Failed to find workspace with name "some-workspace" for content repository "default"' should be thrown + Then an exception of type "RuntimeException" should be thrown with message: + """ + Failed to find workspace with name "some-workspace" for content repository "default" + """ Scenario: Assign group role to root workspace Given the root workspace "some-root-workspace" is created @@ -107,42 +113,54 @@ Feature: Neos WorkspaceService related features Given the root workspace "some-root-workspace" is created When the role COLLABORATOR is assigned to workspace "some-root-workspace" for group "Neos.Neos:AbstractEditor" And the role MANAGER is assigned to workspace "some-root-workspace" for group "Neos.Neos:AbstractEditor" - Then an exception 'Failed to assign role for workspace "some-root-workspace" to subject "Neos.Neos:AbstractEditor" (Content Repository "default"): There is already a role assigned for that user/group, please unassign that first' should be thrown + Then an exception of type "RuntimeException" should be thrown with message: + """ + Failed to assign role for workspace "some-root-workspace" to subject "Neos.Neos:AbstractEditor" (Content Repository "default"): There is already a role assigned for that user/group, please unassign that first + """ Scenario: Assign user role to root workspace Given the root workspace "some-root-workspace" is created - When the role MANAGER is assigned to workspace "some-root-workspace" for user "some-user-id" + When the role MANAGER is assigned to workspace "some-root-workspace" for user "jane.doe" Then the workspace "some-root-workspace" should have the following role assignments: - | Subject type | Subject | Role | - | USER | some-user-id | MANAGER | + | Subject type | Subject | Role | + | USER | janedoe | MANAGER | Scenario: Assign a role to the same user twice Given the root workspace "some-root-workspace" is created - When the role COLLABORATOR is assigned to workspace "some-root-workspace" for user "some-user-id" - And the role MANAGER is assigned to workspace "some-root-workspace" for user "some-user-id" - Then an exception 'Failed to assign role for workspace "some-root-workspace" to subject "some-user-id" (Content Repository "default"): There is already a role assigned for that user/group, please unassign that first' should be thrown + When the role COLLABORATOR is assigned to workspace "some-root-workspace" for user "john.doe" + And the role MANAGER is assigned to workspace "some-root-workspace" for user "john.doe" + Then an exception of type "RuntimeException" should be thrown with message: + """ + Failed to assign role for workspace "some-root-workspace" to subject "johndoe" (Content Repository "default"): There is already a role assigned for that user/group, please unassign that first + """ Scenario: Unassign role from non-existing workspace When the role for group "Neos.Neos:AbstractEditor" is unassigned from workspace "some-workspace" - Then an exception 'Failed to find workspace with name "some-workspace" for content repository "default"' should be thrown + Then an exception of type "RuntimeException" should be thrown with message: + """ + Failed to find workspace with name "some-workspace" for content repository "default" + """ Scenario: Unassign role from workspace that has not been assigned before Given the root workspace "some-root-workspace" is created When the role for group "Neos.Neos:AbstractEditor" is unassigned from workspace "some-root-workspace" - Then an exception 'Failed to unassign role for subject "Neos.Neos:AbstractEditor" from workspace "some-root-workspace" (Content Repository "default"): No role assignment exists for this user/group' should be thrown + Then an exception of type "RuntimeException" should be thrown with message: + """ + Failed to unassign role for subject "Neos.Neos:AbstractEditor" from workspace "some-root-workspace" (Content Repository "default"): No role assignment exists for this user/group + """ Scenario: Assign two roles, then unassign one Given the root workspace "some-root-workspace" is created - And the role MANAGER is assigned to workspace "some-root-workspace" for user "some-user-id" + And the role MANAGER is assigned to workspace "some-root-workspace" for user "jane.doe" And the role COLLABORATOR is assigned to workspace "some-root-workspace" for group "Neos.Neos:AbstractEditor" Then the workspace "some-root-workspace" should have the following role assignments: | Subject type | Subject | Role | | GROUP | Neos.Neos:AbstractEditor | COLLABORATOR | - | USER | some-user-id | MANAGER | + | USER | janedoe | MANAGER | When the role for group "Neos.Neos:AbstractEditor" is unassigned from workspace "some-root-workspace" Then the workspace "some-root-workspace" should have the following role assignments: - | Subject type | Subject | Role | - | USER | some-user-id | MANAGER | + | Subject type | Subject | Role | + | USER | janedoe | MANAGER | Scenario: Workspace permissions for personal workspace for admin user Given the root workspace "live" is created @@ -186,14 +204,14 @@ Feature: Neos WorkspaceService related features Scenario: Workspace permissions for collaborator by user When the root workspace "some-root-workspace" is created - When the role COLLABORATOR is assigned to workspace "some-root-workspace" for user "johndoe" + When the role COLLABORATOR is assigned to workspace "some-root-workspace" for user "john.doe" Then the Neos user "jane.doe" should have the permissions "manage" for workspace "some-root-workspace" And the Neos user "john.doe" should have the permissions "read,write" for workspace "some-root-workspace" And the Neos user "editor" should have no permissions for workspace "some-root-workspace" Scenario: Workspace permissions for manager by user When the root workspace "some-root-workspace" is created - When the role MANAGER is assigned to workspace "some-root-workspace" for user "johndoe" + When the role MANAGER is assigned to workspace "some-root-workspace" for user "john.doe" Then the Neos user "jane.doe" should have the permissions "manage" for workspace "some-root-workspace" And the Neos user "john.doe" should have the permissions "read,write,manage" for workspace "some-root-workspace" And the Neos user "editor" should have no permissions for workspace "some-root-workspace" @@ -212,7 +230,7 @@ Feature: Neos WorkspaceService related features Scenario: Permissions for workspace without metadata Given a root workspace "some-root-workspace" exists without metadata - When the role COLLABORATOR is assigned to workspace "some-root-workspace" for user "janedoe" + When the role COLLABORATOR is assigned to workspace "some-root-workspace" for user "jane.doe" Then the Neos user "jane.doe" should have the permissions "read,write,manage" for workspace "some-root-workspace" And the Neos user "john.doe" should have no permissions for workspace "some-root-workspace" And the Neos user "editor" should have no permissions for workspace "some-root-workspace" From 2ecfe5dc97b8946860e74de544c50e64e4303f8c Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Fri, 8 Nov 2024 15:05:31 +0100 Subject: [PATCH 092/214] Fix node migration transformations and tests by only using "official" commands --- .../AddNewProperty_NoDimensions.feature | 50 ++++++++++++++++-- .../RebasableToOtherWorkspaceInterface.php | 2 +- .../src/NodeMigrationServiceFactory.php | 2 +- ...nsionShineThroughTransformationFactory.php | 4 +- .../AddNewPropertyTransformationFactory.php | 25 +++++---- .../ChangeNodeTypeTransformationFactory.php | 4 +- ...angePropertyValueTransformationFactory.php | 26 ++++++---- ...mensionSpacePointTransformationFactory.php | 4 +- .../RemoveNodeTransformationFactory.php | 4 +- .../RemovePropertyTransformationFactory.php | 16 +++--- ...nameNodeAggregateTransformationFactory.php | 4 +- .../RenamePropertyTransformationFactory.php | 36 +++++++------ ...ripTagsOnPropertyTransformationFactory.php | 52 +++++++++---------- .../TransformationFactoryInterface.php | 4 +- .../Transformation/TransformationsFactory.php | 6 ++- ...gregateDimensionsTransformationFactory.php | 4 +- 16 files changed, 154 insertions(+), 89 deletions(-) diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/AddNewProperty_NoDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/AddNewProperty_NoDimensions.feature index 08f35b9c556..0f6d9270e71 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/AddNewProperty_NoDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/AddNewProperty_NoDimensions.feature @@ -13,6 +13,8 @@ Feature: Add New Property properties: text: type: string + dateTime: + type: DateTime """ And using identifier "default", I define a content repository And I am in content repository "default" @@ -64,7 +66,7 @@ Feature: Add New Property - type: 'AddNewProperty' settings: - newPropertyName: 'aDateOutsideSchema' + newPropertyName: 'dateTime' serializedValue: '2013-09-09T12:04:12+00:00' type: 'DateTime' """ @@ -82,6 +84,46 @@ Feature: Add New Property | text | "Original text" | Then I expect a node identified by migration-cs;other;{} to exist in the content graph And I expect this node to have the following properties: - | Key | Value | - | text | "fixed value" | - | aDateOutsideSchema | Date:2013-09-09T12:04:12+00:00 | + | Key | Value | + | text | "fixed value" | + | dateTime | Date:2013-09-09T12:04:12+00:00 | + + Scenario: Adding a property that is not defined in the node type schema + When I run the following node migration for workspace "live", creating target workspace "migration-workspace" on contentStreamId "migration-cs" and exceptions are caught: + """yaml + migration: + - + filters: + - + type: 'NodeType' + settings: + nodeType: 'Neos.ContentRepository.Testing:Document' + transformations: + - + type: 'AddNewProperty' + settings: + newPropertyName: 'aDateOutsideSchema' + serializedValue: '2013-09-09T12:04:12+00:00' + type: 'DateTime' + """ + Then the last command should have thrown an exception of type "PropertyCannotBeSet" + + Scenario: Adding a property with a different type than defined by the node type schema + When I run the following node migration for workspace "live", creating target workspace "migration-workspace" on contentStreamId "migration-cs" and exceptions are caught: + """yaml + migration: + - + filters: + - + type: 'NodeType' + settings: + nodeType: 'Neos.ContentRepository.Testing:Document' + transformations: + - + type: 'AddNewProperty' + settings: + newPropertyName: 'dateTime' + serializedValue: '2013-09-09T12:04:12+00:00' + type: 'string' + """ + Then the last command should have thrown an exception of type "PropertyCannotBeSet" diff --git a/Neos.ContentRepository.Core/Classes/Feature/Common/RebasableToOtherWorkspaceInterface.php b/Neos.ContentRepository.Core/Classes/Feature/Common/RebasableToOtherWorkspaceInterface.php index d1478c3cfd1..b0c643f6043 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/Common/RebasableToOtherWorkspaceInterface.php +++ b/Neos.ContentRepository.Core/Classes/Feature/Common/RebasableToOtherWorkspaceInterface.php @@ -30,7 +30,7 @@ interface RebasableToOtherWorkspaceInterface { public function createCopyForWorkspace( WorkspaceName $targetWorkspaceName, - ): (self&CommandInterface)|(self&SerializedCommandInterface); + ): (RebasableToOtherWorkspaceInterface&CommandInterface)|(RebasableToOtherWorkspaceInterface&SerializedCommandInterface); /** * called during deserialization from metadata diff --git a/Neos.ContentRepository.NodeMigration/src/NodeMigrationServiceFactory.php b/Neos.ContentRepository.NodeMigration/src/NodeMigrationServiceFactory.php index f6c3f5fee33..7a293ea5ae0 100644 --- a/Neos.ContentRepository.NodeMigration/src/NodeMigrationServiceFactory.php +++ b/Neos.ContentRepository.NodeMigration/src/NodeMigrationServiceFactory.php @@ -39,7 +39,7 @@ public function build(ContentRepositoryServiceFactoryDependencies $serviceFactor $filtersFactory->registerFilter('PropertyNotEmpty', new PropertyNotEmptyFilterFactory()); $filtersFactory->registerFilter('PropertyValue', new PropertyValueFilterFactory()); - $transformationsFactory = new TransformationsFactory($serviceFactoryDependencies->contentRepository); + $transformationsFactory = new TransformationsFactory($serviceFactoryDependencies->contentRepository, $serviceFactoryDependencies->propertyConverter); $transformationsFactory->registerTransformation('AddDimensionShineThrough', new AddDimensionShineThroughTransformationFactory()); $transformationsFactory->registerTransformation('AddNewProperty', new AddNewPropertyTransformationFactory()); $transformationsFactory->registerTransformation('ChangeNodeType', new ChangeNodeTypeTransformationFactory()); diff --git a/Neos.ContentRepository.NodeMigration/src/Transformation/AddDimensionShineThroughTransformationFactory.php b/Neos.ContentRepository.NodeMigration/src/Transformation/AddDimensionShineThroughTransformationFactory.php index f8decab4901..ea774b966c1 100644 --- a/Neos.ContentRepository.NodeMigration/src/Transformation/AddDimensionShineThroughTransformationFactory.php +++ b/Neos.ContentRepository.NodeMigration/src/Transformation/AddDimensionShineThroughTransformationFactory.php @@ -17,6 +17,7 @@ use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\Feature\DimensionSpaceAdjustment\Command\AddDimensionShineThrough; +use Neos\ContentRepository\Core\Infrastructure\Property\PropertyConverter; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; /** @@ -33,7 +34,8 @@ class AddDimensionShineThroughTransformationFactory implements TransformationFac */ public function build( array $settings, - ContentRepository $contentRepository + ContentRepository $contentRepository, + PropertyConverter $propertyConverter, ): GlobalTransformationInterface|NodeAggregateBasedTransformationInterface|NodeBasedTransformationInterface { return new class ( DimensionSpacePoint::fromArray($settings['from']), diff --git a/Neos.ContentRepository.NodeMigration/src/Transformation/AddNewPropertyTransformationFactory.php b/Neos.ContentRepository.NodeMigration/src/Transformation/AddNewPropertyTransformationFactory.php index b6ad21e946a..b89e8aa63f3 100644 --- a/Neos.ContentRepository.NodeMigration/src/Transformation/AddNewPropertyTransformationFactory.php +++ b/Neos.ContentRepository.NodeMigration/src/Transformation/AddNewPropertyTransformationFactory.php @@ -16,9 +16,11 @@ use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; -use Neos\ContentRepository\Core\Feature\NodeModification\Command\SetSerializedNodeProperties; +use Neos\ContentRepository\Core\Feature\NodeModification\Command\SetNodeProperties; +use Neos\ContentRepository\Core\Feature\NodeModification\Dto\PropertyValuesToWrite; use Neos\ContentRepository\Core\Feature\NodeModification\Dto\SerializedPropertyValue; use Neos\ContentRepository\Core\Feature\NodeModification\Dto\SerializedPropertyValues; +use Neos\ContentRepository\Core\Infrastructure\Property\PropertyConverter; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\SharedModel\Node\PropertyNames; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; @@ -31,13 +33,15 @@ class AddNewPropertyTransformationFactory implements TransformationFactoryInterf */ public function build( array $settings, - ContentRepository $contentRepository + ContentRepository $contentRepository, + PropertyConverter $propertyConverter, ): GlobalTransformationInterface|NodeAggregateBasedTransformationInterface|NodeBasedTransformationInterface { return new class ( $settings['newPropertyName'], $settings['type'], $settings['serializedValue'], - $contentRepository + $contentRepository, + $propertyConverter, ) implements NodeBasedTransformationInterface { public function __construct( /** @@ -50,6 +54,7 @@ public function __construct( */ private readonly mixed $serializedValue, private readonly ContentRepository $contentRepository, + private readonly PropertyConverter $propertyConverter, ) { } @@ -63,20 +68,18 @@ public function execute( // we don't need to unset a non-existing property return; } + /** @phpstan-ignore-next-line */ + $deserializedPropertyValue = $this->propertyConverter->deserializePropertyValue(SerializedPropertyValue::create($this->serializedValue, $this->type)); if (!$node->hasProperty($this->newPropertyName)) { $this->contentRepository->handle( - SetSerializedNodeProperties::create( + SetNodeProperties::create( $workspaceNameForWriting, $node->aggregateId, $node->originDimensionSpacePoint, - SerializedPropertyValues::fromArray([ - $this->newPropertyName => SerializedPropertyValue::create( - $this->serializedValue, - $this->type - ) - ]), - PropertyNames::createEmpty() + PropertyValuesToWrite::fromArray([ + $this->newPropertyName => $deserializedPropertyValue, + ]) ) ); } diff --git a/Neos.ContentRepository.NodeMigration/src/Transformation/ChangeNodeTypeTransformationFactory.php b/Neos.ContentRepository.NodeMigration/src/Transformation/ChangeNodeTypeTransformationFactory.php index 42ec7df2b4c..d6f8a0bb328 100644 --- a/Neos.ContentRepository.NodeMigration/src/Transformation/ChangeNodeTypeTransformationFactory.php +++ b/Neos.ContentRepository.NodeMigration/src/Transformation/ChangeNodeTypeTransformationFactory.php @@ -17,6 +17,7 @@ use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\Feature\NodeTypeChange\Command\ChangeNodeAggregateType; use Neos\ContentRepository\Core\Feature\NodeTypeChange\Dto\NodeAggregateTypeChangeChildConstraintConflictResolutionStrategy; +use Neos\ContentRepository\Core\Infrastructure\Property\PropertyConverter; use Neos\ContentRepository\Core\NodeType\NodeTypeName; use Neos\ContentRepository\Core\Projection\ContentGraph\NodeAggregate; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; @@ -35,7 +36,8 @@ class ChangeNodeTypeTransformationFactory implements TransformationFactoryInterf */ public function build( array $settings, - ContentRepository $contentRepository + ContentRepository $contentRepository, + PropertyConverter $propertyConverter, ): GlobalTransformationInterface|NodeAggregateBasedTransformationInterface|NodeBasedTransformationInterface { // by default, we won't delete anything. $nodeAggregateTypeChangeChildConstraintConflictResolutionStrategy diff --git a/Neos.ContentRepository.NodeMigration/src/Transformation/ChangePropertyValueTransformationFactory.php b/Neos.ContentRepository.NodeMigration/src/Transformation/ChangePropertyValueTransformationFactory.php index c0b51852b6b..716aeb8830c 100644 --- a/Neos.ContentRepository.NodeMigration/src/Transformation/ChangePropertyValueTransformationFactory.php +++ b/Neos.ContentRepository.NodeMigration/src/Transformation/ChangePropertyValueTransformationFactory.php @@ -16,9 +16,12 @@ use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; +use Neos\ContentRepository\Core\Feature\NodeModification\Command\SetNodeProperties; use Neos\ContentRepository\Core\Feature\NodeModification\Command\SetSerializedNodeProperties; +use Neos\ContentRepository\Core\Feature\NodeModification\Dto\PropertyValuesToWrite; use Neos\ContentRepository\Core\Feature\NodeModification\Dto\SerializedPropertyValue; use Neos\ContentRepository\Core\Feature\NodeModification\Dto\SerializedPropertyValues; +use Neos\ContentRepository\Core\Infrastructure\Property\PropertyConverter; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\SharedModel\Node\PropertyNames; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; @@ -41,7 +44,8 @@ class ChangePropertyValueTransformationFactory implements TransformationFactoryI */ public function build( array $settings, - ContentRepository $contentRepository + ContentRepository $contentRepository, + PropertyConverter $propertyConverter, ): GlobalTransformationInterface|NodeAggregateBasedTransformationInterface|NodeBasedTransformationInterface { $newSerializedValue = '{current}'; if (isset($settings['newSerializedValue'])) { @@ -69,7 +73,8 @@ public function build( $search, $replace, $currentValuePlaceholder, - $contentRepository + $contentRepository, + $propertyConverter, ) implements NodeBasedTransformationInterface { public function __construct( /** @@ -96,7 +101,8 @@ public function __construct( * current property value into the new value. */ private readonly string $currentValuePlaceholder, - private readonly ContentRepository $contentRepository + private readonly ContentRepository $contentRepository, + private readonly PropertyConverter $propertyConverter, ) { } @@ -126,19 +132,17 @@ public function execute( $this->replace, $newValueWithReplacedCurrentValue ); + /** @phpstan-ignore-next-line */ + $deserializedPropertyValue = $this->propertyConverter->deserializePropertyValue(SerializedPropertyValue::create($newValueWithReplacedSearch, $currentProperty->type)); $this->contentRepository->handle( - SetSerializedNodeProperties::create( + SetNodeProperties::create( $workspaceNameForWriting, $node->aggregateId, $node->originDimensionSpacePoint, - SerializedPropertyValues::fromArray([ - $this->propertyName => SerializedPropertyValue::create( - $newValueWithReplacedSearch, - $currentProperty->type - ) - ]), - PropertyNames::createEmpty() + PropertyValuesToWrite::fromArray([ + $this->propertyName => $deserializedPropertyValue, + ]) ) ); } diff --git a/Neos.ContentRepository.NodeMigration/src/Transformation/MoveDimensionSpacePointTransformationFactory.php b/Neos.ContentRepository.NodeMigration/src/Transformation/MoveDimensionSpacePointTransformationFactory.php index 0b7cc1b7679..47699871c57 100644 --- a/Neos.ContentRepository.NodeMigration/src/Transformation/MoveDimensionSpacePointTransformationFactory.php +++ b/Neos.ContentRepository.NodeMigration/src/Transformation/MoveDimensionSpacePointTransformationFactory.php @@ -17,6 +17,7 @@ use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\Feature\DimensionSpaceAdjustment\Command\MoveDimensionSpacePoint; +use Neos\ContentRepository\Core\Infrastructure\Property\PropertyConverter; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; /** @@ -29,7 +30,8 @@ class MoveDimensionSpacePointTransformationFactory implements TransformationFact */ public function build( array $settings, - ContentRepository $contentRepository + ContentRepository $contentRepository, + PropertyConverter $propertyConverter, ): GlobalTransformationInterface|NodeAggregateBasedTransformationInterface|NodeBasedTransformationInterface { $from = DimensionSpacePoint::fromArray($settings['from']); $to = DimensionSpacePoint::fromArray($settings['to']); diff --git a/Neos.ContentRepository.NodeMigration/src/Transformation/RemoveNodeTransformationFactory.php b/Neos.ContentRepository.NodeMigration/src/Transformation/RemoveNodeTransformationFactory.php index fbd0a2b46fa..0dfca855a8e 100644 --- a/Neos.ContentRepository.NodeMigration/src/Transformation/RemoveNodeTransformationFactory.php +++ b/Neos.ContentRepository.NodeMigration/src/Transformation/RemoveNodeTransformationFactory.php @@ -18,6 +18,7 @@ use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; use Neos\ContentRepository\Core\Feature\NodeRemoval\Command\RemoveNodeAggregate; +use Neos\ContentRepository\Core\Infrastructure\Property\PropertyConverter; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\SharedModel\Node\NodeVariantSelectionStrategy; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; @@ -33,7 +34,8 @@ class RemoveNodeTransformationFactory implements TransformationFactoryInterface */ public function build( array $settings, - ContentRepository $contentRepository + ContentRepository $contentRepository, + PropertyConverter $propertyConverter, ): GlobalTransformationInterface|NodeAggregateBasedTransformationInterface|NodeBasedTransformationInterface { $strategy = null; if (isset($settings['strategy'])) { diff --git a/Neos.ContentRepository.NodeMigration/src/Transformation/RemovePropertyTransformationFactory.php b/Neos.ContentRepository.NodeMigration/src/Transformation/RemovePropertyTransformationFactory.php index c0990547e2d..30e182e42bc 100644 --- a/Neos.ContentRepository.NodeMigration/src/Transformation/RemovePropertyTransformationFactory.php +++ b/Neos.ContentRepository.NodeMigration/src/Transformation/RemovePropertyTransformationFactory.php @@ -16,10 +16,10 @@ use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; -use Neos\ContentRepository\Core\Feature\NodeModification\Command\SetSerializedNodeProperties; -use Neos\ContentRepository\Core\Feature\NodeModification\Dto\SerializedPropertyValues; +use Neos\ContentRepository\Core\Feature\NodeModification\Command\SetNodeProperties; +use Neos\ContentRepository\Core\Feature\NodeModification\Dto\PropertyValuesToWrite; +use Neos\ContentRepository\Core\Infrastructure\Property\PropertyConverter; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; -use Neos\ContentRepository\Core\SharedModel\Node\PropertyNames; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; @@ -33,7 +33,8 @@ class RemovePropertyTransformationFactory implements TransformationFactoryInterf */ public function build( array $settings, - ContentRepository $contentRepository + ContentRepository $contentRepository, + PropertyConverter $propertyConverter, ): GlobalTransformationInterface|NodeAggregateBasedTransformationInterface|NodeBasedTransformationInterface { $propertyName = $settings['property']; return new class ( @@ -56,12 +57,13 @@ public function execute( ): void { if ($node->hasProperty($this->propertyName)) { $this->contentRepository->handle( - SetSerializedNodeProperties::create( + SetNodeProperties::create( $workspaceNameForWriting, $node->aggregateId, $node->originDimensionSpacePoint, - SerializedPropertyValues::createEmpty(), - PropertyNames::fromArray([$this->propertyName]) + PropertyValuesToWrite::fromArray([ + $this->propertyName => null, + ]), ) ); } diff --git a/Neos.ContentRepository.NodeMigration/src/Transformation/RenameNodeAggregateTransformationFactory.php b/Neos.ContentRepository.NodeMigration/src/Transformation/RenameNodeAggregateTransformationFactory.php index 3e05d002d1b..cd277a27641 100644 --- a/Neos.ContentRepository.NodeMigration/src/Transformation/RenameNodeAggregateTransformationFactory.php +++ b/Neos.ContentRepository.NodeMigration/src/Transformation/RenameNodeAggregateTransformationFactory.php @@ -16,6 +16,7 @@ use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\Feature\NodeRenaming\Command\ChangeNodeAggregateName; +use Neos\ContentRepository\Core\Infrastructure\Property\PropertyConverter; use Neos\ContentRepository\Core\Projection\ContentGraph\NodeAggregate; use Neos\ContentRepository\Core\SharedModel\Node\NodeName; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; @@ -28,7 +29,8 @@ class RenameNodeAggregateTransformationFactory implements TransformationFactoryI */ public function build( array $settings, - ContentRepository $contentRepository + ContentRepository $contentRepository, + PropertyConverter $propertyConverter, ): GlobalTransformationInterface|NodeAggregateBasedTransformationInterface|NodeBasedTransformationInterface { $newNodeName = $settings['newNodeName']; diff --git a/Neos.ContentRepository.NodeMigration/src/Transformation/RenamePropertyTransformationFactory.php b/Neos.ContentRepository.NodeMigration/src/Transformation/RenamePropertyTransformationFactory.php index ff70a13bd7f..3d76ca3ba37 100644 --- a/Neos.ContentRepository.NodeMigration/src/Transformation/RenamePropertyTransformationFactory.php +++ b/Neos.ContentRepository.NodeMigration/src/Transformation/RenamePropertyTransformationFactory.php @@ -16,10 +16,10 @@ use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; -use Neos\ContentRepository\Core\Feature\NodeModification\Command\SetSerializedNodeProperties; -use Neos\ContentRepository\Core\Feature\NodeModification\Dto\SerializedPropertyValues; +use Neos\ContentRepository\Core\Feature\NodeModification\Command\SetNodeProperties; +use Neos\ContentRepository\Core\Feature\NodeModification\Dto\PropertyValuesToWrite; +use Neos\ContentRepository\Core\Infrastructure\Property\PropertyConverter; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; -use Neos\ContentRepository\Core\SharedModel\Node\PropertyNames; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; @@ -33,7 +33,8 @@ class RenamePropertyTransformationFactory implements TransformationFactoryInterf */ public function build( array $settings, - ContentRepository $contentRepository + ContentRepository $contentRepository, + PropertyConverter $propertyConverter, ): GlobalTransformationInterface|NodeAggregateBasedTransformationInterface|NodeBasedTransformationInterface { return new class ( @@ -62,20 +63,21 @@ public function execute( ContentStreamId $contentStreamForWriting ): void { - $serializedPropertyValue = $node->properties->serialized()->getProperty($this->from); - if ($serializedPropertyValue !== null) { - $this->contentRepository->handle( - SetSerializedNodeProperties::create( - $workspaceNameForWriting, - $node->aggregateId, - $node->originDimensionSpacePoint, - SerializedPropertyValues::fromArray([ - $this->to => $serializedPropertyValue - ]), - PropertyNames::fromArray([$this->from]) - ) - ); + $propertyValue = $node->properties[$this->from]; + if ($propertyValue === null) { + return; } + $this->contentRepository->handle( + SetNodeProperties::create( + $workspaceNameForWriting, + $node->aggregateId, + $node->originDimensionSpacePoint, + PropertyValuesToWrite::fromArray([ + $this->to => $propertyValue, + $this->from => null, + ]), + ) + ); } }; } diff --git a/Neos.ContentRepository.NodeMigration/src/Transformation/StripTagsOnPropertyTransformationFactory.php b/Neos.ContentRepository.NodeMigration/src/Transformation/StripTagsOnPropertyTransformationFactory.php index 9803c9509b2..6c372de3c3c 100644 --- a/Neos.ContentRepository.NodeMigration/src/Transformation/StripTagsOnPropertyTransformationFactory.php +++ b/Neos.ContentRepository.NodeMigration/src/Transformation/StripTagsOnPropertyTransformationFactory.php @@ -16,11 +16,10 @@ use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; -use Neos\ContentRepository\Core\Feature\NodeModification\Command\SetSerializedNodeProperties; -use Neos\ContentRepository\Core\Feature\NodeModification\Dto\SerializedPropertyValue; -use Neos\ContentRepository\Core\Feature\NodeModification\Dto\SerializedPropertyValues; +use Neos\ContentRepository\Core\Feature\NodeModification\Command\SetNodeProperties; +use Neos\ContentRepository\Core\Feature\NodeModification\Dto\PropertyValuesToWrite; +use Neos\ContentRepository\Core\Infrastructure\Property\PropertyConverter; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; -use Neos\ContentRepository\Core\SharedModel\Node\PropertyNames; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; @@ -34,7 +33,8 @@ class StripTagsOnPropertyTransformationFactory implements TransformationFactoryI */ public function build( array $settings, - ContentRepository $contentRepository + ContentRepository $contentRepository, + PropertyConverter $propertyConverter, ): GlobalTransformationInterface|NodeAggregateBasedTransformationInterface|NodeBasedTransformationInterface { return new class ( $settings['property'], @@ -55,31 +55,27 @@ public function execute( WorkspaceName $workspaceNameForWriting, ContentStreamId $contentStreamForWriting ): void { - $serializedPropertyValue = $node->properties->serialized()->getProperty($this->propertyName); - if ($serializedPropertyValue !== null) { - $propertyValue = $serializedPropertyValue->value; - if (!is_string($propertyValue)) { - throw new \Exception( - 'StripTagsOnProperty can only be applied to properties of type string.', - 1645391885 - ); - } - $newValue = strip_tags($propertyValue); - $this->contentRepository->handle( - SetSerializedNodeProperties::create( - $workspaceNameForWriting, - $node->aggregateId, - $node->originDimensionSpacePoint, - SerializedPropertyValues::fromArray([ - $this->propertyName => SerializedPropertyValue::create( - $newValue, - $serializedPropertyValue->type - ) - ]), - PropertyNames::createEmpty() - ) + $propertyValue = $node->properties[$this->propertyName]; + if ($propertyValue === null) { + return; + } + if (!is_string($propertyValue)) { + throw new \Exception( + sprintf('StripTagsOnProperty can only be applied to properties of type string. Property "%s" is of type %s', $this->propertyName, get_debug_type($propertyValue)), + 1645391885 ); } + $newValue = strip_tags($propertyValue); + $this->contentRepository->handle( + SetNodeProperties::create( + $workspaceNameForWriting, + $node->aggregateId, + $node->originDimensionSpacePoint, + PropertyValuesToWrite::fromArray([ + $this->propertyName => $newValue, + ]), + ) + ); } }; } diff --git a/Neos.ContentRepository.NodeMigration/src/Transformation/TransformationFactoryInterface.php b/Neos.ContentRepository.NodeMigration/src/Transformation/TransformationFactoryInterface.php index df0c2193ed7..ecaa105d733 100644 --- a/Neos.ContentRepository.NodeMigration/src/Transformation/TransformationFactoryInterface.php +++ b/Neos.ContentRepository.NodeMigration/src/Transformation/TransformationFactoryInterface.php @@ -5,6 +5,7 @@ namespace Neos\ContentRepository\NodeMigration\Transformation; use Neos\ContentRepository\Core\ContentRepository; +use Neos\ContentRepository\Core\Infrastructure\Property\PropertyConverter; interface TransformationFactoryInterface { @@ -13,6 +14,7 @@ interface TransformationFactoryInterface */ public function build( array $settings, - ContentRepository $contentRepository + ContentRepository $contentRepository, + PropertyConverter $propertyConverter, ): GlobalTransformationInterface|NodeAggregateBasedTransformationInterface|NodeBasedTransformationInterface; } diff --git a/Neos.ContentRepository.NodeMigration/src/Transformation/TransformationsFactory.php b/Neos.ContentRepository.NodeMigration/src/Transformation/TransformationsFactory.php index 915c05cc3fe..56d8d0eaa02 100644 --- a/Neos.ContentRepository.NodeMigration/src/Transformation/TransformationsFactory.php +++ b/Neos.ContentRepository.NodeMigration/src/Transformation/TransformationsFactory.php @@ -5,6 +5,7 @@ namespace Neos\ContentRepository\NodeMigration\Transformation; use Neos\ContentRepository\Core\ContentRepository; +use Neos\ContentRepository\Core\Infrastructure\Property\PropertyConverter; use Neos\ContentRepository\NodeMigration\MigrationException; use Neos\ContentRepository\NodeMigration\NodeMigrationService; @@ -19,7 +20,8 @@ class TransformationsFactory private array $transformationFactories = []; public function __construct( - private readonly ContentRepository $contentRepository + private readonly ContentRepository $contentRepository, + private readonly PropertyConverter $propertyConverter, ) { } @@ -58,7 +60,7 @@ protected function buildTransformationObject( ): GlobalTransformationInterface|NodeAggregateBasedTransformationInterface|NodeBasedTransformationInterface { $transformationFactory = $this->resolveTransformationFactory($transformationConfiguration['type']); - return $transformationFactory->build($transformationConfiguration['settings'] ?? [], $this->contentRepository); + return $transformationFactory->build($transformationConfiguration['settings'] ?? [], $this->contentRepository, $this->propertyConverter); } /** diff --git a/Neos.ContentRepository.NodeMigration/src/Transformation/UpdateRootNodeAggregateDimensionsTransformationFactory.php b/Neos.ContentRepository.NodeMigration/src/Transformation/UpdateRootNodeAggregateDimensionsTransformationFactory.php index 3b4b47867bb..4e735ae7ad3 100644 --- a/Neos.ContentRepository.NodeMigration/src/Transformation/UpdateRootNodeAggregateDimensionsTransformationFactory.php +++ b/Neos.ContentRepository.NodeMigration/src/Transformation/UpdateRootNodeAggregateDimensionsTransformationFactory.php @@ -6,6 +6,7 @@ use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\Feature\RootNodeCreation\Command\UpdateRootNodeAggregateDimensions; +use Neos\ContentRepository\Core\Infrastructure\Property\PropertyConverter; use Neos\ContentRepository\Core\NodeType\NodeTypeName; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepository\NodeMigration\MigrationException; @@ -17,7 +18,8 @@ class UpdateRootNodeAggregateDimensionsTransformationFactory implements Transfor */ public function build( array $settings, - ContentRepository $contentRepository + ContentRepository $contentRepository, + PropertyConverter $propertyConverter, ): GlobalTransformationInterface|NodeAggregateBasedTransformationInterface|NodeBasedTransformationInterface { if (!isset($settings['nodeType'])) { throw new MigrationException( From 3b38dff7a6f12a2a94e6ef062f496b265c10b39b Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Fri, 8 Nov 2024 16:04:07 +0100 Subject: [PATCH 093/214] TASK: Reintroduce methods and prefer to deprecate them :) --- Neos.Fusion/Classes/Core/FusionSourceCodeCollection.php | 9 +++++++++ .../Classes/Domain/Service/FusionSourceCodeFactory.php | 8 ++++++++ 2 files changed, 17 insertions(+) diff --git a/Neos.Fusion/Classes/Core/FusionSourceCodeCollection.php b/Neos.Fusion/Classes/Core/FusionSourceCodeCollection.php index 00eacaa5b04..c1d4c809330 100644 --- a/Neos.Fusion/Classes/Core/FusionSourceCodeCollection.php +++ b/Neos.Fusion/Classes/Core/FusionSourceCodeCollection.php @@ -52,6 +52,15 @@ public static function tryFromFilePath(string $filePath): self return self::fromFilePath($filePath); } + /** + * @deprecated with Neos 9, remove me :) + */ + public static function tryFromPackageRootFusion(string $packageKey): self + { + $fusionPathAndFilename = sprintf('resource://%s/Private/Fusion/Root.fusion', $packageKey); + return self::tryFromFilePath($fusionPathAndFilename); + } + public static function empty(): self { return new self(); diff --git a/Neos.Neos/Classes/Domain/Service/FusionSourceCodeFactory.php b/Neos.Neos/Classes/Domain/Service/FusionSourceCodeFactory.php index 657d2ef94ff..e8766ba3e5a 100644 --- a/Neos.Neos/Classes/Domain/Service/FusionSourceCodeFactory.php +++ b/Neos.Neos/Classes/Domain/Service/FusionSourceCodeFactory.php @@ -59,6 +59,14 @@ public function createFromAutoIncludes(): FusionSourceCodeCollection return $sourcecode; } + /** + * @deprecated with Neos 9 - YAGNI from the start :) + */ + public function createFromSite(Site $site): FusionSourceCodeCollection + { + return FusionSourceCodeCollection::tryFromPackageRootFusion($site->getSiteResourcesPackageKey()); + } + /** * Generate Fusion prototype definitions for all node types * From bd60fb8e20c461f8ed5c210cbe153da3626dbfc6 Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Fri, 8 Nov 2024 16:10:49 +0100 Subject: [PATCH 094/214] Fine grained PHPStan ignores --- .../src/Transformation/AddNewPropertyTransformationFactory.php | 3 +-- .../ChangePropertyValueTransformationFactory.php | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Neos.ContentRepository.NodeMigration/src/Transformation/AddNewPropertyTransformationFactory.php b/Neos.ContentRepository.NodeMigration/src/Transformation/AddNewPropertyTransformationFactory.php index b89e8aa63f3..1a3dd9cdbc7 100644 --- a/Neos.ContentRepository.NodeMigration/src/Transformation/AddNewPropertyTransformationFactory.php +++ b/Neos.ContentRepository.NodeMigration/src/Transformation/AddNewPropertyTransformationFactory.php @@ -68,8 +68,7 @@ public function execute( // we don't need to unset a non-existing property return; } - /** @phpstan-ignore-next-line */ - $deserializedPropertyValue = $this->propertyConverter->deserializePropertyValue(SerializedPropertyValue::create($this->serializedValue, $this->type)); + $deserializedPropertyValue = $this->propertyConverter->deserializePropertyValue(SerializedPropertyValue::create($this->serializedValue, $this->type)); // @phpstan-ignore neos.cr.internal if (!$node->hasProperty($this->newPropertyName)) { $this->contentRepository->handle( diff --git a/Neos.ContentRepository.NodeMigration/src/Transformation/ChangePropertyValueTransformationFactory.php b/Neos.ContentRepository.NodeMigration/src/Transformation/ChangePropertyValueTransformationFactory.php index 716aeb8830c..bd3d31c915b 100644 --- a/Neos.ContentRepository.NodeMigration/src/Transformation/ChangePropertyValueTransformationFactory.php +++ b/Neos.ContentRepository.NodeMigration/src/Transformation/ChangePropertyValueTransformationFactory.php @@ -132,8 +132,7 @@ public function execute( $this->replace, $newValueWithReplacedCurrentValue ); - /** @phpstan-ignore-next-line */ - $deserializedPropertyValue = $this->propertyConverter->deserializePropertyValue(SerializedPropertyValue::create($newValueWithReplacedSearch, $currentProperty->type)); + $deserializedPropertyValue = $this->propertyConverter->deserializePropertyValue(SerializedPropertyValue::create($newValueWithReplacedSearch, $currentProperty->type)); // @phpstan-ignore neos.cr.internal $this->contentRepository->handle( SetNodeProperties::create( From 35b02dcbe3a78fa7807504f05a143147d6e6bd1f Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Fri, 8 Nov 2024 16:10:55 +0100 Subject: [PATCH 095/214] Fix tests --- ...AggregateWithNode_WithoutDimensions.feature | 8 ++++---- .../RemoveNodeAggregateAfterDisabling.feature | 2 +- .../RemoveNodeAggregateWithDimensions.feature | 4 ++-- .../DimensionMismatch.feature | 2 +- .../StructureAdjustment/Properties.feature | 2 +- .../TetheredNodesReordering.feature | 2 +- .../02-RebasingWithAutoCreatedNodes.feature | 18 +++++++++--------- .../04-AllFeaturePublication.feature | 4 ++-- .../Command/SetNodeProperties.php | 13 +++++++++++++ ...ericCommandExecutionAndEventPublication.php | 6 ++---- .../Features/Fusion/ContentCollection.feature | 2 +- 11 files changed, 37 insertions(+), 26 deletions(-) diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/03-CreateNodeAggregateWithNode_WithoutDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/03-CreateNodeAggregateWithNode_WithoutDimensions.feature index 076f8d72e2e..d71d45460a1 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/03-CreateNodeAggregateWithNode_WithoutDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/03-CreateNodeAggregateWithNode_WithoutDimensions.feature @@ -202,14 +202,14 @@ Feature: Create node aggregate with node | nodeAggregateId | "lady-eleonode-rootford" | | nodeTypeName | "Neos.ContentRepository:Root" | - Given the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: + Given the command CreateNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "sir-david-nodenborough" | | nodeTypeName | "Neos.ContentRepository.Testing:NodeWithoutTetheredChildNodes" | | originDimensionSpacePoint | {} | | parentNodeAggregateId | "lady-eleonode-rootford" | | nodeName | "node" | - And the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: + And the command CreateNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "sir-nodeward-nodington-iii" | | nodeTypeName | "Neos.ContentRepository.Testing:NodeWithoutTetheredChildNodes" | @@ -280,7 +280,7 @@ Feature: Create node aggregate with node | nodeAggregateId | "lady-eleonode-rootford" | | nodeTypeName | "Neos.ContentRepository:Root" | - When the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: + When the command CreateNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "sir-david-nodenborough" | | nodeTypeName | "Neos.ContentRepository.Testing:NodeWithTetheredChildNodes" | @@ -459,7 +459,7 @@ Feature: Create node aggregate with node | nodeAggregateId | "lady-eleonode-rootford" | | nodeTypeName | "Neos.ContentRepository:Root" | - When the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: + When the command CreateNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "sir-david-nodenborough" | | nodeTypeName | "Neos.ContentRepository.Testing:NodeWithTetheredChildNodes" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRemoval/RemoveNodeAggregateAfterDisabling.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRemoval/RemoveNodeAggregateAfterDisabling.feature index 8652a6a8252..156576e5069 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRemoval/RemoveNodeAggregateAfterDisabling.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRemoval/RemoveNodeAggregateAfterDisabling.feature @@ -50,7 +50,7 @@ Feature: Disable a node aggregate | affectedOccupiedDimensionSpacePoints | [{}] | | affectedCoveredDimensionSpacePoints | [{}] | - When the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: + When the command CreateNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "nody-mc-nodeface" | | nodeTypeName | "Neos.ContentRepository.Testing:Document" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRemoval/RemoveNodeAggregateWithDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRemoval/RemoveNodeAggregateWithDimensions.feature index 96e0ecaafb7..742b38d69b6 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRemoval/RemoveNodeAggregateWithDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRemoval/RemoveNodeAggregateWithDimensions.feature @@ -24,7 +24,7 @@ Feature: Remove NodeAggregate | nodeTypeName | "Neos.ContentRepository:Root" | # We have to add another node since root nodes are in all dimension space points and thus cannot be varied # Node /document - And the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: + And the command CreateNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "nody-mc-nodeface" | | nodeTypeName | "Neos.ContentRepository.Testing:Document" | @@ -32,7 +32,7 @@ Feature: Remove NodeAggregate | nodeName | "document" | # We also want to add a child node to make sure it is correctly removed when the parent is removed # Node /document/child-document - And the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: + And the command CreateNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "nodimus-prime" | | nodeTypeName | "Neos.ContentRepository.Testing:Document" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/DimensionMismatch.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/DimensionMismatch.feature index 6f10381a3cb..23347f92229 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/DimensionMismatch.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/DimensionMismatch.feature @@ -30,7 +30,7 @@ Feature: Dimension mismatch Scenario: Generalization detection # Node /document - When the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: + When the command CreateNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "sir-david-nodenborough" | | nodeTypeName | "Neos.ContentRepository.Testing:Document" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/Properties.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/Properties.feature index bd29ba2e76e..aaf81876ec4 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/Properties.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/Properties.feature @@ -28,7 +28,7 @@ Feature: Properties | nodeAggregateId | "lady-eleonode-rootford" | | nodeTypeName | "Neos.ContentRepository:Root" | # Node /document - When the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: + When the command CreateNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "sir-david-nodenborough" | | nodeTypeName | "Neos.ContentRepository.Testing:Document" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/TetheredNodesReordering.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/TetheredNodesReordering.feature index bb4974d7175..197a15e4989 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/TetheredNodesReordering.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/TetheredNodesReordering.feature @@ -28,7 +28,7 @@ Feature: Tethered Nodes Reordering Structure changes | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | | nodeTypeName | "Neos.ContentRepository:Root" | - And the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: + And the command CreateNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "sir-david-nodenborough" | | nodeTypeName | "Neos.ContentRepository.Testing:Document" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W6-WorkspaceRebasing/02-RebasingWithAutoCreatedNodes.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W6-WorkspaceRebasing/02-RebasingWithAutoCreatedNodes.feature index 39eca525c82..bbf470d1c17 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W6-WorkspaceRebasing/02-RebasingWithAutoCreatedNodes.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W6-WorkspaceRebasing/02-RebasingWithAutoCreatedNodes.feature @@ -61,13 +61,13 @@ Feature: Rebasing auto-created nodes works And I expect this node to be a child of node user-cs-identifier;nody-mc-nodeface;{} # - then, for the auto-created child node, set a property. - When the command "SetSerializedNodeProperties" is executed with payload: - | Key | Value | - | workspaceName | "user-test" | - | nodeAggregateId | $this->currentNodeAggregateId | - | originDimensionSpacePoint | {} | - | propertyValues | {"text": {"value":"Modified","type":"string"}} | - | propertiesToUnset | {} | + When the command "SetNodeProperties" is executed with payload: + | Key | Value | + | workspaceName | "user-test" | + | nodeAggregateId | $this->currentNodeAggregateId | + | originDimensionSpacePoint | {} | + | propertyValues | {"text": "Modified"} | + | propertiesToUnset | {} | # ensure that live is outdated so the rebase is required: When the command CreateNodeAggregateWithNode is executed with payload: @@ -80,8 +80,8 @@ Feature: Rebasing auto-created nodes works # rebase of SetSerializedNodeProperties When the command RebaseWorkspace is executed with payload: - | Key | Value | - | workspaceName | "user-test" | + | Key | Value | + | workspaceName | "user-test" | | rebasedContentStreamId | "user-cs-rebased" | # This should properly work; no error. diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/04-AllFeaturePublication.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/04-AllFeaturePublication.feature index a5e32169667..37de2e73450 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/04-AllFeaturePublication.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/04-AllFeaturePublication.feature @@ -346,7 +346,7 @@ Feature: Publishing hide/show scenario of nodes | newContentStreamId | "user-cs-identifier" | # SETUP: set two new nodes in USER workspace - When the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: + When the command CreateNodeAggregateWithNode is executed with payload: | Key | Value | | workspaceName | "user-test" | | nodeAggregateId | "new1-agg" | @@ -354,7 +354,7 @@ Feature: Publishing hide/show scenario of nodes | originDimensionSpacePoint | {} | | parentNodeAggregateId | "lady-eleonode-rootford" | | nodeName | "foo" | - When the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: + When the command CreateNodeAggregateWithNode is executed with payload: | Key | Value | | workspaceName | "user-test" | | nodeAggregateId | "new2-agg" | diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Command/SetNodeProperties.php b/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Command/SetNodeProperties.php index e2a3d63f587..6e4bbb0aab9 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Command/SetNodeProperties.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Command/SetNodeProperties.php @@ -60,4 +60,17 @@ public static function create(WorkspaceName $workspaceName, NodeAggregateId $nod { return new self($workspaceName, $nodeAggregateId, $originDimensionSpacePoint, $propertyValues); } + + /** + * @param array $array + */ + public static function fromArray(array $array): self + { + return new self( + WorkspaceName::fromString($array['workspaceName']), + NodeAggregateId::fromString($array['nodeAggregateId']), + OriginDimensionSpacePoint::fromArray($array['originDimensionSpacePoint']), + PropertyValuesToWrite::fromArray($array['propertyValues']), + ); + } } diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php index dd899ee4e24..d9d7f66def5 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php @@ -20,10 +20,9 @@ use Neos\ContentRepository\Core\EventStore\EventPersister; use Neos\ContentRepository\Core\EventStore\Events; use Neos\ContentRepository\Core\EventStore\EventsToPublish; -use Neos\ContentRepository\Core\Feature\NodeCreation\Command\CreateNodeAggregateWithNodeAndSerializedProperties; use Neos\ContentRepository\Core\Feature\NodeDisabling\Command\DisableNodeAggregate; use Neos\ContentRepository\Core\Feature\NodeDisabling\Command\EnableNodeAggregate; -use Neos\ContentRepository\Core\Feature\NodeModification\Command\SetSerializedNodeProperties; +use Neos\ContentRepository\Core\Feature\NodeModification\Command\SetNodeProperties; use Neos\ContentRepository\Core\Feature\NodeMove\Command\MoveNodeAggregate; use Neos\ContentRepository\Core\Feature\NodeReferencing\Command\SetNodeReferences; use Neos\ContentRepository\Core\Feature\NodeRenaming\Command\ChangeNodeAggregateName; @@ -104,9 +103,8 @@ protected static function resolveShortCommandName(string $shortCommandName): str 'PublishWorkspace' => PublishWorkspace::class, 'PublishIndividualNodesFromWorkspace' => PublishIndividualNodesFromWorkspace::class, 'RebaseWorkspace' => RebaseWorkspace::class, - 'CreateNodeAggregateWithNodeAndSerializedProperties' => CreateNodeAggregateWithNodeAndSerializedProperties::class, 'ChangeNodeAggregateName' => ChangeNodeAggregateName::class, - 'SetSerializedNodeProperties' => SetSerializedNodeProperties::class, + 'SetNodeProperties' => SetNodeProperties::class, 'DisableNodeAggregate' => DisableNodeAggregate::class, 'EnableNodeAggregate' => EnableNodeAggregate::class, 'TagSubtree' => TagSubtree::class, diff --git a/Neos.Neos/Tests/Behavior/Features/Fusion/ContentCollection.feature b/Neos.Neos/Tests/Behavior/Features/Fusion/ContentCollection.feature index b1edf071af4..ab8a61b3e2b 100644 --- a/Neos.Neos/Tests/Behavior/Features/Fusion/ContentCollection.feature +++ b/Neos.Neos/Tests/Behavior/Features/Fusion/ContentCollection.feature @@ -109,7 +109,7 @@ Feature: Tests for the "Neos.Neos:ContentCollection" Fusion prototype """ Scenario: - When the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: + When the command CreateNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "a1" | | nodeTypeName | "Neos.Neos:Test.DocumentType" | From c36073ea9cd80dae992b0fdceb5d671795ad5406 Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Fri, 8 Nov 2024 16:13:30 +0100 Subject: [PATCH 096/214] Avoid union type in `CommandThatFailedDuringRebase` --- .../Classes/Feature/RebaseableCommand.php | 2 +- .../WorkspaceRebase/CommandThatFailedDuringRebase.php | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/Feature/RebaseableCommand.php b/Neos.ContentRepository.Core/Classes/Feature/RebaseableCommand.php index 93aa84c89cc..42a039167c3 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/RebaseableCommand.php +++ b/Neos.ContentRepository.Core/Classes/Feature/RebaseableCommand.php @@ -21,7 +21,7 @@ final readonly class RebaseableCommand { public function __construct( - public (RebasableToOtherWorkspaceInterface&CommandInterface)|(RebasableToOtherWorkspaceInterface&SerializedCommandInterface) $originalCommand, + public RebasableToOtherWorkspaceInterface $originalCommand, public EventMetadata $initiatingMetaData, public SequenceNumber $originalSequenceNumber ) { diff --git a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/CommandThatFailedDuringRebase.php b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/CommandThatFailedDuringRebase.php index f8b4d579732..96b6a433b11 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/CommandThatFailedDuringRebase.php +++ b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/CommandThatFailedDuringRebase.php @@ -14,8 +14,7 @@ namespace Neos\ContentRepository\Core\Feature\WorkspaceRebase; -use Neos\ContentRepository\Core\CommandHandler\CommandInterface; -use Neos\ContentRepository\Core\CommandHandler\SerializedCommandInterface; +use Neos\ContentRepository\Core\Feature\Common\RebasableToOtherWorkspaceInterface; use Neos\EventStore\Model\Event\SequenceNumber; /** @@ -24,12 +23,12 @@ final readonly class CommandThatFailedDuringRebase { /** - * @param CommandInterface|SerializedCommandInterface $command the command that failed + * @param RebasableToOtherWorkspaceInterface $command the command that failed * @param \Throwable $exception how the command failed * @param SequenceNumber $sequenceNumber the event store sequence number of the event containing the command to be rebased */ public function __construct( - public CommandInterface|SerializedCommandInterface $command, + public RebasableToOtherWorkspaceInterface $command, public \Throwable $exception, private SequenceNumber $sequenceNumber, ) { From 717688daf853245dddf9d79381080baad378f420 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Fri, 8 Nov 2024 11:16:53 +0100 Subject: [PATCH 097/214] TASK: Assert that no events exist on the live content stream ... but later --- .../src/Processors/EventStoreImportProcessor.php | 8 +++----- .../Domain/Import/LiveWorkspaceCreationProcessor.php | 5 ----- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/Neos.ContentRepository.Export/src/Processors/EventStoreImportProcessor.php b/Neos.ContentRepository.Export/src/Processors/EventStoreImportProcessor.php index 7af1561c16f..15e9c718427 100644 --- a/Neos.ContentRepository.Export/src/Processors/EventStoreImportProcessor.php +++ b/Neos.ContentRepository.Export/src/Processors/EventStoreImportProcessor.php @@ -12,9 +12,6 @@ use Neos\ContentRepository\Core\Feature\ContentStreamEventStreamName; use Neos\ContentRepository\Core\Feature\ContentStreamForking\Event\ContentStreamWasForked; use Neos\ContentRepository\Core\Feature\ContentStreamRemoval\Event\ContentStreamWasRemoved; -use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Event\RootWorkspaceWasCreated; -use Neos\ContentRepository\Core\Feature\WorkspaceEventStreamName; -use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepository\Export\Event\ValueObject\ExportedEvent; use Neos\ContentRepository\Export\ProcessingContext; @@ -23,6 +20,7 @@ use Neos\EventStore\Exception\ConcurrencyException; use Neos\EventStore\Model\Event; use Neos\EventStore\Model\Event\EventId; +use Neos\EventStore\Model\Event\Version; use Neos\EventStore\Model\Events; use Neos\EventStore\Model\EventStream\ExpectedVersion; use Neos\Flow\Utility\Algorithms; @@ -100,9 +98,9 @@ public function run(ProcessingContext $context): void $contentStreamStreamName = ContentStreamEventStreamName::fromContentStreamId($workspace->currentContentStreamId)->getEventStreamName(); try { - $this->eventStore->commit($contentStreamStreamName, Events::fromArray($domainEvents), ExpectedVersion::ANY()); + $this->eventStore->commit($contentStreamStreamName, Events::fromArray($domainEvents), ExpectedVersion::fromVersion(Version::first())); } catch (ConcurrencyException $e) { - throw new \RuntimeException(sprintf('Failed to publish %d events because the event stream "%s" already exists (3)', count($domainEvents), $contentStreamStreamName->value), 1729506818, $e); + throw new \RuntimeException(sprintf('Failed to publish %d events because the event stream "%s" for workspace "%s" already contains events.', count($domainEvents), $contentStreamStreamName->value, $workspace->workspaceName->value), 1729506818, $e); } } } diff --git a/Neos.Neos/Classes/Domain/Import/LiveWorkspaceCreationProcessor.php b/Neos.Neos/Classes/Domain/Import/LiveWorkspaceCreationProcessor.php index 97824318b99..788dad93734 100644 --- a/Neos.Neos/Classes/Domain/Import/LiveWorkspaceCreationProcessor.php +++ b/Neos.Neos/Classes/Domain/Import/LiveWorkspaceCreationProcessor.php @@ -40,11 +40,6 @@ public function run(ProcessingContext $context): void { $context->dispatch(Severity::NOTICE, 'Creating live workspace'); $liveWorkspace = $this->contentRepository->findWorkspaceByName(WorkspaceName::forLive()); - $liveContentStreamVersion = $liveWorkspace ? $this->contentRepository->findContentStreamById($liveWorkspace->currentContentStreamId)?->version : null; - if ($liveWorkspace && $liveContentStreamVersion !== 0) { - // todo we cannot use `hasPublishableChanges` here... maybe introduce `hasChanges`? - throw new \RuntimeException('Live workspace already contains content please run "site:pruneAll" before importing.'); - } if ($liveWorkspace !== null) { $context->dispatch(Severity::NOTICE, 'Workspace already exists, skipping'); return; From efb7fbd979cadee2747fce2dd38abf8c15248938 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Fri, 8 Nov 2024 16:18:41 +0100 Subject: [PATCH 098/214] TASK: Remove obsolete `I run the event migration for workspace :workspace` --- .../Tests/Behavior/Bootstrap/FeatureContext.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Bootstrap/FeatureContext.php b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Bootstrap/FeatureContext.php index 1812c6cbeb7..415bf6d8b04 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Bootstrap/FeatureContext.php +++ b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Bootstrap/FeatureContext.php @@ -91,14 +91,13 @@ public function iHaveTheFollowingNodeDataRows(TableNode $nodeDataRows): void public function iRunTheEventMigrationWithRootnodeMapping(string $rootNodeMapping): void { $rootNodeTypeMapping = RootNodeTypeMapping::fromArray(json_decode($rootNodeMapping, true)); - $this->iRunTheEventMigration(null, $rootNodeTypeMapping); + $this->iRunTheEventMigration($rootNodeTypeMapping); } /** * @When I run the event migration - * @When I run the event migration for workspace :workspace */ - public function iRunTheEventMigration(string $workspace = null, RootNodeTypeMapping $rootNodeTypeMapping = null): void + public function iRunTheEventMigration(RootNodeTypeMapping $rootNodeTypeMapping = null): void { $nodeTypeManager = $this->currentContentRepository->getNodeTypeManager(); $propertyMapper = $this->getObject(PropertyMapper::class); From 8e9b33337f180f7d9e653b1ff82f3c6163fe8984 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Fri, 8 Nov 2024 17:00:46 +0100 Subject: [PATCH 099/214] TASK: Add docs for Serialized Command --- .../Classes/CommandHandler/CommandInterface.php | 4 ++-- .../CommandHandler/SerializedCommandInterface.php | 10 ++++++++++ .../Common/RebasableToOtherWorkspaceInterface.php | 3 +-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandInterface.php b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandInterface.php index 8264248a6f9..ba5d5f1c350 100644 --- a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandInterface.php +++ b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandInterface.php @@ -5,9 +5,9 @@ namespace Neos\ContentRepository\Core\CommandHandler; /** - * Common (marker) interface for all commands of the Content Repository + * Common (marker) interface for all commands of the content repository * - * @internal because extra commands are no extension point + * @internal sealed interface. Custom commands cannot be handled and are no extension point! */ interface CommandInterface { diff --git a/Neos.ContentRepository.Core/Classes/CommandHandler/SerializedCommandInterface.php b/Neos.ContentRepository.Core/Classes/CommandHandler/SerializedCommandInterface.php index 342de7d5d5b..24a948730db 100644 --- a/Neos.ContentRepository.Core/Classes/CommandHandler/SerializedCommandInterface.php +++ b/Neos.ContentRepository.Core/Classes/CommandHandler/SerializedCommandInterface.php @@ -5,8 +5,18 @@ namespace Neos\ContentRepository\Core\CommandHandler; /** + * Common (marker) interface for all commands that need to be serialized for rebasing + * + * During a rebase, the command (either {@see CommandInterface} or this serialized counterpart) will be deserialized + * from array {@see SerializedCommandInterface::fromArray()} and reapplied {@see CommandSimulator} + * * @internal */ interface SerializedCommandInterface { + /** + * called during deserialization from metadata + * @param array $array + */ + public static function fromArray(array $array): self; } diff --git a/Neos.ContentRepository.Core/Classes/Feature/Common/RebasableToOtherWorkspaceInterface.php b/Neos.ContentRepository.Core/Classes/Feature/Common/RebasableToOtherWorkspaceInterface.php index b0c643f6043..20320903a8a 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/Common/RebasableToOtherWorkspaceInterface.php +++ b/Neos.ContentRepository.Core/Classes/Feature/Common/RebasableToOtherWorkspaceInterface.php @@ -19,8 +19,7 @@ use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; /** - * This interface is implemented by **commands** which can be rebased to other Content Streams. This is basically all - * node-based commands. + * This interface is implemented by **commands** which can be rebased to other workspaces. * * Reminder: a rebase can fail, because the target content stream might contain conflicting changes. * From e9d576b3c345723b7832c1510b907fcbfee8b23b Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Fri, 8 Nov 2024 17:15:42 +0100 Subject: [PATCH 100/214] TASK: Remove `SerializedCommandInterface` again and use `RebasableToOtherWorkspaceInterface` directly --- .../Classes/CommandHandler/CommandBus.php | 3 ++- .../CommandHandlerInterface.php | 5 +++-- .../CommandHandler/CommandInterface.php | 2 +- .../SerializedCommandInterface.php | 22 ------------------- .../RebasableToOtherWorkspaceInterface.php | 14 +++++++----- .../DimensionSpaceCommandHandler.php | 10 ++++----- .../Feature/NodeAggregateCommandHandler.php | 8 +++---- ...gregateWithNodeAndSerializedProperties.php | 2 -- .../NodeDuplicationCommandHandler.php | 13 +++++------ .../Command/SetSerializedNodeProperties.php | 2 -- .../Command/SetSerializedNodeReferences.php | 2 -- .../Classes/Feature/RebaseableCommand.php | 4 +--- .../Feature/WorkspaceCommandHandler.php | 9 +++----- 13 files changed, 34 insertions(+), 62 deletions(-) delete mode 100644 Neos.ContentRepository.Core/Classes/CommandHandler/SerializedCommandInterface.php diff --git a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandBus.php b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandBus.php index 3cbdec086b7..5621e17fd2d 100644 --- a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandBus.php +++ b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandBus.php @@ -6,6 +6,7 @@ use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\EventStore\EventsToPublish; +use Neos\ContentRepository\Core\Feature\Common\RebasableToOtherWorkspaceInterface; /** * Implementation Detail of {@see ContentRepository::handle}, which does the command dispatching to the different @@ -31,7 +32,7 @@ public function __construct( /** * @return EventsToPublish|\Generator */ - public function handle(CommandInterface|SerializedCommandInterface $command): EventsToPublish|\Generator + public function handle(CommandInterface|RebasableToOtherWorkspaceInterface $command): EventsToPublish|\Generator { // multiple handlers must not handle the same command foreach ($this->handlers as $handler) { diff --git a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHandlerInterface.php b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHandlerInterface.php index 4ad1e53919b..93cba7111ef 100644 --- a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHandlerInterface.php +++ b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHandlerInterface.php @@ -5,6 +5,7 @@ namespace Neos\ContentRepository\Core\CommandHandler; use Neos\ContentRepository\Core\EventStore\EventsToPublish; +use Neos\ContentRepository\Core\Feature\Common\RebasableToOtherWorkspaceInterface; /** * Common interface for all Content Repository command handlers @@ -15,7 +16,7 @@ */ interface CommandHandlerInterface { - public function canHandle(CommandInterface|SerializedCommandInterface $command): bool; + public function canHandle(CommandInterface|RebasableToOtherWorkspaceInterface $command): bool; /** * "simple" command handlers return EventsToPublish directly @@ -25,5 +26,5 @@ public function canHandle(CommandInterface|SerializedCommandInterface $command): * * @return EventsToPublish|\Generator */ - public function handle(CommandInterface|SerializedCommandInterface $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish|\Generator; + public function handle(CommandInterface|RebasableToOtherWorkspaceInterface $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish|\Generator; } diff --git a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandInterface.php b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandInterface.php index ba5d5f1c350..dcd162afb10 100644 --- a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandInterface.php +++ b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandInterface.php @@ -5,7 +5,7 @@ namespace Neos\ContentRepository\Core\CommandHandler; /** - * Common (marker) interface for all commands of the content repository + * Common (marker) interface for all api commands of the content repository * * @internal sealed interface. Custom commands cannot be handled and are no extension point! */ diff --git a/Neos.ContentRepository.Core/Classes/CommandHandler/SerializedCommandInterface.php b/Neos.ContentRepository.Core/Classes/CommandHandler/SerializedCommandInterface.php deleted file mode 100644 index 24a948730db..00000000000 --- a/Neos.ContentRepository.Core/Classes/CommandHandler/SerializedCommandInterface.php +++ /dev/null @@ -1,22 +0,0 @@ - $array - */ - public static function fromArray(array $array): self; -} diff --git a/Neos.ContentRepository.Core/Classes/Feature/Common/RebasableToOtherWorkspaceInterface.php b/Neos.ContentRepository.Core/Classes/Feature/Common/RebasableToOtherWorkspaceInterface.php index 20320903a8a..7a33844a0ac 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/Common/RebasableToOtherWorkspaceInterface.php +++ b/Neos.ContentRepository.Core/Classes/Feature/Common/RebasableToOtherWorkspaceInterface.php @@ -14,22 +14,26 @@ namespace Neos\ContentRepository\Core\Feature\Common; -use Neos\ContentRepository\Core\CommandHandler\CommandInterface; -use Neos\ContentRepository\Core\CommandHandler\SerializedCommandInterface; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; /** - * This interface is implemented by **commands** which can be rebased to other workspaces. + * Common (marker) interface for all **commands** that need to be serialized for rebasing to other workspaces + * + * If the api command {@see CommandInterface} is serializable on its own it will directly implement this interface. + * For complex commands a serialized counterpart - which is not api - will be build which implements this interface. + * + * During a rebase, the command (either the original {@see CommandInterface} or its serialized counterpart) will be deserialized + * from array {@see SerializedCommandInterface::fromArray()} and reapplied via the {@see CommandSimulator} * * Reminder: a rebase can fail, because the target content stream might contain conflicting changes. * * @internal used internally for the rebasing mechanism of content streams */ -interface RebasableToOtherWorkspaceInterface +interface RebasableToOtherWorkspaceInterface extends \JsonSerializable { public function createCopyForWorkspace( WorkspaceName $targetWorkspaceName, - ): (RebasableToOtherWorkspaceInterface&CommandInterface)|(RebasableToOtherWorkspaceInterface&SerializedCommandInterface); + ): self; /** * called during deserialization from metadata diff --git a/Neos.ContentRepository.Core/Classes/Feature/DimensionSpaceAdjustment/DimensionSpaceCommandHandler.php b/Neos.ContentRepository.Core/Classes/Feature/DimensionSpaceAdjustment/DimensionSpaceCommandHandler.php index 411e9801dd9..72695af98c7 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/DimensionSpaceAdjustment/DimensionSpaceCommandHandler.php +++ b/Neos.ContentRepository.Core/Classes/Feature/DimensionSpaceAdjustment/DimensionSpaceCommandHandler.php @@ -15,9 +15,8 @@ */ use Neos\ContentRepository\Core\CommandHandler\CommandHandlerInterface; -use Neos\ContentRepository\Core\CommandHandler\CommandInterface; use Neos\ContentRepository\Core\CommandHandler\CommandHandlingDependencies; -use Neos\ContentRepository\Core\CommandHandler\SerializedCommandInterface; +use Neos\ContentRepository\Core\CommandHandler\CommandInterface; use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\DimensionSpace\ContentDimensionZookeeper; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; @@ -27,13 +26,14 @@ use Neos\ContentRepository\Core\DimensionSpace\VariantType; use Neos\ContentRepository\Core\EventStore\Events; use Neos\ContentRepository\Core\EventStore\EventsToPublish; -use Neos\ContentRepository\Core\Feature\RebaseableCommand; +use Neos\ContentRepository\Core\Feature\Common\RebasableToOtherWorkspaceInterface; use Neos\ContentRepository\Core\Feature\ContentStreamEventStreamName; use Neos\ContentRepository\Core\Feature\DimensionSpaceAdjustment\Command\AddDimensionShineThrough; use Neos\ContentRepository\Core\Feature\DimensionSpaceAdjustment\Command\MoveDimensionSpacePoint; use Neos\ContentRepository\Core\Feature\DimensionSpaceAdjustment\Event\DimensionShineThroughWasAdded; use Neos\ContentRepository\Core\Feature\DimensionSpaceAdjustment\Event\DimensionSpacePointWasMoved; use Neos\ContentRepository\Core\Feature\DimensionSpaceAdjustment\Exception\DimensionSpacePointAlreadyExists; +use Neos\ContentRepository\Core\Feature\RebaseableCommand; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\EventStore\Model\EventStream\ExpectedVersion; @@ -49,12 +49,12 @@ public function __construct( ) { } - public function canHandle(CommandInterface|SerializedCommandInterface $command): bool + public function canHandle(CommandInterface|RebasableToOtherWorkspaceInterface $command): bool { return method_exists($this, 'handle' . (new \ReflectionClass($command))->getShortName()); } - public function handle(CommandInterface|SerializedCommandInterface $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish + public function handle(CommandInterface|RebasableToOtherWorkspaceInterface $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish { /** @phpstan-ignore-next-line */ return match ($command::class) { diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeAggregateCommandHandler.php b/Neos.ContentRepository.Core/Classes/Feature/NodeAggregateCommandHandler.php index a54aefa9cae..f014440718c 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeAggregateCommandHandler.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeAggregateCommandHandler.php @@ -15,14 +15,14 @@ namespace Neos\ContentRepository\Core\Feature; use Neos\ContentRepository\Core\CommandHandler\CommandHandlerInterface; -use Neos\ContentRepository\Core\CommandHandler\CommandInterface; use Neos\ContentRepository\Core\CommandHandler\CommandHandlingDependencies; -use Neos\ContentRepository\Core\CommandHandler\SerializedCommandInterface; +use Neos\ContentRepository\Core\CommandHandler\CommandInterface; use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\DimensionSpace; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; use Neos\ContentRepository\Core\EventStore\EventsToPublish; use Neos\ContentRepository\Core\Feature\Common\ConstraintChecks; +use Neos\ContentRepository\Core\Feature\Common\RebasableToOtherWorkspaceInterface; use Neos\ContentRepository\Core\Feature\Common\TetheredNodeInternals; use Neos\ContentRepository\Core\Feature\NodeCreation\Command\CreateNodeAggregateWithNode; use Neos\ContentRepository\Core\Feature\NodeCreation\Command\CreateNodeAggregateWithNodeAndSerializedProperties; @@ -87,12 +87,12 @@ public function __construct( ) { } - public function canHandle(CommandInterface|SerializedCommandInterface $command): bool + public function canHandle(CommandInterface|RebasableToOtherWorkspaceInterface $command): bool { return method_exists($this, 'handle' . (new \ReflectionClass($command))->getShortName()); } - public function handle(CommandInterface|SerializedCommandInterface $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish + public function handle(CommandInterface|RebasableToOtherWorkspaceInterface $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish { /** @phpstan-ignore-next-line */ return match ($command::class) { diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/Command/CreateNodeAggregateWithNodeAndSerializedProperties.php b/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/Command/CreateNodeAggregateWithNodeAndSerializedProperties.php index 239120db1f0..3aa8d471d0e 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/Command/CreateNodeAggregateWithNodeAndSerializedProperties.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/Command/CreateNodeAggregateWithNodeAndSerializedProperties.php @@ -14,7 +14,6 @@ namespace Neos\ContentRepository\Core\Feature\NodeCreation\Command; -use Neos\ContentRepository\Core\CommandHandler\SerializedCommandInterface; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; use Neos\ContentRepository\Core\Feature\Common\MatchableWithNodeIdToPublishOrDiscardInterface; use Neos\ContentRepository\Core\Feature\Common\RebasableToOtherWorkspaceInterface; @@ -34,7 +33,6 @@ * @internal implementation detail, use {@see CreateNodeAggregateWithNode} instead. */ final readonly class CreateNodeAggregateWithNodeAndSerializedProperties implements - SerializedCommandInterface, \JsonSerializable, MatchableWithNodeIdToPublishOrDiscardInterface, RebasableToOtherWorkspaceInterface diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/NodeDuplicationCommandHandler.php b/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/NodeDuplicationCommandHandler.php index 31a5a897af8..cce5dd61e29 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/NodeDuplicationCommandHandler.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/NodeDuplicationCommandHandler.php @@ -14,10 +14,8 @@ namespace Neos\ContentRepository\Core\Feature\NodeDuplication; -use Neos\ContentRepository\Core\CommandHandler\CommandHandlingDependencies; -use Neos\ContentRepository\Core\CommandHandler\SerializedCommandInterface; -use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphInterface; use Neos\ContentRepository\Core\CommandHandler\CommandHandlerInterface; +use Neos\ContentRepository\Core\CommandHandler\CommandHandlingDependencies; use Neos\ContentRepository\Core\CommandHandler\CommandInterface; use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\DimensionSpace\ContentDimensionZookeeper; @@ -28,17 +26,18 @@ use Neos\ContentRepository\Core\EventStore\EventsToPublish; use Neos\ContentRepository\Core\Feature\Common\ConstraintChecks; use Neos\ContentRepository\Core\Feature\Common\InterdimensionalSiblings; -use Neos\ContentRepository\Core\Feature\RebaseableCommand; use Neos\ContentRepository\Core\Feature\Common\NodeCreationInternals; +use Neos\ContentRepository\Core\Feature\Common\RebasableToOtherWorkspaceInterface; use Neos\ContentRepository\Core\Feature\ContentStreamEventStreamName; use Neos\ContentRepository\Core\Feature\NodeCreation\Event\NodeAggregateWithNodeWasCreated; use Neos\ContentRepository\Core\Feature\NodeDuplication\Command\CopyNodesRecursively; use Neos\ContentRepository\Core\Feature\NodeDuplication\Dto\NodeSubtreeSnapshot; +use Neos\ContentRepository\Core\Feature\RebaseableCommand; use Neos\ContentRepository\Core\NodeType\NodeTypeManager; +use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphInterface; use Neos\ContentRepository\Core\SharedModel\Exception\NodeConstraintException; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Node\NodeName; -use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; /** * @internal from userland, you'll use ContentRepository::handle to dispatch commands @@ -65,12 +64,12 @@ protected function getAllowedDimensionSubspace(): DimensionSpacePointSet return $this->contentDimensionZookeeper->getAllowedDimensionSubspace(); } - public function canHandle(CommandInterface|SerializedCommandInterface $command): bool + public function canHandle(CommandInterface|RebasableToOtherWorkspaceInterface $command): bool { return method_exists($this, 'handle' . (new \ReflectionClass($command))->getShortName()); } - public function handle(CommandInterface|SerializedCommandInterface $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish + public function handle(CommandInterface|RebasableToOtherWorkspaceInterface $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish { /** @phpstan-ignore-next-line */ return match ($command::class) { diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Command/SetSerializedNodeProperties.php b/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Command/SetSerializedNodeProperties.php index f5d382f0387..b3d38bf88dc 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Command/SetSerializedNodeProperties.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Command/SetSerializedNodeProperties.php @@ -14,7 +14,6 @@ namespace Neos\ContentRepository\Core\Feature\NodeModification\Command; -use Neos\ContentRepository\Core\CommandHandler\SerializedCommandInterface; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; use Neos\ContentRepository\Core\Feature\Common\MatchableWithNodeIdToPublishOrDiscardInterface; use Neos\ContentRepository\Core\Feature\Common\RebasableToOtherWorkspaceInterface; @@ -32,7 +31,6 @@ * @internal implementation detail, use {@see SetNodeProperties} instead. */ final readonly class SetSerializedNodeProperties implements - SerializedCommandInterface, \JsonSerializable, MatchableWithNodeIdToPublishOrDiscardInterface, RebasableToOtherWorkspaceInterface diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Command/SetSerializedNodeReferences.php b/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Command/SetSerializedNodeReferences.php index dd075e4e547..5e95099f806 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Command/SetSerializedNodeReferences.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Command/SetSerializedNodeReferences.php @@ -14,7 +14,6 @@ namespace Neos\ContentRepository\Core\Feature\NodeReferencing\Command; -use Neos\ContentRepository\Core\CommandHandler\SerializedCommandInterface; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; use Neos\ContentRepository\Core\Feature\Common\MatchableWithNodeIdToPublishOrDiscardInterface; use Neos\ContentRepository\Core\Feature\Common\RebasableToOtherWorkspaceInterface; @@ -31,7 +30,6 @@ * @internal implementation detail, use {@see SetNodeReferences} instead. */ final readonly class SetSerializedNodeReferences implements - SerializedCommandInterface, \JsonSerializable, MatchableWithNodeIdToPublishOrDiscardInterface, RebasableToOtherWorkspaceInterface diff --git a/Neos.ContentRepository.Core/Classes/Feature/RebaseableCommand.php b/Neos.ContentRepository.Core/Classes/Feature/RebaseableCommand.php index 42a039167c3..210f91b8b8a 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/RebaseableCommand.php +++ b/Neos.ContentRepository.Core/Classes/Feature/RebaseableCommand.php @@ -4,8 +4,6 @@ namespace Neos\ContentRepository\Core\Feature; -use Neos\ContentRepository\Core\CommandHandler\CommandInterface; -use Neos\ContentRepository\Core\CommandHandler\SerializedCommandInterface; use Neos\ContentRepository\Core\EventStore\DecoratedEvent; use Neos\ContentRepository\Core\EventStore\Events; use Neos\ContentRepository\Core\EventStore\InitiatingEventMetadata; @@ -44,7 +42,7 @@ public static function extractFromEventMetaData(EventMetadata $eventMetadata, Se ), 1547815341); } /** @var class-string $commandToRebaseClass */ - /** @var (RebasableToOtherWorkspaceInterface&CommandInterface)|(RebasableToOtherWorkspaceInterface&SerializedCommandInterface) $commandInstance */ + /** @var RebasableToOtherWorkspaceInterface $commandInstance */ $commandInstance = $commandToRebaseClass::fromArray($commandToRebasePayload); return new self( $commandInstance, diff --git a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php index dc8c12c7dca..cbec3e0d8e0 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php +++ b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php @@ -18,17 +18,15 @@ use Neos\ContentRepository\Core\CommandHandler\CommandHandlingDependencies; use Neos\ContentRepository\Core\CommandHandler\CommandInterface; use Neos\ContentRepository\Core\CommandHandler\CommandSimulatorFactory; -use Neos\ContentRepository\Core\CommandHandler\SerializedCommandInterface; use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\EventStore\DecoratedEvent; -use Neos\ContentRepository\Core\EventStore\EventInterface; use Neos\ContentRepository\Core\EventStore\EventNormalizer; use Neos\ContentRepository\Core\EventStore\Events; use Neos\ContentRepository\Core\EventStore\EventsToPublish; use Neos\ContentRepository\Core\Feature\Common\PublishableToWorkspaceInterface; +use Neos\ContentRepository\Core\Feature\Common\RebasableToOtherWorkspaceInterface; use Neos\ContentRepository\Core\Feature\ContentStreamClosing\Event\ContentStreamWasClosed; use Neos\ContentRepository\Core\Feature\ContentStreamClosing\Event\ContentStreamWasReopened; -use Neos\ContentRepository\Core\Feature\ContentStreamForking\Event\ContentStreamWasForked; use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Command\CreateRootWorkspace; use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Command\CreateWorkspace; use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Event\RootWorkspaceWasCreated; @@ -63,7 +61,6 @@ use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceStatus; use Neos\EventStore\EventStoreInterface; -use Neos\EventStore\Model\Event\EventType; use Neos\EventStore\Model\Event\SequenceNumber; use Neos\EventStore\Model\Event\Version; use Neos\EventStore\Model\EventStream\EventStreamInterface; @@ -83,12 +80,12 @@ public function __construct( ) { } - public function canHandle(CommandInterface|SerializedCommandInterface $command): bool + public function canHandle(CommandInterface|RebasableToOtherWorkspaceInterface $command): bool { return method_exists($this, 'handle' . (new \ReflectionClass($command))->getShortName()); } - public function handle(CommandInterface|SerializedCommandInterface $command, CommandHandlingDependencies $commandHandlingDependencies): \Generator + public function handle(CommandInterface|RebasableToOtherWorkspaceInterface $command, CommandHandlingDependencies $commandHandlingDependencies): \Generator { /** @phpstan-ignore-next-line */ return match ($command::class) { From ddf2a5289187048b57daef3e550798f84dab5105 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Fri, 8 Nov 2024 19:38:39 +0100 Subject: [PATCH 101/214] TASK: Adjust documentation of `CommandInterface` --- .../Classes/CommandHandler/CommandInterface.php | 7 ++++++- .../Feature/Common/RebasableToOtherWorkspaceInterface.php | 4 +++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandInterface.php b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandInterface.php index dcd162afb10..a29d2c85a7a 100644 --- a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandInterface.php +++ b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandInterface.php @@ -4,8 +4,13 @@ namespace Neos\ContentRepository\Core\CommandHandler; +use Neos\ContentRepository\Core\Feature\Common\RebasableToOtherWorkspaceInterface; + /** - * Common (marker) interface for all api commands of the content repository + * Common (marker) interface for all public api commands of the content repository + * + * Note that this interface does not mark all commands. + * Complex public commands will not be serializable on its own and are required to be serialized into a {@see RebasableToOtherWorkspaceInterface} * * @internal sealed interface. Custom commands cannot be handled and are no extension point! */ diff --git a/Neos.ContentRepository.Core/Classes/Feature/Common/RebasableToOtherWorkspaceInterface.php b/Neos.ContentRepository.Core/Classes/Feature/Common/RebasableToOtherWorkspaceInterface.php index 7a33844a0ac..f0f2a10a6c3 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/Common/RebasableToOtherWorkspaceInterface.php +++ b/Neos.ContentRepository.Core/Classes/Feature/Common/RebasableToOtherWorkspaceInterface.php @@ -14,6 +14,8 @@ namespace Neos\ContentRepository\Core\Feature\Common; +use Neos\ContentRepository\Core\CommandHandler\CommandInterface; +use Neos\ContentRepository\Core\CommandHandler\CommandSimulator; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; /** @@ -23,7 +25,7 @@ * For complex commands a serialized counterpart - which is not api - will be build which implements this interface. * * During a rebase, the command (either the original {@see CommandInterface} or its serialized counterpart) will be deserialized - * from array {@see SerializedCommandInterface::fromArray()} and reapplied via the {@see CommandSimulator} + * from array {@see RebasableToOtherWorkspaceInterface::fromArray()} and reapplied via the {@see CommandSimulator} * * Reminder: a rebase can fail, because the target content stream might contain conflicting changes. * From ea6719504b46330fde2d28741eb90acf358c6050 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Fri, 8 Nov 2024 20:30:26 +0100 Subject: [PATCH 102/214] TASK: Adjust documentation and todos in legacy migration --- .../Classes/Command/SiteCommandController.php | 25 ++++++++++++------- .../Classes/LegacyExportService.php | 3 +-- .../Processors/EventExportProcessor.php | 6 +---- .../Classes/Command/SiteCommandController.php | 6 ++--- README.md | 12 ++------- 5 files changed, 23 insertions(+), 29 deletions(-) diff --git a/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/SiteCommandController.php b/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/SiteCommandController.php index ba608344a5e..8f57f0dc526 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/SiteCommandController.php +++ b/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/SiteCommandController.php @@ -21,8 +21,6 @@ use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Export\Severity; use Neos\ContentRepository\LegacyNodeMigration\LegacyExportServiceFactory; -use Neos\ContentRepository\LegacyNodeMigration\LegacyMigrationService; -use Neos\ContentRepository\LegacyNodeMigration\LegacyMigrationServiceFactory; use Neos\ContentRepository\LegacyNodeMigration\RootNodeTypeMapping; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Cli\CommandController; @@ -47,10 +45,13 @@ public function __construct( /** * Migrate from the Legacy CR * + * Note that the dimension configuration and the node type schema must be migrated of the content repository to import to and it must be setup. + * + * @param string $contentRepository The target content repository that will be used for importing into * @param string|null $config JSON encoded configuration, for example '{"dbal": {"dbname": "some-other-db"}, "resourcesPath": "/some/absolute/path", "rootNodes": {"/sites": "Neos.Neos:Sites", "/other": "My.Package:SomeOtherRoot"}}' * @throws \Exception */ - public function migrateLegacyDataCommand(string $contentRepository = 'default', bool $verbose = false, string $config = null): void + public function migrateLegacyDataCommand(string $contentRepository = 'default', string $config = null, bool $verbose = false, ): void { if ($config !== null) { try { @@ -69,7 +70,7 @@ public function migrateLegacyDataCommand(string $contentRepository = 'default', $resourcesPath = $this->determineResourcesPath(); $rootNodes = $this->getDefaultRootNodes(); if (!$this->output->askConfirmation(sprintf('Do you want to migrate nodes from the current database "%s@%s" (y/n)? ', $this->connection->getParams()['dbname'] ?? '?', $this->connection->getParams()['host'] ?? '?'))) { - $connection = $this->adjustDataBaseConnection($this->connection); + $connection = $this->adjustDatabaseConnection($this->connection); } else { $connection = $this->connection; } @@ -96,6 +97,9 @@ public function migrateLegacyDataCommand(string $contentRepository = 'default', $this->createOnMessageClosure($verbose) ); + $this->outputLine('Migrated data. Importing into new content repository ...'); + + // todo check if cr is setup before!!! do not fail here!!! $this->siteImportService->importFromPath( $contentRepositoryId, $temporaryFilePath, @@ -111,11 +115,14 @@ public function migrateLegacyDataCommand(string $contentRepository = 'default', /** * Export from the Legacy CR into a specified directory path * + * Note that the dimension configuration and the node type schema must be migrated of the reference content repository + * + * @param string $contentRepository The reference content repository that can later be used for importing into * @param string $path The path to the directory, will be created if missing * @param string|null $config JSON encoded configuration, for example '{"dbal": {"dbname": "some-other-db"}, "resourcesPath": "/some/absolute/path", "rootNodes": {"/sites": "Neos.Neos:Sites", "/other": "My.Package:SomeOtherRoot"}}' * @throws \Exception */ - public function exportLegacyDataCommand(string $path, bool $verbose = false, string $config = null): void + public function exportLegacyDataCommand(string $path, string $contentRepository = 'default', string $config = null, bool $verbose = false): void { if ($config !== null) { try { @@ -134,7 +141,7 @@ public function exportLegacyDataCommand(string $path, bool $verbose = false, str $resourcesPath = $this->determineResourcesPath(); $rootNodes = $this->getDefaultRootNodes(); if (!$this->output->askConfirmation(sprintf('Do you want to migrate nodes from the current database "%s@%s" (y/n)? ', $this->connection->getParams()['dbname'] ?? '?', $this->connection->getParams()['host'] ?? '?'))) { - $connection = $this->adjustDataBaseConnection($this->connection); + $connection = $this->adjustDatabaseConnection($this->connection); } else { $connection = $this->connection; } @@ -143,7 +150,7 @@ public function exportLegacyDataCommand(string $path, bool $verbose = false, str Files::createDirectoryRecursively($path); $legacyExportService = $this->contentRepositoryRegistry->buildService( - ContentRepositoryId::fromString('default'), + ContentRepositoryId::fromString($contentRepository), new LegacyExportServiceFactory( $connection, $resourcesPath, @@ -164,7 +171,7 @@ public function exportLegacyDataCommand(string $path, bool $verbose = false, str /** * @throws DBALException */ - private function adjustDataBaseConnection(Connection $connection): Connection + private function adjustDatabaseConnection(Connection $connection): Connection { $connectionParams = $connection->getParams(); $connectionParams['driver'] = $this->output->select(sprintf('Driver? [%s] ', $connectionParams['driver'] ?? ''), ['pdo_mysql', 'pdo_sqlite', 'pdo_pgsql'], $connectionParams['driver'] ?? null); @@ -189,7 +196,7 @@ private function verifyDatabaseConnection(Connection $connection): void } catch (ConnectionException $exception) { $this->outputLine('Failed to connect to database "%s": %s', [$connection->getDatabase(), $exception->getMessage()]); $this->outputLine('Please verify connection parameters...'); - $this->adjustDataBaseConnection($connection); + $this->adjustDatabaseConnection($connection); } } while (true); } diff --git a/Neos.ContentRepository.LegacyNodeMigration/Classes/LegacyExportService.php b/Neos.ContentRepository.LegacyNodeMigration/Classes/LegacyExportService.php index 49399f4eb3e..dc59b6a79ce 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Classes/LegacyExportService.php +++ b/Neos.ContentRepository.LegacyNodeMigration/Classes/LegacyExportService.php @@ -22,7 +22,6 @@ use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface; use Neos\ContentRepository\Core\Infrastructure\Property\PropertyConverter; use Neos\ContentRepository\Core\NodeType\NodeTypeManager; -use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Export\Asset\Adapters\DbalAssetLoader; use Neos\ContentRepository\Export\Asset\Adapters\FileSystemResourceLoader; use Neos\ContentRepository\Export\Asset\AssetExporter; @@ -62,7 +61,7 @@ public function exportToPath(string $path, \Closure $onProcessor, \Closure $onMe $processors = Processors::fromArray([ 'Exporting assets' => new AssetExportProcessor($this->nodeTypeManager, $assetExporter, new NodeDataLoader($this->connection)), - 'Exporting node data' => new EventExportProcessor( $this->nodeTypeManager, $this->propertyMapper, $this->propertyConverter, $this->interDimensionalVariationGraph, $this->eventNormalizer, $this->rootNodeTypeMapping,new NodeDataLoader($this->connection)), + 'Exporting node data' => new EventExportProcessor($this->nodeTypeManager, $this->propertyMapper, $this->propertyConverter, $this->interDimensionalVariationGraph, $this->eventNormalizer, $this->rootNodeTypeMapping, new NodeDataLoader($this->connection)), 'Exporting sites data' => new SitesExportProcessor(new SiteDataLoader($this->connection), new DomainDataLoader($this->connection)), ]); diff --git a/Neos.ContentRepository.LegacyNodeMigration/Classes/Processors/EventExportProcessor.php b/Neos.ContentRepository.LegacyNodeMigration/Classes/Processors/EventExportProcessor.php index 7b9447904a6..3020baf2b2a 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Classes/Processors/EventExportProcessor.php +++ b/Neos.ContentRepository.LegacyNodeMigration/Classes/Processors/EventExportProcessor.php @@ -105,11 +105,7 @@ public function run(ProcessingContext $context): void continue; } } - try { - $this->processNodeData($context, $nodeDataRow); - } catch (MigrationException $e) { - throw new \RuntimeException($e->getMessage(), 1729506899, $e); - } + $this->processNodeData($context, $nodeDataRow); } // Set References, now when the full import is done. foreach ($this->nodeReferencesWereSetEvents as $nodeReferencesWereSetEvent) { diff --git a/Neos.Neos/Classes/Command/SiteCommandController.php b/Neos.Neos/Classes/Command/SiteCommandController.php index 08199679ae6..59601e38d6f 100644 --- a/Neos.Neos/Classes/Command/SiteCommandController.php +++ b/Neos.Neos/Classes/Command/SiteCommandController.php @@ -161,13 +161,13 @@ public function createCommand($name, $packageKey, $nodeType, $nodeName = null, $ * This command allows importing sites from the given path/package. The format must * be identical to that produced by the exportAll command. * - * !!! The live workspace has to be empty prior to importing. !!! - * * If a path is specified, this command expects the corresponding directory to contain the exported files * * If a package key is specified, this command expects the export files to be located in the private resources * directory of the given package (Resources/Private/Content). * + * **Note that the live workspace has to be empty prior to importing.** + * * @param string|null $packageKey Package key specifying the package containing the sites content * @param string|null $path relative or absolute path and filename to the export files * @return void @@ -199,7 +199,7 @@ public function importAllCommand(string $packageKey = null, string $path = null, * Export sites * * This command exports all sites of the content repository. - ** + * * If a path is specified, this command creates the directory if needed and exports into that. * * If a package key is specified, this command exports to the private resources diff --git a/README.md b/README.md index ea185d368e3..5df252f9b2e 100644 --- a/README.md +++ b/README.md @@ -70,22 +70,14 @@ You can chose from one of the following options: #### Migrating an existing (Neos < 9.0) Site ``` bash -# WORKAROUND: for now, you still need to create a site (which must match the root node name) -# !! in the future, you would want to import *INTO* a given site (and replace its root node) -./flow site:create neosdemo Neos.Demo Neos.Demo:Document.Homepage - -# the following config points to a Neos 8.0 database (adjust to your needs), created by -# the legacy "./flow site:import Neos.Demo" command. +# the following config points to a Neos 8.0 database (adjust to your needs) ./flow site:migrateLegacyData --config '{"dbal": {"dbname": "neos80"}, "resourcesPath": "/path/to/neos-8.0/Data/Persistent/Resources"}' ``` #### Importing an existing (Neos >= 9.0) Site from an Export ``` bash -# make sure this cr is empty -./flow site:pruneAll -# import the event stream from the Neos.Demo package -./flow site:importAll Packages/Sites/Neos.Demo/Resources/Private/Content +./flow site:importAll --package-key Neos.Demo ``` ### Running Neos From 210a087cd6a580ca45671e85175f0753a4edd9b7 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Fri, 8 Nov 2024 20:49:41 +0100 Subject: [PATCH 103/214] TASK: Remove obsolete `ProjectionReplayProcessor` --- .../Processors/ProjectionReplayProcessor.php | 25 ------------------- 1 file changed, 25 deletions(-) delete mode 100644 Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionReplayProcessor.php diff --git a/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionReplayProcessor.php b/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionReplayProcessor.php deleted file mode 100644 index b77110a42a4..00000000000 --- a/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionReplayProcessor.php +++ /dev/null @@ -1,25 +0,0 @@ -projectionService->replayAllProjections(CatchUpOptions::create()); - } -} From d79fe107f80ac4dbb779341aa28eebd70049628f Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 9 Nov 2024 11:27:01 +0100 Subject: [PATCH 104/214] TASK: Overhaul api of `CommandThatFailedDuringRebase` --- .../CommandThatFailedDuringRebase.php | 59 +++++++++++++++++-- .../Exception/WorkspaceRebaseFailed.php | 8 +-- ...ricCommandExecutionAndEventPublication.php | 4 +- 3 files changed, 60 insertions(+), 11 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/CommandThatFailedDuringRebase.php b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/CommandThatFailedDuringRebase.php index 96b6a433b11..4f6fd46d615 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/CommandThatFailedDuringRebase.php +++ b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/CommandThatFailedDuringRebase.php @@ -15,6 +15,18 @@ namespace Neos\ContentRepository\Core\Feature\WorkspaceRebase; use Neos\ContentRepository\Core\Feature\Common\RebasableToOtherWorkspaceInterface; +use Neos\ContentRepository\Core\Feature\NodeCreation\Command\CreateNodeAggregateWithNodeAndSerializedProperties; +use Neos\ContentRepository\Core\Feature\NodeDisabling\Command\DisableNodeAggregate; +use Neos\ContentRepository\Core\Feature\NodeDisabling\Command\EnableNodeAggregate; +use Neos\ContentRepository\Core\Feature\NodeModification\Command\SetSerializedNodeProperties; +use Neos\ContentRepository\Core\Feature\NodeMove\Command\MoveNodeAggregate; +use Neos\ContentRepository\Core\Feature\NodeReferencing\Command\SetSerializedNodeReferences; +use Neos\ContentRepository\Core\Feature\NodeRemoval\Command\RemoveNodeAggregate; +use Neos\ContentRepository\Core\Feature\NodeTypeChange\Command\ChangeNodeAggregateType; +use Neos\ContentRepository\Core\Feature\NodeVariation\Command\CreateNodeVariant; +use Neos\ContentRepository\Core\Feature\SubtreeTagging\Command\TagSubtree; +use Neos\ContentRepository\Core\Feature\SubtreeTagging\Command\UntagSubtree; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\EventStore\Model\Event\SequenceNumber; /** @@ -23,17 +35,44 @@ final readonly class CommandThatFailedDuringRebase { /** - * @param RebasableToOtherWorkspaceInterface $command the command that failed - * @param \Throwable $exception how the command failed - * @param SequenceNumber $sequenceNumber the event store sequence number of the event containing the command to be rebased + * @internal */ public function __construct( - public RebasableToOtherWorkspaceInterface $command, - public \Throwable $exception, + private RebasableToOtherWorkspaceInterface $command, + private \Throwable $exception, private SequenceNumber $sequenceNumber, ) { } + /** + * The node aggregate id of the failed command + */ + public function getAffectedNodeAggregateId(): ?NodeAggregateId + { + return match ($this->command::class) { + MoveNodeAggregate::class, + SetSerializedNodeProperties::class, + CreateNodeAggregateWithNodeAndSerializedProperties::class, + TagSubtree::class, + DisableNodeAggregate::class, + UntagSubtree::class, + EnableNodeAggregate::class, + RemoveNodeAggregate::class, + ChangeNodeAggregateType::class, + CreateNodeVariant::class => $this->command->nodeAggregateId, + SetSerializedNodeReferences::class => $this->command->sourceNodeAggregateId, + default => null + }; + } + + /** + * How the command failed that was attempted to be rebased + */ + public function getException(): \Throwable + { + return $this->exception; + } + /** * The event store sequence number of the event containing the command to be rebased * @@ -43,4 +82,14 @@ public function getSequenceNumber(): SequenceNumber { return $this->sequenceNumber; } + + /** + * The command that failed + * + * @internal exposed for testing and experimental use cases + */ + public function getCommand(): RebasableToOtherWorkspaceInterface + { + return $this->command; + } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/Exception/WorkspaceRebaseFailed.php b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/Exception/WorkspaceRebaseFailed.php index b1bae671156..1ac611e412e 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/Exception/WorkspaceRebaseFailed.php +++ b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/Exception/WorkspaceRebaseFailed.php @@ -36,7 +36,7 @@ public static function duringRebase(CommandsThatFailedDuringRebase $commandsThat $commandsThatFailedDuringRebase, sprintf('Rebase failed: %s', self::renderMessage($commandsThatFailedDuringRebase)), 1729974936, - $commandsThatFailedDuringRebase->first()?->exception + $commandsThatFailedDuringRebase->first()?->getException() ); } @@ -46,7 +46,7 @@ public static function duringPublish(CommandsThatFailedDuringRebase $commandsTha $commandsThatFailedDuringRebase, sprintf('Publication failed: %s', self::renderMessage($commandsThatFailedDuringRebase)), 1729974980, - $commandsThatFailedDuringRebase->first()?->exception + $commandsThatFailedDuringRebase->first()?->getException() ); } @@ -56,13 +56,13 @@ public static function duringDiscard(CommandsThatFailedDuringRebase $commandsTha $commandsThatFailedDuringRebase, sprintf('Discard failed: %s', self::renderMessage($commandsThatFailedDuringRebase)), 1729974982, - $commandsThatFailedDuringRebase->first()?->exception + $commandsThatFailedDuringRebase->first()?->getException() ); } private static function renderMessage(CommandsThatFailedDuringRebase $commandsThatFailedDuringRebase): string { $firstFailure = $commandsThatFailedDuringRebase->first(); - return sprintf('"%s" and %d further failures', $firstFailure?->exception->getMessage(), count($commandsThatFailedDuringRebase) - 1); + return sprintf('"%s" and %d further failures', $firstFailure?->getException()->getMessage(), count($commandsThatFailedDuringRebase) - 1); } } diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php index d9d7f66def5..ccd1e73fd42 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php @@ -176,8 +176,8 @@ public function theLastCommandShouldHaveThrownTheWorkspaceRebaseFailedWith(Table foreach ($exception->commandsThatFailedDuringRebase as $commandsThatFailed) { $actualComparableHash[] = [ 'SequenceNumber' => (string)$commandsThatFailed->getSequenceNumber()->value, - 'Command' => (new \ReflectionClass($commandsThatFailed->command))->getShortName(), - 'Exception' => (new \ReflectionClass($commandsThatFailed->exception))->getShortName(), + 'Command' => (new \ReflectionClass($commandsThatFailed->getCommand()))->getShortName(), + 'Exception' => (new \ReflectionClass($commandsThatFailed->getException()))->getShortName(), ]; } From a3aa2266bb5d77fa55b64e6a76ca09aeb18c6e14 Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Sat, 9 Nov 2024 12:15:50 +0100 Subject: [PATCH 105/214] Move command hooks invokation to ContentRepository --- .../Classes/CommandHandler/CommandBus.php | 19 ++----------------- .../Classes/CommandHandler/CommandHooks.php | 10 +++++++++- .../Classes/ContentRepository.php | 5 ++++- .../CommandHooksFactoryDependencies.php | 4 ++++ .../Factory/ContentRepositoryFactory.php | 15 +++++++-------- 5 files changed, 26 insertions(+), 27 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandBus.php b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandBus.php index 262a3acee72..92673a00c82 100644 --- a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandBus.php +++ b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandBus.php @@ -23,7 +23,6 @@ public function __construct( // todo pass $commandHandlingDependencies in each command handler instead of into the commandBus private CommandHandlingDependencies $commandHandlingDependencies, - private CommandHooks $commandHooks, CommandHandlerInterface ...$handlers ) { $this->handlers = $handlers; @@ -36,13 +35,9 @@ public function handle(CommandInterface $command): EventsToPublish|\Generator { // multiple handlers must not handle the same command foreach ($this->handlers as $handler) { - if (!$handler->canHandle($command)) { - continue; + if ($handler->canHandle($command)) { + return $handler->handle($command, $this->commandHandlingDependencies); } - foreach ($this->commandHooks as $commandHook) { - $command = $commandHook->onBeforeHandle($command); - } - return $handler->handle($command, $this->commandHandlingDependencies); } throw new \RuntimeException(sprintf('No handler found for Command "%s"', get_debug_type($command)), 1649582778); } @@ -51,18 +46,8 @@ public function withAdditionalHandlers(CommandHandlerInterface ...$handlers): se { return new self( $this->commandHandlingDependencies, - $this->commandHooks, ...$this->handlers, ...$handlers, ); } - - public function withCommandHooks(CommandHooks $commandHooks): self - { - return new self( - $this->commandHandlingDependencies, - $commandHooks, - ...$this->handlers, - ); - } } diff --git a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHooks.php b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHooks.php index c926f6b915f..fec4fc25750 100644 --- a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHooks.php +++ b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHooks.php @@ -10,7 +10,7 @@ * @implements \IteratorAggregate * @internal */ -final readonly class CommandHooks implements \IteratorAggregate, \Countable +final readonly class CommandHooks implements CommandHookInterface, \IteratorAggregate, \Countable { /** * @var array @@ -45,4 +45,12 @@ public function count(): int { return count($this->commandHooks); } + + public function onBeforeHandle(CommandInterface $command): CommandInterface + { + foreach ($this->commandHooks as $commandHook) { + $command = $commandHook->onBeforeHandle($command); + } + return $command; + } } diff --git a/Neos.ContentRepository.Core/Classes/ContentRepository.php b/Neos.ContentRepository.Core/Classes/ContentRepository.php index f474dda8191..b0ba0a26436 100644 --- a/Neos.ContentRepository.Core/Classes/ContentRepository.php +++ b/Neos.ContentRepository.Core/Classes/ContentRepository.php @@ -15,6 +15,7 @@ namespace Neos\ContentRepository\Core; use Neos\ContentRepository\Core\CommandHandler\CommandBus; +use Neos\ContentRepository\Core\CommandHandler\CommandHooks; use Neos\ContentRepository\Core\CommandHandler\CommandInterface; use Neos\ContentRepository\Core\Dimension\ContentDimensionSourceInterface; use Neos\ContentRepository\Core\DimensionSpace\InterDimensionalVariationGraph; @@ -83,7 +84,8 @@ public function __construct( private readonly ContentDimensionSourceInterface $contentDimensionSource, private readonly UserIdProviderInterface $userIdProvider, private readonly ClockInterface $clock, - private readonly ContentGraphReadModelInterface $contentGraphReadModel + private readonly ContentGraphReadModelInterface $contentGraphReadModel, + private readonly CommandHooks $commandHooks, ) { } @@ -94,6 +96,7 @@ public function __construct( */ public function handle(CommandInterface $command): void { + $command = $this->commandHooks->onBeforeHandle($command); // the commands only calculate which events they want to have published, but do not do the // publishing themselves $eventsToPublishOrGenerator = $this->commandBus->handle($command); diff --git a/Neos.ContentRepository.Core/Classes/Factory/CommandHooksFactoryDependencies.php b/Neos.ContentRepository.Core/Classes/Factory/CommandHooksFactoryDependencies.php index 42ef4256022..083fa9a10f6 100644 --- a/Neos.ContentRepository.Core/Classes/Factory/CommandHooksFactoryDependencies.php +++ b/Neos.ContentRepository.Core/Classes/Factory/CommandHooksFactoryDependencies.php @@ -14,6 +14,7 @@ namespace Neos\ContentRepository\Core\Factory; +use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphReadModelInterface; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; /** @@ -23,6 +24,7 @@ { private function __construct( public ContentRepositoryId $contentRepositoryId, + public ContentGraphReadModelInterface $contentGraphReadModel, ) { } @@ -31,9 +33,11 @@ private function __construct( */ public static function create( ContentRepositoryId $contentRepositoryId, + ContentGraphReadModelInterface $contentGraphReadModel, ): self { return new self( $contentRepositoryId, + $contentGraphReadModel, ); } } diff --git a/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php b/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php index 858deefcc10..2469a37f30d 100644 --- a/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php +++ b/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php @@ -56,7 +56,7 @@ public function __construct( ProjectionsAndCatchUpHooksFactory $projectionsAndCatchUpHooksFactory, private readonly UserIdProviderInterface $userIdProvider, private readonly ClockInterface $clock, - private readonly CommandHooksFactory|null $commandHooksFactory, + private readonly CommandHooksFactory $commandHooksFactory, ) { $contentDimensionZookeeper = new ContentDimensionZookeeper($contentDimensionSource); $interDimensionalVariationGraph = new InterDimensionalVariationGraph( @@ -105,7 +105,6 @@ public function getOrBuild(): ContentRepository // we dont need full recursion in rebase - e.g apply workspace commands - and thus we can use this set for simulation $commandBusForRebaseableCommands = new CommandBus( $commandHandlingDependencies, - CommandHooks::none(), new NodeAggregateCommandHandler( $this->projectionFactoryDependencies->nodeTypeManager, $this->projectionFactoryDependencies->contentDimensionZookeeper, @@ -136,11 +135,10 @@ public function getOrBuild(): ContentRepository $this->projectionFactoryDependencies->eventNormalizer, ) ); - if ($this->commandHooksFactory !== null) { - $commandHooks = $this->commandHooksFactory->build(CommandHooksFactoryDependencies::create($this->contentRepositoryId)); - $publicCommandBus = $publicCommandBus->withCommandHooks($commandHooks); - } - + $commandHooks = $this->commandHooksFactory->build(CommandHooksFactoryDependencies::create( + $this->contentRepositoryId, + $this->projectionsAndCatchUpHooks->contentGraphProjection->getState(), + )); $this->contentRepository = new ContentRepository( $this->contentRepositoryId, $publicCommandBus, @@ -153,7 +151,8 @@ public function getOrBuild(): ContentRepository $this->projectionFactoryDependencies->contentDimensionSource, $this->userIdProvider, $this->clock, - $contentGraphReadModel + $contentGraphReadModel, + $commandHooks, ); $this->isBuilding = false; return $this->contentRepository; From ccbe354276ed941ccb70882b3d658bd585b47941 Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Sat, 9 Nov 2024 12:18:57 +0100 Subject: [PATCH 106/214] Relax `ContentRepository::commandHook` type --- Neos.ContentRepository.Core/Classes/ContentRepository.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/ContentRepository.php b/Neos.ContentRepository.Core/Classes/ContentRepository.php index b0ba0a26436..b2ef11475b5 100644 --- a/Neos.ContentRepository.Core/Classes/ContentRepository.php +++ b/Neos.ContentRepository.Core/Classes/ContentRepository.php @@ -15,7 +15,7 @@ namespace Neos\ContentRepository\Core; use Neos\ContentRepository\Core\CommandHandler\CommandBus; -use Neos\ContentRepository\Core\CommandHandler\CommandHooks; +use Neos\ContentRepository\Core\CommandHandler\CommandHookInterface; use Neos\ContentRepository\Core\CommandHandler\CommandInterface; use Neos\ContentRepository\Core\Dimension\ContentDimensionSourceInterface; use Neos\ContentRepository\Core\DimensionSpace\InterDimensionalVariationGraph; @@ -85,7 +85,7 @@ public function __construct( private readonly UserIdProviderInterface $userIdProvider, private readonly ClockInterface $clock, private readonly ContentGraphReadModelInterface $contentGraphReadModel, - private readonly CommandHooks $commandHooks, + private readonly CommandHookInterface $commandHook, ) { } @@ -96,7 +96,7 @@ public function __construct( */ public function handle(CommandInterface $command): void { - $command = $this->commandHooks->onBeforeHandle($command); + $command = $this->commandHook->onBeforeHandle($command); // the commands only calculate which events they want to have published, but do not do the // publishing themselves $eventsToPublishOrGenerator = $this->commandBus->handle($command); From c5274cd5c1ea4a7d04433c248841c041d6bb990f Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Sat, 9 Nov 2024 17:09:00 +0100 Subject: [PATCH 107/214] BUGFIX: Re-introduce stricter checks for subtree en/disabling and tagging Previously, when trying to disable an already disabled node, we just ignored that fact and did not produce any events. That leads to a potentially opaque behavior: ``` User A: log into backend User B: log into backend User A: Hide Node X User B: Hide Node X User B: Publish User A: Rebase User B: Unhide Node X User B: Publish User A: Rebase <--- now Node X is re-enabled even though User A had explicitly disabled it ``` With this change, the sequence is: ``` User A: log into backend User B: log into backend User A: Hide Node X User B: Hide Node X User B: Publish User A: Rebase <--- this leads to a conflict because both users disabled the same node. And User A can now explicitly decide to ignore that fact ``` Note: This mostly reverts #4284 that was introduced because we did not have a conflict resolution in place Related: #4284 --- ...ableNodeAggregate_ConstraintChecks.feature | 11 ++-------- ...ableNodeAggregate_ConstraintChecks.feature | 5 ++--- .../TagSubtree_WithoutDimensions.feature | 18 +++++++-------- .../03-MoreBasicFeatures.feature | 14 +++--------- .../CommandHandler/CommandSimulator.php | 4 ---- .../Classes/EventStore/EventPersister.php | 3 --- .../Classes/EventStore/Events.php | 5 ----- .../Classes/EventStore/EventsToPublish.php | 9 -------- .../NodeAggregateIsAlreadyDisabled.php | 22 +++++++++++++++++++ .../NodeAggregateIsAlreadyEnabled.php | 22 +++++++++++++++++++ .../Feature/NodeDisabling/NodeDisabling.php | 8 +++---- .../SubtreeTagging/Command/TagSubtree.php | 1 - .../SubtreeTagging/Command/UntagSubtree.php | 1 - .../Exception/SubtreeIsAlreadyTagged.php | 22 +++++++++++++++++++ .../Exception/SubtreeIsNotTagged.php | 22 +++++++++++++++++++ .../Feature/SubtreeTagging/SubtreeTagging.php | 8 +++---- 16 files changed, 112 insertions(+), 63 deletions(-) create mode 100644 Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/Exception/NodeAggregateIsAlreadyDisabled.php create mode 100644 Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/Exception/NodeAggregateIsAlreadyEnabled.php create mode 100644 Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Exception/SubtreeIsAlreadyTagged.php create mode 100644 Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Exception/SubtreeIsNotTagged.php diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/01-DisableNodeAggregate_ConstraintChecks.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/01-DisableNodeAggregate_ConstraintChecks.feature index 91c845e2b27..5a69b19b85a 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/01-DisableNodeAggregate_ConstraintChecks.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/01-DisableNodeAggregate_ConstraintChecks.feature @@ -61,19 +61,12 @@ Feature: Constraint checks on node aggregate disabling | coveredDimensionSpacePoint | {"language": "de"} | | nodeVariantSelectionStrategy | "allVariants" | - # Note: The behavior has been changed with https://github.com/neos/neos-development-collection/pull/4284 and the test was adjusted accordingly - When the command DisableNodeAggregate is executed with payload: + When the command DisableNodeAggregate is executed with payload and exceptions are caught: | Key | Value | | nodeAggregateId | "sir-david-nodenborough" | | coveredDimensionSpacePoint | {"language": "de"} | | nodeVariantSelectionStrategy | "allVariants" | - Then I expect exactly 4 events to be published on stream with prefix "ContentStream:cs-identifier" - And event at index 3 is of type "SubtreeWasTagged" with payload: - | Key | Expected | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "sir-david-nodenborough" | - | affectedDimensionSpacePoints | [{"language":"de"},{"language":"gsw"}] | - | tag | "disabled" | + Then the last command should have thrown an exception of type "NodeAggregateIsAlreadyDisabled" Scenario: Try to disable a node aggregate in a non-existing dimension space point diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/04-EnableNodeAggregate_ConstraintChecks.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/04-EnableNodeAggregate_ConstraintChecks.feature index d50f8cea106..0c4e9a2ba1f 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/04-EnableNodeAggregate_ConstraintChecks.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/04-EnableNodeAggregate_ConstraintChecks.feature @@ -44,13 +44,12 @@ Feature: Enable a node aggregate | nodeVariantSelectionStrategy | "allVariants" | Then the last command should have thrown an exception of type "NodeAggregateCurrentlyDoesNotExist" - # Note: The behavior has been changed with https://github.com/neos/neos-development-collection/pull/4284 and the test was adjusted accordingly Scenario: Try to enable an already enabled node aggregate - When the command EnableNodeAggregate is executed with payload: + When the command EnableNodeAggregate is executed with payload and exceptions are caught: | Key | Value | | nodeAggregateId | "sir-david-nodenborough" | | nodeVariantSelectionStrategy | "allVariants" | - Then I expect exactly 3 events to be published on stream with prefix "ContentStream:cs-identifier" + Then the last command should have thrown an exception of type "NodeAggregateIsAlreadyEnabled" Scenario: Try to enable a node aggregate in a non-existing dimension space point When the command EnableNodeAggregate is executed with payload and exceptions are caught: diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/SubtreeTagging/TagSubtree_WithoutDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/SubtreeTagging/TagSubtree_WithoutDimensions.feature index 82b5aa3dc73..6a75a86fe61 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/SubtreeTagging/TagSubtree_WithoutDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/SubtreeTagging/TagSubtree_WithoutDimensions.feature @@ -37,7 +37,7 @@ Feature: Tag subtree without dimensions | b | Neos.ContentRepository.Testing:Document | root | b | | b1 | Neos.ContentRepository.Testing:Document | b | b1 | - Scenario: Tagging the same node twice with the same subtree tag is ignored + Scenario: Tagging the same node twice with the same subtree tag When the command TagSubtree is executed with payload: | Key | Value | | nodeAggregateId | "a1" | @@ -50,23 +50,23 @@ Feature: Tag subtree without dimensions | nodeAggregateId | "a1" | | affectedDimensionSpacePoints | [[]] | | tag | "tag1" | - When the command TagSubtree is executed with payload: + When the command TagSubtree is executed with payload and exceptions are caught: | Key | Value | | nodeAggregateId | "a1" | | nodeVariantSelectionStrategy | "allVariants" | | tag | "tag1" | - Then I expect exactly 14 events to be published on stream with prefix "ContentStream:cs-identifier" + Then the last command should have thrown an exception of type "SubtreeIsAlreadyTagged" - Scenario: Untagging a node without tags is ignored + Scenario: Untagging a node without tags Then I expect exactly 13 events to be published on stream with prefix "ContentStream:cs-identifier" - When the command UntagSubtree is executed with payload: + When the command UntagSubtree is executed with payload and exceptions are caught: | Key | Value | | nodeAggregateId | "a1" | | nodeVariantSelectionStrategy | "allVariants" | | tag | "tag1" | - Then I expect exactly 13 events to be published on stream with prefix "ContentStream:cs-identifier" + Then the last command should have thrown an exception of type "SubtreeIsNotTagged" - Scenario: Untagging a node that is only implicitly tagged (inherited) is ignored + Scenario: Untagging a node that is only implicitly tagged (inherited) When the command TagSubtree is executed with payload: | Key | Value | | nodeAggregateId | "a1" | @@ -79,12 +79,12 @@ Feature: Tag subtree without dimensions | nodeAggregateId | "a1" | | affectedDimensionSpacePoints | [[]] | | tag | "tag1" | - When the command UntagSubtree is executed with payload: + When the command UntagSubtree is executed with payload and exceptions are caught: | Key | Value | | nodeAggregateId | "a1a" | | nodeVariantSelectionStrategy | "allVariants" | | tag | "tag1" | - Then I expect exactly 14 events to be published on stream with prefix "ContentStream:cs-identifier" + Then the last command should have thrown an exception of type "SubtreeIsNotTagged" Scenario: Tagging subtree with arbitrary strategy since dimensions are not involved When the command TagSubtree is executed with payload: diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/03-MoreBasicFeatures.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/03-MoreBasicFeatures.feature index 398d368f0a5..545b56cfea8 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/03-MoreBasicFeatures.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/03-MoreBasicFeatures.feature @@ -182,7 +182,7 @@ Feature: Publishing individual nodes (basics) | originDimensionSpacePoint | {} | | propertyValues | {"image": "Bla bli blub"} | - Scenario: Tag the same node in live and in the user workspace so that a rebase will omit the user change + Scenario: Tag the same node in live and in the user workspace so that a rebase will lead to a conflict When the command TagSubtree is executed with payload: | Key | Value | | workspaceName | "live" | @@ -195,20 +195,12 @@ Feature: Publishing individual nodes (basics) | nodeAggregateId | "sir-unchanged" | | nodeVariantSelectionStrategy | "allVariants" | | tag | "tag1" | - When the command PublishIndividualNodesFromWorkspace is executed with payload: + When the command PublishIndividualNodesFromWorkspace is executed with payload and exceptions are caught: | Key | Value | | workspaceName | "user-test" | | nodesToPublish | [{"dimensionSpacePoint": {}, "nodeAggregateId": "sir-unchanged"}] | | contentStreamIdForRemainingPart | "user-cs-identifier-remaining" | - - When I am in workspace "live" and dimension space point {} - Then I expect node aggregate identifier "sir-unchanged" to lead to node cs-identifier;sir-unchanged;{} - And I expect this node to be exactly explicitly tagged "tag1" - - When I am in workspace "user-test" and dimension space point {} - Then I expect node aggregate identifier "sir-unchanged" to lead to node user-cs-identifier-remaining;sir-unchanged;{} - And I expect this node to be exactly explicitly tagged "tag1" - Then workspace user-test has status UP_TO_DATE + Then the last command should have thrown an exception of type "WorkspaceRebaseFailed" Scenario: It is possible to publish all nodes When the command PublishIndividualNodesFromWorkspace is executed with payload: diff --git a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandSimulator.php b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandSimulator.php index ee3ee2de52c..10add5ff78d 100644 --- a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandSimulator.php +++ b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandSimulator.php @@ -101,10 +101,6 @@ private function handle(RebaseableCommand $rebaseableCommand): void throw new \RuntimeException(sprintf('%s expects an instance of %s to be returned. Got %s when handling %s', self::class, EventsToPublish::class, get_debug_type($eventsToPublish), $rebaseableCommand->originalCommand::class)); } - if ($eventsToPublish->events->isEmpty()) { - return; - } - $normalizedEvents = Events::fromArray( $eventsToPublish->events->map(function (EventInterface|DecoratedEvent $event) use ( $rebaseableCommand diff --git a/Neos.ContentRepository.Core/Classes/EventStore/EventPersister.php b/Neos.ContentRepository.Core/Classes/EventStore/EventPersister.php index 4909d50e661..f6c38f803d1 100644 --- a/Neos.ContentRepository.Core/Classes/EventStore/EventPersister.php +++ b/Neos.ContentRepository.Core/Classes/EventStore/EventPersister.php @@ -28,9 +28,6 @@ public function __construct( */ public function publishEvents(ContentRepository $contentRepository, EventsToPublish $eventsToPublish): void { - if ($eventsToPublish->events->isEmpty()) { - return; - } $normalizedEvents = Events::fromArray( $eventsToPublish->events->map($this->eventNormalizer->normalize(...)) ); diff --git a/Neos.ContentRepository.Core/Classes/EventStore/Events.php b/Neos.ContentRepository.Core/Classes/EventStore/Events.php index 872aab9a56d..f9d9abc8191 100644 --- a/Neos.ContentRepository.Core/Classes/EventStore/Events.php +++ b/Neos.ContentRepository.Core/Classes/EventStore/Events.php @@ -56,11 +56,6 @@ public function map(\Closure $callback): array return array_map($callback, $this->events); } - public function isEmpty(): bool - { - return empty($this->events); - } - public function count(): int { return count($this->events); diff --git a/Neos.ContentRepository.Core/Classes/EventStore/EventsToPublish.php b/Neos.ContentRepository.Core/Classes/EventStore/EventsToPublish.php index d7b4ab5bcfe..ee9e22ef103 100644 --- a/Neos.ContentRepository.Core/Classes/EventStore/EventsToPublish.php +++ b/Neos.ContentRepository.Core/Classes/EventStore/EventsToPublish.php @@ -25,15 +25,6 @@ public function __construct( ) { } - public static function empty(): self - { - return new EventsToPublish( - StreamName::fromString("empty"), - Events::fromArray([]), - ExpectedVersion::ANY() - ); - } - public function withAppendedEvents(Events $events): self { return new self( diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/Exception/NodeAggregateIsAlreadyDisabled.php b/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/Exception/NodeAggregateIsAlreadyDisabled.php new file mode 100644 index 00000000000..c659a1532c5 --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/Exception/NodeAggregateIsAlreadyDisabled.php @@ -0,0 +1,22 @@ +coveredDimensionSpacePoint ); if ($nodeAggregate->getDimensionSpacePointsTaggedWith(SubtreeTag::disabled())->contains($command->coveredDimensionSpacePoint)) { - // already disabled, so we can return a no-operation. - return EventsToPublish::empty(); + throw new NodeAggregateIsAlreadyDisabled(sprintf('Node aggregate "%s" cannot be disabled because it is already explicitly disabled for dimension space point %s', $nodeAggregate->nodeAggregateId->value, $command->coveredDimensionSpacePoint->toJson()), 1731166196); } $affectedDimensionSpacePoints = $command->nodeVariantSelectionStrategy @@ -114,8 +115,7 @@ public function handleEnableNodeAggregate( $command->coveredDimensionSpacePoint ); if (!$nodeAggregate->getDimensionSpacePointsTaggedWith(SubtreeTag::disabled())->contains($command->coveredDimensionSpacePoint)) { - // already enabled, so we can return a no-operation. - return EventsToPublish::empty(); + throw new NodeAggregateIsAlreadyEnabled(sprintf('Node aggregate "%s" cannot be enabled because is not explicitly disabled for dimension space point %s', $nodeAggregate->nodeAggregateId->value, $command->coveredDimensionSpacePoint->toJson()), 1731166142); } $affectedDimensionSpacePoints = $command->nodeVariantSelectionStrategy diff --git a/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Command/TagSubtree.php b/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Command/TagSubtree.php index d4d37c8b8cb..a0e362cc3cd 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Command/TagSubtree.php +++ b/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Command/TagSubtree.php @@ -22,7 +22,6 @@ use Neos\ContentRepository\Core\Feature\WorkspacePublication\Dto\NodeIdToPublishOrDiscard; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Node\NodeVariantSelectionStrategy; -use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; /** diff --git a/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Command/UntagSubtree.php b/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Command/UntagSubtree.php index 1ae9b4624a2..525d60aad2b 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Command/UntagSubtree.php +++ b/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Command/UntagSubtree.php @@ -22,7 +22,6 @@ use Neos\ContentRepository\Core\Feature\WorkspacePublication\Dto\NodeIdToPublishOrDiscard; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Node\NodeVariantSelectionStrategy; -use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; /** diff --git a/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Exception/SubtreeIsAlreadyTagged.php b/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Exception/SubtreeIsAlreadyTagged.php new file mode 100644 index 00000000000..8a1388e7d9c --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Exception/SubtreeIsAlreadyTagged.php @@ -0,0 +1,22 @@ +getDimensionSpacePointsTaggedWith($command->tag)->contains($command->coveredDimensionSpacePoint)) { - // already explicitly tagged with the same Subtree Tag, so we can return a no-operation. - return EventsToPublish::empty(); + throw new SubtreeIsAlreadyTagged(sprintf('Cannot add subtree tag "%s" because node aggregate "%s" is already explicitly tagged with that tag in dimension space point %s', $command->tag->value, $nodeAggregate->nodeAggregateId->value, $command->coveredDimensionSpacePoint->toJson()), 1731167142); } $affectedDimensionSpacePoints = $command->nodeVariantSelectionStrategy @@ -93,8 +94,7 @@ public function handleUntagSubtree(UntagSubtree $command, CommandHandlingDepende ); if (!$nodeAggregate->getDimensionSpacePointsTaggedWith($command->tag)->contains($command->coveredDimensionSpacePoint)) { - // not explicitly tagged with the given Subtree Tag, so we can return a no-operation. - return EventsToPublish::empty(); + throw new SubtreeIsNotTagged(sprintf('Cannot remove subtree tag "%s" because node aggregate "%s" is not explicitly tagged with that tag in dimension space point %s', $command->tag->value, $nodeAggregate->nodeAggregateId->value, $command->coveredDimensionSpacePoint->toJson()), 1731167464); } $affectedDimensionSpacePoints = $command->nodeVariantSelectionStrategy From d6ff6bb204adb877b31a5ea7fc8b87260ad9c4e1 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 9 Nov 2024 17:44:54 +0100 Subject: [PATCH 108/214] TASK: Do not expose that `Command`'s failed during rebase This is an implementation detail, and we might change this concept and rather do constraint checks for events on the read model and then publish those. --- .../01-ConstraintChecks.feature | 4 +- .../03-RebasingWithConflictingChanges.feature | 10 +- .../01-ConstraintChecks.feature | 10 +- .../02-DiscardWorkspace.feature | 4 +- .../CommandHandler/CommandSimulator.php | 24 ++--- .../DimensionSpace/DimensionSpacePointSet.php | 8 ++ .../Classes/Feature/RebaseableCommand.php | 16 ++-- .../Classes/Feature/RebaseableCommands.php | 2 +- .../Feature/WorkspaceCommandHandler.php | 16 ++-- .../CommandThatFailedDuringRebase.php | 95 ------------------- .../EventThatFailedDuringRebase.php | 74 +++++++++++++++ ...e.php => EventsThatFailedDuringRebase.php} | 12 +-- .../Exception/WorkspaceRebaseFailed.php | 34 +++---- ...ricCommandExecutionAndEventPublication.php | 4 +- 14 files changed, 153 insertions(+), 160 deletions(-) delete mode 100644 Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/CommandThatFailedDuringRebase.php create mode 100644 Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/EventThatFailedDuringRebase.php rename Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/{CommandsThatFailedDuringRebase.php => EventsThatFailedDuringRebase.php} (69%) diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W10-IndividualNodeDiscarding/01-ConstraintChecks.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W10-IndividualNodeDiscarding/01-ConstraintChecks.feature index 7c5c5a7c002..bb15c251e28 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W10-IndividualNodeDiscarding/01-ConstraintChecks.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W10-IndividualNodeDiscarding/01-ConstraintChecks.feature @@ -73,8 +73,8 @@ Feature: Workspace discarding - complex chained functionality | nodesToDiscard | [{"workspaceName": "user-ws", "dimensionSpacePoint": {"language": "en"}, "nodeAggregateId": "sir-david-nodenborough"}, {"workspaceName": "user-ws", "dimensionSpacePoint": {"language": "en"}, "nodeAggregateId": "sir-david-nodenborough"}] | | newContentStreamId | "user-cs-id-rebased" | Then the last command should have thrown the WorkspaceRebaseFailed exception with: - | SequenceNumber | Command | Exception | - | 11 | CreateNodeVariant | NodeAggregateDoesCurrentlyNotCoverDimensionSpacePoint | + | SequenceNumber | Event | Exception | + | 11 | NodeGeneralizationVariantWasCreated | NodeAggregateDoesCurrentlyNotCoverDimensionSpacePoint | When the command DiscardWorkspace is executed with payload: | Key | Value | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W6-WorkspaceRebasing/03-RebasingWithConflictingChanges.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W6-WorkspaceRebasing/03-RebasingWithConflictingChanges.feature index bbd1de5307c..7f30b4c68af 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W6-WorkspaceRebasing/03-RebasingWithConflictingChanges.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W6-WorkspaceRebasing/03-RebasingWithConflictingChanges.feature @@ -101,8 +101,8 @@ Feature: Workspace rebasing - conflicting changes Then I expect the content stream "user-cs-two" to exist Then I expect the content stream "user-cs-two-rebased" to not exist Then the last command should have thrown the WorkspaceRebaseFailed exception with: - | SequenceNumber | Command | Exception | - | 13 | SetSerializedNodeProperties | NodeAggregateCurrentlyDoesNotExist | + | SequenceNumber | Event | Exception | + | 13 | NodePropertiesWereSet | NodeAggregateCurrentlyDoesNotExist | When the command RebaseWorkspace is executed with payload: | Key | Value | @@ -169,9 +169,9 @@ Feature: Workspace rebasing - conflicting changes Then I expect the content stream "user-cs-identifier" to exist Then I expect the content stream "user-cs-identifier-rebased" to not exist Then the last command should have thrown the WorkspaceRebaseFailed exception with: - | SequenceNumber | Command | Exception | - | 12 | SetSerializedNodeProperties | NodeAggregateCurrentlyDoesNotExist | - | 14 | SetSerializedNodeProperties | NodeAggregateCurrentlyDoesNotExist | + | SequenceNumber | Event | Exception | + | 12 | NodePropertiesWereSet | NodeAggregateCurrentlyDoesNotExist | + | 14 | NodePropertiesWereSet | NodeAggregateCurrentlyDoesNotExist | When the command RebaseWorkspace is executed with payload: | Key | Value | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/01-ConstraintChecks.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/01-ConstraintChecks.feature index ac52e28289b..60bf7aadc2b 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/01-ConstraintChecks.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/01-ConstraintChecks.feature @@ -83,9 +83,9 @@ Feature: Workspace publication - complex chained functionality | nodesToPublish | [{"dimensionSpacePoint": {"language": "de"}, "nodeAggregateId": "sir-nodebelig"}] | | newContentStreamId | "user-cs-id-rebased" | Then the last command should have thrown the WorkspaceRebaseFailed exception with: - | SequenceNumber | Command | Exception | - | 13 | SetSerializedNodeProperties | NodeAggregateCurrentlyDoesNotExist | - | 14 | SetSerializedNodeProperties | NodeAggregateCurrentlyDoesNotExist | + | SequenceNumber | Event | Exception | + | 13 | NodePropertiesWereSet | NodeAggregateCurrentlyDoesNotExist | + | 14 | NodePropertiesWereSet | NodeAggregateCurrentlyDoesNotExist | Scenario: Vary to generalization, then publish only the child node so that an exception is thrown. Ensure that the workspace recovers from this When the command CreateNodeVariant is executed with payload: @@ -107,8 +107,8 @@ Feature: Workspace publication - complex chained functionality | nodesToPublish | [{"workspaceName": "user-ws", "dimensionSpacePoint": {"language": "en"}, "nodeAggregateId": "nody-mc-nodeface"}] | | newContentStreamId | "user-cs-id-rebased" | Then the last command should have thrown the WorkspaceRebaseFailed exception with: - | SequenceNumber | Command | Exception | - | 13 | CreateNodeVariant | NodeAggregateDoesCurrentlyNotCoverDimensionSpacePoint | + | SequenceNumber | Event | Exception | + | 13 | NodeGeneralizationVariantWasCreated | NodeAggregateDoesCurrentlyNotCoverDimensionSpacePoint | When the command PublishWorkspace is executed with payload: | Key | Value | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W9-WorkspaceDiscarding/02-DiscardWorkspace.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W9-WorkspaceDiscarding/02-DiscardWorkspace.feature index 0e180f7d0dd..158cd4dabb5 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W9-WorkspaceDiscarding/02-DiscardWorkspace.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W9-WorkspaceDiscarding/02-DiscardWorkspace.feature @@ -139,8 +139,8 @@ Feature: Workspace discarding - basic functionality | workspaceName | "user-ws-two" | | rebasedContentStreamId | "user-cs-two-rebased" | Then the last command should have thrown the WorkspaceRebaseFailed exception with: - | SequenceNumber | Command | Exception | - | 13 | SetSerializedNodeProperties | NodeAggregateCurrentlyDoesNotExist | + | SequenceNumber | Event | Exception | + | 13 | NodePropertiesWereSet | NodeAggregateCurrentlyDoesNotExist | Then workspace user-ws-two has status OUTDATED diff --git a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandSimulator.php b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandSimulator.php index ee3ee2de52c..015fdd86947 100644 --- a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandSimulator.php +++ b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandSimulator.php @@ -9,8 +9,8 @@ use Neos\ContentRepository\Core\EventStore\EventNormalizer; use Neos\ContentRepository\Core\EventStore\EventsToPublish; use Neos\ContentRepository\Core\Feature\RebaseableCommand; -use Neos\ContentRepository\Core\Feature\WorkspaceRebase\CommandsThatFailedDuringRebase; -use Neos\ContentRepository\Core\Feature\WorkspaceRebase\CommandThatFailedDuringRebase; +use Neos\ContentRepository\Core\Feature\WorkspaceRebase\EventsThatFailedDuringRebase; +use Neos\ContentRepository\Core\Feature\WorkspaceRebase\EventThatFailedDuringRebase; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphProjectionInterface; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\EventStore\Helper\InMemoryEventStore; @@ -45,7 +45,7 @@ */ final class CommandSimulator { - private CommandsThatFailedDuringRebase $commandsThatFailedDuringRebase; + private EventsThatFailedDuringRebase $eventsThatFailedDuringRebase; private readonly InMemoryEventStore $inMemoryEventStore; @@ -56,7 +56,7 @@ public function __construct( private readonly WorkspaceName $workspaceNameToSimulateIn, ) { $this->inMemoryEventStore = new InMemoryEventStore(); - $this->commandsThatFailedDuringRebase = new CommandsThatFailedDuringRebase(); + $this->eventsThatFailedDuringRebase = new EventsThatFailedDuringRebase(); } /** @@ -86,9 +86,11 @@ private function handle(RebaseableCommand $rebaseableCommand): void try { $eventsToPublish = $this->commandBus->handle($commandInWorkspace); } catch (\Exception $exception) { - $this->commandsThatFailedDuringRebase = $this->commandsThatFailedDuringRebase->withAppended( - new CommandThatFailedDuringRebase( - $rebaseableCommand->originalCommand, + $originalEvent = $this->eventNormalizer->denormalize($rebaseableCommand->originalEvent); + + $this->eventsThatFailedDuringRebase = $this->eventsThatFailedDuringRebase->withAppended( + new EventThatFailedDuringRebase( + $originalEvent, $exception, $rebaseableCommand->originalSequenceNumber ) @@ -159,13 +161,13 @@ public function eventStream(): EventStreamInterface return $this->inMemoryEventStore->load(VirtualStreamName::all()); } - public function hasCommandsThatFailed(): bool + public function hasEventsThatFailed(): bool { - return !$this->commandsThatFailedDuringRebase->isEmpty(); + return !$this->eventsThatFailedDuringRebase->isEmpty(); } - public function getCommandsThatFailed(): CommandsThatFailedDuringRebase + public function getEventsThatFailed(): EventsThatFailedDuringRebase { - return $this->commandsThatFailedDuringRebase; + return $this->eventsThatFailedDuringRebase; } } diff --git a/Neos.ContentRepository.Core/Classes/DimensionSpace/DimensionSpacePointSet.php b/Neos.ContentRepository.Core/Classes/DimensionSpace/DimensionSpacePointSet.php index 191dcd2d7f6..65d19b1e04c 100644 --- a/Neos.ContentRepository.Core/Classes/DimensionSpace/DimensionSpacePointSet.php +++ b/Neos.ContentRepository.Core/Classes/DimensionSpace/DimensionSpacePointSet.php @@ -148,6 +148,14 @@ public function equals(DimensionSpacePointSet $other): bool return $thisPointHashes === $otherPointHashes; } + public function getFirst(): ?DimensionSpacePoint + { + foreach ($this->points as $point) { + return $point; + } + return null; + } + public function getIterator(): \Traversable { yield from $this->points; diff --git a/Neos.ContentRepository.Core/Classes/Feature/RebaseableCommand.php b/Neos.ContentRepository.Core/Classes/Feature/RebaseableCommand.php index 210f91b8b8a..b62d85eddd8 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/RebaseableCommand.php +++ b/Neos.ContentRepository.Core/Classes/Feature/RebaseableCommand.php @@ -9,9 +9,11 @@ use Neos\ContentRepository\Core\EventStore\InitiatingEventMetadata; use Neos\ContentRepository\Core\Feature\Common\PublishableToWorkspaceInterface; use Neos\ContentRepository\Core\Feature\Common\RebasableToOtherWorkspaceInterface; +use Neos\EventStore\Model\Event; use Neos\EventStore\Model\Event\EventId; use Neos\EventStore\Model\Event\EventMetadata; use Neos\EventStore\Model\Event\SequenceNumber; +use Neos\EventStore\Model\EventEnvelope; /** * @internal @@ -20,17 +22,18 @@ { public function __construct( public RebasableToOtherWorkspaceInterface $originalCommand, + public Event $originalEvent, public EventMetadata $initiatingMetaData, public SequenceNumber $originalSequenceNumber ) { } - public static function extractFromEventMetaData(EventMetadata $eventMetadata, SequenceNumber $sequenceNumber): self + public static function extractFromEventEnvelope(EventEnvelope $eventEnvelope): self { - $commandToRebaseClass = $eventMetadata->value['commandClass'] ?? null; - $commandToRebasePayload = $eventMetadata->value['commandPayload'] ?? null; + $commandToRebaseClass = $eventEnvelope->event->metadata?->value['commandClass'] ?? null; + $commandToRebasePayload = $eventEnvelope->event->metadata?->value['commandPayload'] ?? null; - if ($commandToRebaseClass === null || $commandToRebasePayload === null) { + if ($commandToRebaseClass === null || $commandToRebasePayload === null || $eventEnvelope->event->metadata === null) { throw new \RuntimeException('Command cannot be extracted from metadata, missing commandClass or commandPayload.', 1729847804); } @@ -46,8 +49,9 @@ public static function extractFromEventMetaData(EventMetadata $eventMetadata, Se $commandInstance = $commandToRebaseClass::fromArray($commandToRebasePayload); return new self( $commandInstance, - InitiatingEventMetadata::extractInitiatingMetadata($eventMetadata), - $sequenceNumber + $eventEnvelope->event, + InitiatingEventMetadata::extractInitiatingMetadata($eventEnvelope->event->metadata), + $eventEnvelope->sequenceNumber ); } diff --git a/Neos.ContentRepository.Core/Classes/Feature/RebaseableCommands.php b/Neos.ContentRepository.Core/Classes/Feature/RebaseableCommands.php index 5f4146321fe..fd746e051a4 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/RebaseableCommands.php +++ b/Neos.ContentRepository.Core/Classes/Feature/RebaseableCommands.php @@ -30,7 +30,7 @@ public static function extractFromEventStream(EventStreamInterface $eventStream) $commands = []; foreach ($eventStream as $eventEnvelope) { if ($eventEnvelope->event->metadata && isset($eventEnvelope->event->metadata?->value['commandClass'])) { - $commands[] = RebaseableCommand::extractFromEventMetaData($eventEnvelope->event->metadata, $eventEnvelope->sequenceNumber); + $commands[] = RebaseableCommand::extractFromEventEnvelope($eventEnvelope); } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php index cbec3e0d8e0..2d3467eae40 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php +++ b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php @@ -236,8 +236,8 @@ static function ($handle) use ($rebaseableCommands): void { } ); - if ($commandSimulator->hasCommandsThatFailed()) { - throw WorkspaceRebaseFailed::duringPublish($commandSimulator->getCommandsThatFailed()); + if ($commandSimulator->hasEventsThatFailed()) { + throw WorkspaceRebaseFailed::duringPublish($commandSimulator->getEventsThatFailed()); } yield new EventsToPublish( @@ -381,7 +381,7 @@ static function ($handle) use ($rebaseableCommands): void { if ( $command->rebaseErrorHandlingStrategy === RebaseErrorHandlingStrategy::STRATEGY_FAIL - && $commandSimulator->hasCommandsThatFailed() + && $commandSimulator->hasEventsThatFailed() ) { yield $this->reopenContentStream( $workspace->currentContentStreamId, @@ -389,7 +389,7 @@ static function ($handle) use ($rebaseableCommands): void { ); // throw an exception that contains all the information about what exactly failed - throw WorkspaceRebaseFailed::duringRebase($commandSimulator->getCommandsThatFailed()); + throw WorkspaceRebaseFailed::duringRebase($commandSimulator->getEventsThatFailed()); } // if we got so far without an exception (or if we don't care), we can switch the workspace's active content stream. @@ -504,13 +504,13 @@ static function ($handle) use ($commandSimulator, $matchingCommands, $remainingC } ); - if ($commandSimulator->hasCommandsThatFailed()) { + if ($commandSimulator->hasEventsThatFailed()) { yield $this->reopenContentStream( $workspace->currentContentStreamId, $commandHandlingDependencies ); - throw WorkspaceRebaseFailed::duringPublish($commandSimulator->getCommandsThatFailed()); + throw WorkspaceRebaseFailed::duringPublish($commandSimulator->getEventsThatFailed()); } // this could be a no-op for the rare case when a command returns empty events e.g. the node was already tagged with this subtree tag, meaning we actually just rebase @@ -622,12 +622,12 @@ static function ($handle) use ($commandsToKeep): void { } ); - if ($commandSimulator->hasCommandsThatFailed()) { + if ($commandSimulator->hasEventsThatFailed()) { yield $this->reopenContentStream( $workspace->currentContentStreamId, $commandHandlingDependencies ); - throw WorkspaceRebaseFailed::duringDiscard($commandSimulator->getCommandsThatFailed()); + throw WorkspaceRebaseFailed::duringDiscard($commandSimulator->getEventsThatFailed()); } yield from $this->forkNewContentStreamAndApplyEvents( diff --git a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/CommandThatFailedDuringRebase.php b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/CommandThatFailedDuringRebase.php deleted file mode 100644 index 4f6fd46d615..00000000000 --- a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/CommandThatFailedDuringRebase.php +++ /dev/null @@ -1,95 +0,0 @@ -command::class) { - MoveNodeAggregate::class, - SetSerializedNodeProperties::class, - CreateNodeAggregateWithNodeAndSerializedProperties::class, - TagSubtree::class, - DisableNodeAggregate::class, - UntagSubtree::class, - EnableNodeAggregate::class, - RemoveNodeAggregate::class, - ChangeNodeAggregateType::class, - CreateNodeVariant::class => $this->command->nodeAggregateId, - SetSerializedNodeReferences::class => $this->command->sourceNodeAggregateId, - default => null - }; - } - - /** - * How the command failed that was attempted to be rebased - */ - public function getException(): \Throwable - { - return $this->exception; - } - - /** - * The event store sequence number of the event containing the command to be rebased - * - * @internal exposed for testing - */ - public function getSequenceNumber(): SequenceNumber - { - return $this->sequenceNumber; - } - - /** - * The command that failed - * - * @internal exposed for testing and experimental use cases - */ - public function getCommand(): RebasableToOtherWorkspaceInterface - { - return $this->command; - } -} diff --git a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/EventThatFailedDuringRebase.php b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/EventThatFailedDuringRebase.php new file mode 100644 index 00000000000..8c27ba35030 --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/EventThatFailedDuringRebase.php @@ -0,0 +1,74 @@ +event instanceof EmbedsNodeAggregateId + ? $this->event->getNodeAggregateId() + : null; + } + + /** + * How the command failed that was attempted to be rebased + */ + public function getException(): \Throwable + { + return $this->exception; + } + + /** + * The event store sequence number of the event containing the command to be rebased + * + * @internal exposed for testing + */ + public function getSequenceNumber(): SequenceNumber + { + return $this->sequenceNumber; + } + + /** + * The command that failed + * + * @internal exposed for testing and experimental use cases + */ + public function getEvent(): EventInterface + { + return $this->event; + } +} diff --git a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/CommandsThatFailedDuringRebase.php b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/EventsThatFailedDuringRebase.php similarity index 69% rename from Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/CommandsThatFailedDuringRebase.php rename to Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/EventsThatFailedDuringRebase.php index 24f1087ee81..943fab58ce4 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/CommandsThatFailedDuringRebase.php +++ b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/EventsThatFailedDuringRebase.php @@ -15,28 +15,28 @@ namespace Neos\ContentRepository\Core\Feature\WorkspaceRebase; /** - * @implements \IteratorAggregate + * @implements \IteratorAggregate * * @api part of the exception exposed when rebasing failed */ -final readonly class CommandsThatFailedDuringRebase implements \IteratorAggregate, \Countable +final readonly class EventsThatFailedDuringRebase implements \IteratorAggregate, \Countable { /** - * @var array + * @var array */ private array $items; - public function __construct(CommandThatFailedDuringRebase ...$items) + public function __construct(EventThatFailedDuringRebase ...$items) { $this->items = array_values($items); } - public function withAppended(CommandThatFailedDuringRebase $item): self + public function withAppended(EventThatFailedDuringRebase $item): self { return new self(...[...$this->items, $item]); } - public function first(): ?CommandThatFailedDuringRebase + public function first(): ?EventThatFailedDuringRebase { return $this->items[0] ?? null; } diff --git a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/Exception/WorkspaceRebaseFailed.php b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/Exception/WorkspaceRebaseFailed.php index 1ac611e412e..ad6d68afdaf 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/Exception/WorkspaceRebaseFailed.php +++ b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/Exception/WorkspaceRebaseFailed.php @@ -14,7 +14,7 @@ namespace Neos\ContentRepository\Core\Feature\WorkspaceRebase\Exception; -use Neos\ContentRepository\Core\Feature\WorkspaceRebase\CommandsThatFailedDuringRebase; +use Neos\ContentRepository\Core\Feature\WorkspaceRebase\EventsThatFailedDuringRebase; /** * @api this exception contains information about what exactly went wrong during rebase @@ -22,7 +22,7 @@ final class WorkspaceRebaseFailed extends \Exception { private function __construct( - public readonly CommandsThatFailedDuringRebase $commandsThatFailedDuringRebase, + public readonly EventsThatFailedDuringRebase $eventsThatFailedDuringRebase, string $message, int $code, ?\Throwable $previous, @@ -30,39 +30,39 @@ private function __construct( parent::__construct($message, $code, $previous); } - public static function duringRebase(CommandsThatFailedDuringRebase $commandsThatFailedDuringRebase): self + public static function duringRebase(EventsThatFailedDuringRebase $eventsThatFailedDuringRebase): self { return new self( - $commandsThatFailedDuringRebase, - sprintf('Rebase failed: %s', self::renderMessage($commandsThatFailedDuringRebase)), + $eventsThatFailedDuringRebase, + sprintf('Rebase failed: %s', self::renderMessage($eventsThatFailedDuringRebase)), 1729974936, - $commandsThatFailedDuringRebase->first()?->getException() + $eventsThatFailedDuringRebase->first()?->getException() ); } - public static function duringPublish(CommandsThatFailedDuringRebase $commandsThatFailedDuringRebase): self + public static function duringPublish(EventsThatFailedDuringRebase $eventsThatFailedDuringRebase): self { return new self( - $commandsThatFailedDuringRebase, - sprintf('Publication failed: %s', self::renderMessage($commandsThatFailedDuringRebase)), + $eventsThatFailedDuringRebase, + sprintf('Publication failed: %s', self::renderMessage($eventsThatFailedDuringRebase)), 1729974980, - $commandsThatFailedDuringRebase->first()?->getException() + $eventsThatFailedDuringRebase->first()?->getException() ); } - public static function duringDiscard(CommandsThatFailedDuringRebase $commandsThatFailedDuringRebase): self + public static function duringDiscard(EventsThatFailedDuringRebase $eventsThatFailedDuringRebase): self { return new self( - $commandsThatFailedDuringRebase, - sprintf('Discard failed: %s', self::renderMessage($commandsThatFailedDuringRebase)), + $eventsThatFailedDuringRebase, + sprintf('Discard failed: %s', self::renderMessage($eventsThatFailedDuringRebase)), 1729974982, - $commandsThatFailedDuringRebase->first()?->getException() + $eventsThatFailedDuringRebase->first()?->getException() ); } - private static function renderMessage(CommandsThatFailedDuringRebase $commandsThatFailedDuringRebase): string + private static function renderMessage(EventsThatFailedDuringRebase $eventsThatFailedDuringRebase): string { - $firstFailure = $commandsThatFailedDuringRebase->first(); - return sprintf('"%s" and %d further failures', $firstFailure?->getException()->getMessage(), count($commandsThatFailedDuringRebase) - 1); + $firstFailure = $eventsThatFailedDuringRebase->first(); + return sprintf('"%s" and %d further failures', $firstFailure?->getException()->getMessage(), count($eventsThatFailedDuringRebase) - 1); } } diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php index ccd1e73fd42..f7c05d7b553 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php @@ -173,10 +173,10 @@ public function theLastCommandShouldHaveThrownTheWorkspaceRebaseFailedWith(Table Assert::assertInstanceOf(WorkspaceRebaseFailed::class, $exception, sprintf('Actual exception: %s (%s): %s', get_class($exception), $exception->getCode(), $exception->getMessage())); $actualComparableHash = []; - foreach ($exception->commandsThatFailedDuringRebase as $commandsThatFailed) { + foreach ($exception->eventsThatFailedDuringRebase as $commandsThatFailed) { $actualComparableHash[] = [ 'SequenceNumber' => (string)$commandsThatFailed->getSequenceNumber()->value, - 'Command' => (new \ReflectionClass($commandsThatFailed->getCommand()))->getShortName(), + 'Event' => (new \ReflectionClass($commandsThatFailed->getEvent()))->getShortName(), 'Exception' => (new \ReflectionClass($commandsThatFailed->getException()))->getShortName(), ]; } From 48ebbda430ace4b56de876e10042b1764d086ebb Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 9 Nov 2024 17:55:51 +0100 Subject: [PATCH 109/214] TASK: Rename `EventThatFailedDuringRebase` to `ConflictingEvent` because events dont fail usually :D --- .../CommandHandler/CommandSimulator.php | 20 +++++------ .../Feature/WorkspaceCommandHandler.php | 16 ++++----- ...dDuringRebase.php => ConflictingEvent.php} | 8 ++--- ...DuringRebase.php => ConflictingEvents.php} | 12 +++---- .../Exception/WorkspaceRebaseFailed.php | 34 +++++++++---------- ...ricCommandExecutionAndEventPublication.php | 8 ++--- 6 files changed, 49 insertions(+), 49 deletions(-) rename Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/{EventThatFailedDuringRebase.php => ConflictingEvent.php} (89%) rename Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/{EventsThatFailedDuringRebase.php => ConflictingEvents.php} (69%) diff --git a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandSimulator.php b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandSimulator.php index 015fdd86947..1b2d46e5333 100644 --- a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandSimulator.php +++ b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandSimulator.php @@ -9,8 +9,8 @@ use Neos\ContentRepository\Core\EventStore\EventNormalizer; use Neos\ContentRepository\Core\EventStore\EventsToPublish; use Neos\ContentRepository\Core\Feature\RebaseableCommand; -use Neos\ContentRepository\Core\Feature\WorkspaceRebase\EventsThatFailedDuringRebase; -use Neos\ContentRepository\Core\Feature\WorkspaceRebase\EventThatFailedDuringRebase; +use Neos\ContentRepository\Core\Feature\WorkspaceRebase\ConflictingEvents; +use Neos\ContentRepository\Core\Feature\WorkspaceRebase\ConflictingEvent; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphProjectionInterface; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\EventStore\Helper\InMemoryEventStore; @@ -45,7 +45,7 @@ */ final class CommandSimulator { - private EventsThatFailedDuringRebase $eventsThatFailedDuringRebase; + private ConflictingEvents $conflictingEvents; private readonly InMemoryEventStore $inMemoryEventStore; @@ -56,7 +56,7 @@ public function __construct( private readonly WorkspaceName $workspaceNameToSimulateIn, ) { $this->inMemoryEventStore = new InMemoryEventStore(); - $this->eventsThatFailedDuringRebase = new EventsThatFailedDuringRebase(); + $this->conflictingEvents = new ConflictingEvents(); } /** @@ -88,8 +88,8 @@ private function handle(RebaseableCommand $rebaseableCommand): void } catch (\Exception $exception) { $originalEvent = $this->eventNormalizer->denormalize($rebaseableCommand->originalEvent); - $this->eventsThatFailedDuringRebase = $this->eventsThatFailedDuringRebase->withAppended( - new EventThatFailedDuringRebase( + $this->conflictingEvents = $this->conflictingEvents->withAppended( + new ConflictingEvent( $originalEvent, $exception, $rebaseableCommand->originalSequenceNumber @@ -161,13 +161,13 @@ public function eventStream(): EventStreamInterface return $this->inMemoryEventStore->load(VirtualStreamName::all()); } - public function hasEventsThatFailed(): bool + public function hasConflicts(): bool { - return !$this->eventsThatFailedDuringRebase->isEmpty(); + return !$this->conflictingEvents->isEmpty(); } - public function getEventsThatFailed(): EventsThatFailedDuringRebase + public function getConflictingEvents(): ConflictingEvents { - return $this->eventsThatFailedDuringRebase; + return $this->conflictingEvents; } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php index 2d3467eae40..b1be71e7ce6 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php +++ b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php @@ -236,8 +236,8 @@ static function ($handle) use ($rebaseableCommands): void { } ); - if ($commandSimulator->hasEventsThatFailed()) { - throw WorkspaceRebaseFailed::duringPublish($commandSimulator->getEventsThatFailed()); + if ($commandSimulator->hasConflicts()) { + throw WorkspaceRebaseFailed::duringPublish($commandSimulator->getConflictingEvents()); } yield new EventsToPublish( @@ -381,7 +381,7 @@ static function ($handle) use ($rebaseableCommands): void { if ( $command->rebaseErrorHandlingStrategy === RebaseErrorHandlingStrategy::STRATEGY_FAIL - && $commandSimulator->hasEventsThatFailed() + && $commandSimulator->hasConflicts() ) { yield $this->reopenContentStream( $workspace->currentContentStreamId, @@ -389,7 +389,7 @@ static function ($handle) use ($rebaseableCommands): void { ); // throw an exception that contains all the information about what exactly failed - throw WorkspaceRebaseFailed::duringRebase($commandSimulator->getEventsThatFailed()); + throw WorkspaceRebaseFailed::duringRebase($commandSimulator->getConflictingEvents()); } // if we got so far without an exception (or if we don't care), we can switch the workspace's active content stream. @@ -504,13 +504,13 @@ static function ($handle) use ($commandSimulator, $matchingCommands, $remainingC } ); - if ($commandSimulator->hasEventsThatFailed()) { + if ($commandSimulator->hasConflicts()) { yield $this->reopenContentStream( $workspace->currentContentStreamId, $commandHandlingDependencies ); - throw WorkspaceRebaseFailed::duringPublish($commandSimulator->getEventsThatFailed()); + throw WorkspaceRebaseFailed::duringPublish($commandSimulator->getConflictingEvents()); } // this could be a no-op for the rare case when a command returns empty events e.g. the node was already tagged with this subtree tag, meaning we actually just rebase @@ -622,12 +622,12 @@ static function ($handle) use ($commandsToKeep): void { } ); - if ($commandSimulator->hasEventsThatFailed()) { + if ($commandSimulator->hasConflicts()) { yield $this->reopenContentStream( $workspace->currentContentStreamId, $commandHandlingDependencies ); - throw WorkspaceRebaseFailed::duringDiscard($commandSimulator->getEventsThatFailed()); + throw WorkspaceRebaseFailed::duringDiscard($commandSimulator->getConflictingEvents()); } yield from $this->forkNewContentStreamAndApplyEvents( diff --git a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/EventThatFailedDuringRebase.php b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/ConflictingEvent.php similarity index 89% rename from Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/EventThatFailedDuringRebase.php rename to Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/ConflictingEvent.php index 8c27ba35030..21a0fe146e9 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/EventThatFailedDuringRebase.php +++ b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/ConflictingEvent.php @@ -22,7 +22,7 @@ /** * @api part of the exception exposed when rebasing failed */ -final readonly class EventThatFailedDuringRebase +final readonly class ConflictingEvent { /** * @internal @@ -35,7 +35,7 @@ public function __construct( } /** - * The node aggregate id of the failed command + * The node aggregate id of the conflicting event */ public function getAffectedNodeAggregateId(): ?NodeAggregateId { @@ -45,7 +45,7 @@ public function getAffectedNodeAggregateId(): ?NodeAggregateId } /** - * How the command failed that was attempted to be rebased + * The exception for the conflict */ public function getException(): \Throwable { @@ -63,7 +63,7 @@ public function getSequenceNumber(): SequenceNumber } /** - * The command that failed + * The event that conflicts * * @internal exposed for testing and experimental use cases */ diff --git a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/EventsThatFailedDuringRebase.php b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/ConflictingEvents.php similarity index 69% rename from Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/EventsThatFailedDuringRebase.php rename to Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/ConflictingEvents.php index 943fab58ce4..1b30aa61007 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/EventsThatFailedDuringRebase.php +++ b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/ConflictingEvents.php @@ -15,28 +15,28 @@ namespace Neos\ContentRepository\Core\Feature\WorkspaceRebase; /** - * @implements \IteratorAggregate + * @implements \IteratorAggregate * * @api part of the exception exposed when rebasing failed */ -final readonly class EventsThatFailedDuringRebase implements \IteratorAggregate, \Countable +final readonly class ConflictingEvents implements \IteratorAggregate, \Countable { /** - * @var array + * @var array */ private array $items; - public function __construct(EventThatFailedDuringRebase ...$items) + public function __construct(ConflictingEvent ...$items) { $this->items = array_values($items); } - public function withAppended(EventThatFailedDuringRebase $item): self + public function withAppended(ConflictingEvent $item): self { return new self(...[...$this->items, $item]); } - public function first(): ?EventThatFailedDuringRebase + public function first(): ?ConflictingEvent { return $this->items[0] ?? null; } diff --git a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/Exception/WorkspaceRebaseFailed.php b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/Exception/WorkspaceRebaseFailed.php index ad6d68afdaf..499ea7a247c 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/Exception/WorkspaceRebaseFailed.php +++ b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/Exception/WorkspaceRebaseFailed.php @@ -14,7 +14,7 @@ namespace Neos\ContentRepository\Core\Feature\WorkspaceRebase\Exception; -use Neos\ContentRepository\Core\Feature\WorkspaceRebase\EventsThatFailedDuringRebase; +use Neos\ContentRepository\Core\Feature\WorkspaceRebase\ConflictingEvents; /** * @api this exception contains information about what exactly went wrong during rebase @@ -22,7 +22,7 @@ final class WorkspaceRebaseFailed extends \Exception { private function __construct( - public readonly EventsThatFailedDuringRebase $eventsThatFailedDuringRebase, + public readonly ConflictingEvents $conflictingEvents, string $message, int $code, ?\Throwable $previous, @@ -30,39 +30,39 @@ private function __construct( parent::__construct($message, $code, $previous); } - public static function duringRebase(EventsThatFailedDuringRebase $eventsThatFailedDuringRebase): self + public static function duringRebase(ConflictingEvents $conflictingEvents): self { return new self( - $eventsThatFailedDuringRebase, - sprintf('Rebase failed: %s', self::renderMessage($eventsThatFailedDuringRebase)), + $conflictingEvents, + sprintf('Rebase failed: %s', self::renderMessage($conflictingEvents)), 1729974936, - $eventsThatFailedDuringRebase->first()?->getException() + $conflictingEvents->first()?->getException() ); } - public static function duringPublish(EventsThatFailedDuringRebase $eventsThatFailedDuringRebase): self + public static function duringPublish(ConflictingEvents $conflictingEvents): self { return new self( - $eventsThatFailedDuringRebase, - sprintf('Publication failed: %s', self::renderMessage($eventsThatFailedDuringRebase)), + $conflictingEvents, + sprintf('Publication failed: %s', self::renderMessage($conflictingEvents)), 1729974980, - $eventsThatFailedDuringRebase->first()?->getException() + $conflictingEvents->first()?->getException() ); } - public static function duringDiscard(EventsThatFailedDuringRebase $eventsThatFailedDuringRebase): self + public static function duringDiscard(ConflictingEvents $conflictingEvents): self { return new self( - $eventsThatFailedDuringRebase, - sprintf('Discard failed: %s', self::renderMessage($eventsThatFailedDuringRebase)), + $conflictingEvents, + sprintf('Discard failed: %s', self::renderMessage($conflictingEvents)), 1729974982, - $eventsThatFailedDuringRebase->first()?->getException() + $conflictingEvents->first()?->getException() ); } - private static function renderMessage(EventsThatFailedDuringRebase $eventsThatFailedDuringRebase): string + private static function renderMessage(ConflictingEvents $conflictingEvents): string { - $firstFailure = $eventsThatFailedDuringRebase->first(); - return sprintf('"%s" and %d further failures', $firstFailure?->getException()->getMessage(), count($eventsThatFailedDuringRebase) - 1); + $firstConflict = $conflictingEvents->first(); + return sprintf('"%s" and %d further conflicts', $firstConflict?->getException()->getMessage(), count($conflictingEvents) - 1); } } diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php index f7c05d7b553..cdc96abf93b 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php @@ -173,11 +173,11 @@ public function theLastCommandShouldHaveThrownTheWorkspaceRebaseFailedWith(Table Assert::assertInstanceOf(WorkspaceRebaseFailed::class, $exception, sprintf('Actual exception: %s (%s): %s', get_class($exception), $exception->getCode(), $exception->getMessage())); $actualComparableHash = []; - foreach ($exception->eventsThatFailedDuringRebase as $commandsThatFailed) { + foreach ($exception->conflictingEvents as $conflictingEvent) { $actualComparableHash[] = [ - 'SequenceNumber' => (string)$commandsThatFailed->getSequenceNumber()->value, - 'Event' => (new \ReflectionClass($commandsThatFailed->getEvent()))->getShortName(), - 'Exception' => (new \ReflectionClass($commandsThatFailed->getException()))->getShortName(), + 'SequenceNumber' => (string)$conflictingEvent->getSequenceNumber()->value, + 'Event' => (new \ReflectionClass($conflictingEvent->getEvent()))->getShortName(), + 'Exception' => (new \ReflectionClass($conflictingEvent->getException()))->getShortName(), ]; } From 8b8eea7700669e1edaece155d6ebbfc60a39022a Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Sat, 9 Nov 2024 19:16:46 +0100 Subject: [PATCH 110/214] Extract authenticated account from current Neos user ...instead of the security context --- Neos.Neos/Classes/Domain/Model/User.php | 12 ++++++++++-- .../Classes/Controller/WorkspaceController.php | 12 ++++-------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/Neos.Neos/Classes/Domain/Model/User.php b/Neos.Neos/Classes/Domain/Model/User.php index 24db0c6cfc3..33abe790644 100644 --- a/Neos.Neos/Classes/Domain/Model/User.php +++ b/Neos.Neos/Classes/Domain/Model/User.php @@ -92,13 +92,21 @@ public function setPreferences(UserPreferences $preferences) * @api */ public function isActive() + { + return $this->getFirstActiveAccount() !== null; + } + + /** + * @api + */ + public function getFirstActiveAccount(): ?Account { foreach ($this->accounts as $account) { /** @var Account $account */ if ($account->isActive()) { - return true; + return $account; } } - return false; + return null; } } diff --git a/Neos.Workspace.Ui/Classes/Controller/WorkspaceController.php b/Neos.Workspace.Ui/Classes/Controller/WorkspaceController.php index 2a8665181de..3b45f907c58 100644 --- a/Neos.Workspace.Ui/Classes/Controller/WorkspaceController.php +++ b/Neos.Workspace.Ui/Classes/Controller/WorkspaceController.php @@ -43,7 +43,6 @@ use Neos\Flow\Mvc\Exception\StopActionException; use Neos\Flow\Package\PackageManager; use Neos\Flow\Property\PropertyMapper; -use Neos\Flow\Security\Context; use Neos\Flow\Security\Exception\AccessDeniedException; use Neos\Media\Domain\Model\AssetInterface; use Neos\Media\Domain\Model\ImageInterface; @@ -91,9 +90,6 @@ class WorkspaceController extends AbstractModuleController #[Flow\Inject] protected PropertyMapper $propertyMapper; - #[Flow\Inject] - protected Context $securityContext; - #[Flow\Inject] protected UserService $userService; @@ -114,7 +110,7 @@ class WorkspaceController extends AbstractModuleController */ public function indexAction(): void { - $authenticatedAccount = $this->securityContext->getAccount(); + $authenticatedAccount = $this->userService->getCurrentUser()?->getFirstActiveAccount(); if ($authenticatedAccount === null) { throw new AccessDeniedException('No user authenticated', 1718308216); } @@ -164,7 +160,7 @@ classification: $workspaceMetadata->classification->name, public function showAction(WorkspaceName $workspace): void { - $authenticatedAccount = $this->securityContext->getAccount(); + $authenticatedAccount = $this->userService->getCurrentUser()?->getFirstActiveAccount(); if ($authenticatedAccount === null) { throw new AccessDeniedException('No user authenticated', 1720371024); } @@ -293,7 +289,7 @@ public function updateAction( $contentRepositoryId = SiteDetectionResult::fromRequest($this->request->getHttpRequest())->contentRepositoryId; $contentRepository = $this->contentRepositoryRegistry->get($contentRepositoryId); - $authenticatedAccount = $this->securityContext->getAccount(); + $authenticatedAccount = $this->userService->getCurrentUser()?->getFirstActiveAccount(); if ($authenticatedAccount === null) { throw new AccessDeniedException('No user is authenticated', 1729620262); } @@ -1012,7 +1008,7 @@ protected function prepareBaseWorkspaceOptions( ContentRepository $contentRepository, WorkspaceName $excludedWorkspace = null, ): array { - $authenticatedAccount = $this->securityContext->getAccount(); + $authenticatedAccount = $this->userService->getCurrentUser()?->getFirstActiveAccount(); $baseWorkspaceOptions = []; $workspaces = $contentRepository->findWorkspaces(); foreach ($workspaces as $workspace) { From 4352b2e02f0d77d6896554f924c4b8cb5fb3cd3e Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 9 Nov 2024 11:46:40 +0100 Subject: [PATCH 111/214] BUGFIX: Fix reopen content stream if base workspace was written to during publication ... and a ConcurrencyException is thrown Introduces a `WorkspacePublicationDuringWritingTest` parallel test (with own cr) to assert that behaviour. --- .../Configuration/Settings.yaml | 18 +- .../Parallel/AbstractParallelTestCase.php | 11 +- .../WorkspacePublicationDuringWritingTest.php | 249 ++++++++++++++++++ ...p => WorkspaceWritingDuringRebaseTest.php} | 10 +- .../Classes/ContentRepository.php | 11 +- .../Feature/WorkspaceCommandHandler.php | 38 +-- Neos.Neos/Classes/Domain/Model/UserId.php | 2 +- composer.json | 2 +- 8 files changed, 313 insertions(+), 28 deletions(-) create mode 100644 Neos.ContentRepository.BehavioralTests/Tests/Parallel/WorkspacePublicationDuringWriting/WorkspacePublicationDuringWritingTest.php rename Neos.ContentRepository.BehavioralTests/Tests/Parallel/WorkspaceWritingDuringRebase/{WorkspaceWritingDuringRebase.php => WorkspaceWritingDuringRebaseTest.php} (97%) diff --git a/Neos.ContentRepository.BehavioralTests/Configuration/Settings.yaml b/Neos.ContentRepository.BehavioralTests/Configuration/Settings.yaml index 212fb010b63..dd92883ec16 100644 --- a/Neos.ContentRepository.BehavioralTests/Configuration/Settings.yaml +++ b/Neos.ContentRepository.BehavioralTests/Configuration/Settings.yaml @@ -20,7 +20,7 @@ Neos: ContentRepositoryRegistry: contentRepositories: - test_parallel: + test_parallel_a: eventStore: factoryObjectName: Neos\ContentRepositoryRegistry\Factory\EventStore\DoctrineEventStoreFactory nodeTypeManager: @@ -35,6 +35,22 @@ Neos: contentGraphProjection: factoryObjectName: Neos\ContentGraph\DoctrineDbalAdapter\DoctrineDbalContentGraphProjectionFactory + test_parallel_b: + eventStore: + factoryObjectName: Neos\ContentRepositoryRegistry\Factory\EventStore\DoctrineEventStoreFactory + nodeTypeManager: + factoryObjectName: Neos\ContentRepository\BehavioralTests\TestSuite\Behavior\GherkinPyStringNodeBasedNodeTypeManagerFactory + contentDimensionSource: + factoryObjectName: Neos\ContentRepository\BehavioralTests\TestSuite\Behavior\GherkinTableNodeBasedContentDimensionSourceFactory + userIdProvider: + factoryObjectName: Neos\ContentRepositoryRegistry\Factory\UserIdProvider\StaticUserIdProviderFactory + clock: + factoryObjectName: Neos\ContentRepositoryRegistry\Factory\Clock\SystemClockFactory + propertyConverters: {} + contentGraphProjection: + factoryObjectName: Neos\ContentGraph\DoctrineDbalAdapter\DoctrineDbalContentGraphProjectionFactory + + Flow: object: includeClasses: diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Parallel/AbstractParallelTestCase.php b/Neos.ContentRepository.BehavioralTests/Tests/Parallel/AbstractParallelTestCase.php index 67afbdc91ab..ef97e1bea36 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Parallel/AbstractParallelTestCase.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Parallel/AbstractParallelTestCase.php @@ -52,14 +52,15 @@ final protected function awaitFile(string $filename): void } } - final protected function awaitSharedLock($resource, int $maximumCycles = 2000): void + final protected function awaitFileRemoval(string $filename): void { $waiting = 0; - while (!flock($resource, LOCK_SH)) { - usleep(10000); + while (!is_file($filename)) { + usleep(1000); $waiting++; - if ($waiting > $maximumCycles) { - throw new \Exception('timeout while waiting on shared lock'); + clearstatcache(true, $filename); + if ($waiting > 60000) { + throw new \Exception('timeout while waiting on file ' . $filename); } } } diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Parallel/WorkspacePublicationDuringWriting/WorkspacePublicationDuringWritingTest.php b/Neos.ContentRepository.BehavioralTests/Tests/Parallel/WorkspacePublicationDuringWriting/WorkspacePublicationDuringWritingTest.php new file mode 100644 index 00000000000..8de4ffbf10b --- /dev/null +++ b/Neos.ContentRepository.BehavioralTests/Tests/Parallel/WorkspacePublicationDuringWriting/WorkspacePublicationDuringWritingTest.php @@ -0,0 +1,249 @@ +log('------ process started ------'); + // todo refrain from Gherkin naming here and make fakes easier to use: https://github.com/neos/neos-development-collection/pull/5346 + GherkinTableNodeBasedContentDimensionSourceFactory::$contentDimensionsToUse = new class implements ContentDimensionSourceInterface + { + public function getDimension(ContentDimensionId $dimensionId): ?ContentDimension + { + return null; + } + public function getContentDimensionsOrderedByPriority(): array + { + return []; + } + }; + // todo refrain from Gherkin naming here and make fakes easier to use: https://github.com/neos/neos-development-collection/pull/5346 + GherkinPyStringNodeBasedNodeTypeManagerFactory::$nodeTypesToUse = new NodeTypeManager( + fn (): array => [ + 'Neos.ContentRepository:Root' => [], + 'Neos.ContentRepository.Testing:Document' => [ + 'properties' => [ + 'title' => [ + 'type' => 'string' + ] + ] + ] + ] + ); + + $setupLockResource = fopen(self::SETUP_LOCK_PATH, 'w+'); + + $exclusiveNonBlockingLockResult = flock($setupLockResource, LOCK_EX | LOCK_NB); + if ($exclusiveNonBlockingLockResult === false) { + $this->log('waiting for setup'); + if (!flock($setupLockResource, LOCK_SH)) { + throw new \RuntimeException('failed to acquire blocking shared lock'); + } + $this->contentRepository = $this->contentRepositoryRegistry + ->get(ContentRepositoryId::fromString('test_parallel_a')); + $this->log('wait for setup finished'); + return; + } + + $this->log('setup started'); + $contentRepository = $this->setUpContentRepository(ContentRepositoryId::fromString('test_parallel_a')); + + $origin = OriginDimensionSpacePoint::createWithoutDimensions(); + $contentRepository->handle(CreateRootWorkspace::create( + WorkspaceName::forLive(), + ContentStreamId::fromString('live-cs-id') + )); + $contentRepository->handle(CreateRootNodeAggregateWithNode::create( + WorkspaceName::forLive(), + NodeAggregateId::fromString('lady-eleonode-rootford'), + NodeTypeName::fromString(NodeTypeName::ROOT_NODE_TYPE_NAME) + )); + $contentRepository->handle(CreateNodeAggregateWithNode::create( + WorkspaceName::forLive(), + NodeAggregateId::fromString('nody-mc-nodeface'), + NodeTypeName::fromString('Neos.ContentRepository.Testing:Document'), + $origin, + NodeAggregateId::fromString('lady-eleonode-rootford'), + initialPropertyValues: PropertyValuesToWrite::fromArray([ + 'title' => 'title-original' + ]) + )); + $contentRepository->handle(CreateWorkspace::create( + WorkspaceName::fromString('user-test'), + WorkspaceName::forLive(), + ContentStreamId::fromString('user-cs-id') + )); + for ($i = 0; $i <= 5000; $i++) { + $contentRepository->handle(CreateNodeAggregateWithNode::create( + WorkspaceName::fromString('user-test'), + NodeAggregateId::fromString('nody-mc-nodeface-' . $i), + NodeTypeName::fromString('Neos.ContentRepository.Testing:Document'), + $origin, + NodeAggregateId::fromString('lady-eleonode-rootford'), + initialPropertyValues: PropertyValuesToWrite::fromArray([ + 'title' => 'title' + ]) + )); + } + $this->contentRepository = $contentRepository; + + if (!flock($setupLockResource, LOCK_UN)) { + throw new \RuntimeException('failed to release setup lock'); + } + + $this->log('setup finished'); + } + + /** + * @test + * @group parallel + */ + public function whileANodesArWrittenOnLive(): void + { + $this->log('writing started'); + + touch(self::WRITING_IS_RUNNING_FLAG_PATH); + + try { + for ($i = 0; $i <= 50; $i++) { + $this->contentRepository->handle( + SetNodeProperties::create( + WorkspaceName::forLive(), + NodeAggregateId::fromString('nody-mc-nodeface'), + OriginDimensionSpacePoint::createWithoutDimensions(), + PropertyValuesToWrite::fromArray([ + 'title' => 'changed-title-' . $i + ]) + ) + ); + } + } finally { + unlink(self::WRITING_IS_RUNNING_FLAG_PATH); + } + + $this->log('writing finished'); + Assert::assertTrue(true, 'No exception was thrown ;)'); + } + + /** + * @test + * @group parallel + */ + public function thenConcurrentPublishLeadsToException(): void + { + if (!is_file(self::WRITING_IS_RUNNING_FLAG_PATH)) { + $this->log('waiting to publish'); + + $this->awaitFile(self::WRITING_IS_RUNNING_FLAG_PATH); + // If write is the process that does the (slowish) setup, and then waits for the rebase to start, + // We give the CR some time to close the content stream + // TODO find another way than to randomly wait!!! + // The problem is, if we dont sleep it happens often that the modification works only then the rebase is startet _really_ + // Doing the modification several times in hope that the second one fails will likely just stop the rebase thread as it cannot close + usleep(10000); + } + + $this->log('publish started'); + + $actualException = null; + try { + $this->contentRepository->handle(PublishWorkspace::create( + WorkspaceName::fromString('user-test') + )); + } catch (\Exception $thrownException) { + $actualException = $thrownException; + } + + $this->log('publish finished'); + + if ($actualException === null) { + Assert::fail(sprintf('No exception was thrown')); + } + + if ($actualException instanceof \RuntimeException && $actualException->getCode() === 1652279016) { + // todo can be removed soon + $this->log(sprintf('got expected RuntimeException exception: %s', $actualException->getMessage())); + } elseif ($actualException instanceof ConcurrencyException) { + $this->log(sprintf('got expected ConcurrencyException exception: %s', $actualException->getMessage())); + } else { + Assert::assertInstanceOf(ConcurrencyException::class, $actualException); + } + + $this->awaitFileRemoval(self::WRITING_IS_RUNNING_FLAG_PATH); + + // just to make sure were up-to-date now! + $this->contentRepository->catchupProjections(); + + // writing to user works!!! + try { + $this->contentRepository->handle( + SetNodeProperties::create( + WorkspaceName::fromString('user-test'), + NodeAggregateId::fromString('nody-mc-nodeface'), + OriginDimensionSpacePoint::createWithoutDimensions(), + PropertyValuesToWrite::fromArray([ + 'title' => 'written-after-failed-publish' + ]) + ) + ); + } catch (ContentStreamIsClosed $exception) { + Assert::fail(sprintf('Workspace that failed to be publish cannot be written: %s', $exception->getMessage())); + } + + $node = $this->contentRepository->getContentGraph(WorkspaceName::fromString('user-test')) + ->getSubgraph(DimensionSpacePoint::createWithoutDimensions(), VisibilityConstraints::withoutRestrictions()) + ->findNodeById(NodeAggregateId::fromString('nody-mc-nodeface')); + + Assert::assertSame('written-after-failed-publish', $node?->getProperty('title')); + } +} diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Parallel/WorkspaceWritingDuringRebase/WorkspaceWritingDuringRebase.php b/Neos.ContentRepository.BehavioralTests/Tests/Parallel/WorkspaceWritingDuringRebase/WorkspaceWritingDuringRebaseTest.php similarity index 97% rename from Neos.ContentRepository.BehavioralTests/Tests/Parallel/WorkspaceWritingDuringRebase/WorkspaceWritingDuringRebase.php rename to Neos.ContentRepository.BehavioralTests/Tests/Parallel/WorkspaceWritingDuringRebase/WorkspaceWritingDuringRebaseTest.php index dedd5d918da..ef66c7ceccd 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Parallel/WorkspaceWritingDuringRebase/WorkspaceWritingDuringRebase.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Parallel/WorkspaceWritingDuringRebase/WorkspaceWritingDuringRebaseTest.php @@ -43,7 +43,7 @@ use Neos\Flow\ObjectManagement\ObjectManagerInterface; use PHPUnit\Framework\Assert; -class WorkspaceWritingDuringRebase extends AbstractParallelTestCase +class WorkspaceWritingDuringRebaseTest extends AbstractParallelTestCase { private const SETUP_LOCK_PATH = __DIR__ . '/setup-lock'; @@ -88,15 +88,17 @@ public function getContentDimensionsOrderedByPriority(): array $exclusiveNonBlockingLockResult = flock($setupLockResource, LOCK_EX | LOCK_NB); if ($exclusiveNonBlockingLockResult === false) { $this->log('waiting for setup'); - $this->awaitSharedLock($setupLockResource); + if (!flock($setupLockResource, LOCK_SH)) { + throw new \RuntimeException('failed to acquire blocking shared lock'); + } $this->contentRepository = $this->contentRepositoryRegistry - ->get(ContentRepositoryId::fromString('test_parallel')); + ->get(ContentRepositoryId::fromString('test_parallel_b')); $this->log('wait for setup finished'); return; } $this->log('setup started'); - $contentRepository = $this->setUpContentRepository(ContentRepositoryId::fromString('test_parallel')); + $contentRepository = $this->setUpContentRepository(ContentRepositoryId::fromString('test_parallel_b')); $origin = OriginDimensionSpacePoint::createWithoutDimensions(); $contentRepository->handle(CreateRootWorkspace::create( diff --git a/Neos.ContentRepository.Core/Classes/ContentRepository.php b/Neos.ContentRepository.Core/Classes/ContentRepository.php index 592870b3412..c3277a1d4f3 100644 --- a/Neos.ContentRepository.Core/Classes/ContentRepository.php +++ b/Neos.ContentRepository.Core/Classes/ContentRepository.php @@ -46,6 +46,7 @@ use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepository\Core\SharedModel\Workspace\Workspaces; use Neos\EventStore\EventStoreInterface; +use Neos\EventStore\Exception\ConcurrencyException; use Neos\EventStore\Model\EventEnvelope; use Neos\EventStore\Model\EventStream\VirtualStreamName; use Psr\Clock\ClockInterface; @@ -116,7 +117,15 @@ public function handle(CommandInterface $command): void continue; } $eventsToPublish = $this->enrichEventsToPublishWithMetadata($yieldedEventsToPublish); - $commitResult = $this->eventPersister->publishWithoutCatchup($eventsToPublish); + try { + $commitResult = $this->eventPersister->publishWithoutCatchup($eventsToPublish); + } catch (ConcurrencyException $concurrencyException) { + $yieldedErrorStrategy = $toPublish->throw($concurrencyException); + if ($yieldedErrorStrategy instanceof EventsToPublish) { + $this->eventPersister->publishWithoutCatchup($yieldedErrorStrategy); + } + throw $concurrencyException; + } $yieldedEventsToPublish = $toPublish->send($commitResult); } } finally { diff --git a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php index 0be16cd86cb..5ceb1c02076 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php +++ b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php @@ -63,6 +63,7 @@ use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceStatus; use Neos\EventStore\EventStoreInterface; +use Neos\EventStore\Exception\ConcurrencyException; use Neos\EventStore\Model\Event\SequenceNumber; use Neos\EventStore\Model\Event\Version; use Neos\EventStore\Model\EventStream\EventStreamInterface; @@ -217,12 +218,12 @@ private function handlePublishWorkspace( $command->newContentStreamId, $rebaseableCommands ); - } catch (WorkspaceRebaseFailed $workspaceRebaseFailed) { // and rethrow in yield - // todo catch all + } catch (WorkspaceRebaseFailed|ConcurrencyException $publishFailed) { + // todo catch all? Dont catch ANY ConcurrencyException because say if forking failed we dont need to reopen? yield $this->reopenContentStreamWithoutConstraints( $workspace->currentContentStreamId ); - throw $workspaceRebaseFailed; + throw $publishFailed; } } @@ -247,6 +248,7 @@ static function ($handle) use ($rebaseableCommands): void { throw WorkspaceRebaseFailed::duringPublish($commandSimulator->getCommandsThatFailed()); } + // todo throw base workspace was modified in the meantime to distinguish exception above to reopen? $commitResult = yield new EventsToPublish( ContentStreamEventStreamName::fromContentStreamId($baseWorkspace->currentContentStreamId) ->getEventStreamName(), @@ -255,7 +257,6 @@ static function ($handle) use ($rebaseableCommands): void { $baseWorkspace->currentContentStreamId, $commandSimulator->eventStream(), ), - // todo can fail; must reopen!!!!! ExpectedVersion::fromVersion($baseWorkspaceContentStreamVersion) ); @@ -514,17 +515,24 @@ static function ($handle) use ($commandSimulator, $matchingCommands, $remainingC throw WorkspaceRebaseFailed::duringPublish($commandSimulator->getCommandsThatFailed()); } - // this could be a no-op for the rare case when a command returns empty events e.g. the node was already tagged with this subtree tag, meaning we actually just rebase - $commitResult = yield new EventsToPublish( - ContentStreamEventStreamName::fromContentStreamId($baseWorkspace->currentContentStreamId) - ->getEventStreamName(), - $this->getCopiedEventsOfEventStream( - $baseWorkspace->workspaceName, - $baseWorkspace->currentContentStreamId, - $commandSimulator->eventStream()->withMaximumSequenceNumber($highestSequenceNumberForMatching), - ), - ExpectedVersion::fromVersion($baseWorkspaceContentStreamVersion) - ); + try { + // this could be a no-op for the rare case when a command returns empty events e.g. the node was already tagged with this subtree tag, meaning we actually just rebase + $commitResult = yield new EventsToPublish( + ContentStreamEventStreamName::fromContentStreamId($baseWorkspace->currentContentStreamId) + ->getEventStreamName(), + $this->getCopiedEventsOfEventStream( + $baseWorkspace->workspaceName, + $baseWorkspace->currentContentStreamId, + $commandSimulator->eventStream()->withMaximumSequenceNumber($highestSequenceNumberForMatching), + ), + ExpectedVersion::fromVersion($baseWorkspaceContentStreamVersion) + ); + } catch (ConcurrencyException $concurrencyException) { + yield $this->reopenContentStreamWithoutConstraints( + $workspace->currentContentStreamId + ); + throw $concurrencyException; + } yield from $this->forkNewContentStreamAndApplyEvents( $command->contentStreamIdForRemainingPart, diff --git a/Neos.Neos/Classes/Domain/Model/UserId.php b/Neos.Neos/Classes/Domain/Model/UserId.php index 2011ebb9a56..cf73375ddee 100644 --- a/Neos.Neos/Classes/Domain/Model/UserId.php +++ b/Neos.Neos/Classes/Domain/Model/UserId.php @@ -15,7 +15,7 @@ public function __construct( public string $value ) { if (!preg_match('/^([a-z0-9\-]{1,40})$/', $value)) { - throw new \InvalidArgumentException(sprintf('Invalid user id "%s" (a user id must only contain lowercase characters, numbers and the "-" sign).', 1718293224)); + throw new \InvalidArgumentException(sprintf('Invalid user id "%s" (a user id must only contain lowercase characters, numbers and the "-" sign).', $this->value), 1718293224); } } diff --git a/composer.json b/composer.json index 9a568f75d0b..acaedea5359 100644 --- a/composer.json +++ b/composer.json @@ -109,7 +109,7 @@ "../../bin/phpunit --colors --stop-on-failure -c ../../Build/BuildEssentials/PhpUnit/UnitTests.xml Neos.ContentRepositoryRegistry/Tests/Unit" ], "test:parallel": [ - "FLOW_CONTEXT=Testing/Behat ../../bin/paratest --debug -v --functional --group parallel --processes 2 --colors --stop-on-failure -c ../../Build/BuildEssentials/PhpUnit/FunctionalTests.xml Neos.ContentRepository.BehavioralTests/Tests/Parallel/WorkspaceWritingDuringRebase/WorkspaceWritingDuringRebase.php" + "FLOW_CONTEXT=Testing/Behat ../../bin/paratest --debug -v --functional --processes 2 --colors --stop-on-failure -c ../../Build/BuildEssentials/PhpUnit/FunctionalTests.xml Neos.ContentRepository.BehavioralTests/Tests/Parallel" ], "test:behat-cli": "../../bin/behat -f progress --strict --no-interaction", "test:behavioral": [ From 18072f695823924fae05c2455a8b356fab8a96a2 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 9 Nov 2024 20:10:54 +0100 Subject: [PATCH 112/214] TASK: Fix parallel tests by ensuring only one is run at time That allows us to use the same content repository. Previously a super slow paratest would lead that another testcase will already be started and its setup then run twice at the end. https://github.com/paratestphp/paratest/discussions/905 --- .../Configuration/Settings.yaml | 18 +----------------- .../WorkspacePublicationDuringWritingTest.php | 4 ++-- .../WorkspaceWritingDuringRebaseTest.php | 4 ++-- composer.json | 11 ++++++++++- 4 files changed, 15 insertions(+), 22 deletions(-) diff --git a/Neos.ContentRepository.BehavioralTests/Configuration/Settings.yaml b/Neos.ContentRepository.BehavioralTests/Configuration/Settings.yaml index dd92883ec16..212fb010b63 100644 --- a/Neos.ContentRepository.BehavioralTests/Configuration/Settings.yaml +++ b/Neos.ContentRepository.BehavioralTests/Configuration/Settings.yaml @@ -20,7 +20,7 @@ Neos: ContentRepositoryRegistry: contentRepositories: - test_parallel_a: + test_parallel: eventStore: factoryObjectName: Neos\ContentRepositoryRegistry\Factory\EventStore\DoctrineEventStoreFactory nodeTypeManager: @@ -35,22 +35,6 @@ Neos: contentGraphProjection: factoryObjectName: Neos\ContentGraph\DoctrineDbalAdapter\DoctrineDbalContentGraphProjectionFactory - test_parallel_b: - eventStore: - factoryObjectName: Neos\ContentRepositoryRegistry\Factory\EventStore\DoctrineEventStoreFactory - nodeTypeManager: - factoryObjectName: Neos\ContentRepository\BehavioralTests\TestSuite\Behavior\GherkinPyStringNodeBasedNodeTypeManagerFactory - contentDimensionSource: - factoryObjectName: Neos\ContentRepository\BehavioralTests\TestSuite\Behavior\GherkinTableNodeBasedContentDimensionSourceFactory - userIdProvider: - factoryObjectName: Neos\ContentRepositoryRegistry\Factory\UserIdProvider\StaticUserIdProviderFactory - clock: - factoryObjectName: Neos\ContentRepositoryRegistry\Factory\Clock\SystemClockFactory - propertyConverters: {} - contentGraphProjection: - factoryObjectName: Neos\ContentGraph\DoctrineDbalAdapter\DoctrineDbalContentGraphProjectionFactory - - Flow: object: includeClasses: diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Parallel/WorkspacePublicationDuringWriting/WorkspacePublicationDuringWritingTest.php b/Neos.ContentRepository.BehavioralTests/Tests/Parallel/WorkspacePublicationDuringWriting/WorkspacePublicationDuringWritingTest.php index 8de4ffbf10b..e6bfcdf7f46 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Parallel/WorkspacePublicationDuringWriting/WorkspacePublicationDuringWritingTest.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Parallel/WorkspacePublicationDuringWriting/WorkspacePublicationDuringWritingTest.php @@ -90,13 +90,13 @@ public function getContentDimensionsOrderedByPriority(): array throw new \RuntimeException('failed to acquire blocking shared lock'); } $this->contentRepository = $this->contentRepositoryRegistry - ->get(ContentRepositoryId::fromString('test_parallel_a')); + ->get(ContentRepositoryId::fromString('test_parallel')); $this->log('wait for setup finished'); return; } $this->log('setup started'); - $contentRepository = $this->setUpContentRepository(ContentRepositoryId::fromString('test_parallel_a')); + $contentRepository = $this->setUpContentRepository(ContentRepositoryId::fromString('test_parallel')); $origin = OriginDimensionSpacePoint::createWithoutDimensions(); $contentRepository->handle(CreateRootWorkspace::create( diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Parallel/WorkspaceWritingDuringRebase/WorkspaceWritingDuringRebaseTest.php b/Neos.ContentRepository.BehavioralTests/Tests/Parallel/WorkspaceWritingDuringRebase/WorkspaceWritingDuringRebaseTest.php index ef66c7ceccd..b1022e2b6e6 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Parallel/WorkspaceWritingDuringRebase/WorkspaceWritingDuringRebaseTest.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Parallel/WorkspaceWritingDuringRebase/WorkspaceWritingDuringRebaseTest.php @@ -92,13 +92,13 @@ public function getContentDimensionsOrderedByPriority(): array throw new \RuntimeException('failed to acquire blocking shared lock'); } $this->contentRepository = $this->contentRepositoryRegistry - ->get(ContentRepositoryId::fromString('test_parallel_b')); + ->get(ContentRepositoryId::fromString('test_parallel')); $this->log('wait for setup finished'); return; } $this->log('setup started'); - $contentRepository = $this->setUpContentRepository(ContentRepositoryId::fromString('test_parallel_b')); + $contentRepository = $this->setUpContentRepository(ContentRepositoryId::fromString('test_parallel')); $origin = OriginDimensionSpacePoint::createWithoutDimensions(); $contentRepository->handle(CreateRootWorkspace::create( diff --git a/composer.json b/composer.json index acaedea5359..9e2e8bca36a 100644 --- a/composer.json +++ b/composer.json @@ -109,7 +109,7 @@ "../../bin/phpunit --colors --stop-on-failure -c ../../Build/BuildEssentials/PhpUnit/UnitTests.xml Neos.ContentRepositoryRegistry/Tests/Unit" ], "test:parallel": [ - "FLOW_CONTEXT=Testing/Behat ../../bin/paratest --debug -v --functional --processes 2 --colors --stop-on-failure -c ../../Build/BuildEssentials/PhpUnit/FunctionalTests.xml Neos.ContentRepository.BehavioralTests/Tests/Parallel" + "for f in Neos.ContentRepository.BehavioralTests/Tests/Parallel/**/*Test.php; do ../../bin/paratest --debug -v --functional --processes 2 --colors --stop-on-failure -c ../../Build/BuildEssentials/PhpUnit/FunctionalTests.xml $f; done" ], "test:behat-cli": "../../bin/behat -f progress --strict --no-interaction", "test:behavioral": [ @@ -296,5 +296,14 @@ "phpunit/phpunit": "^9.0", "neos/behat": "*", "league/flysystem-memory": "^3" + }, + + "config": { + "_comment": "We need to insert a vendor dir (even though composer install MUST NOT be run here) but so autoloading works for composer scripts", + "vendor-dir": "../Libraries", + "allow-plugins": { + "neos/composer-plugin": false, + "cweagans/composer-patches": false + } } } From 9a6127b6f553ca4cf32414155daf81a8f426720b Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 9 Nov 2024 20:30:27 +0100 Subject: [PATCH 113/214] TASK: Simplify code and remove reopen cs logic into `publishWorkspace` --- .../WorkspacePublicationDuringWritingTest.php | 21 +++++ .../Feature/WorkspaceCommandHandler.php | 82 +++++++++---------- composer.json | 3 +- 3 files changed, 63 insertions(+), 43 deletions(-) diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Parallel/WorkspacePublicationDuringWriting/WorkspacePublicationDuringWritingTest.php b/Neos.ContentRepository.BehavioralTests/Tests/Parallel/WorkspacePublicationDuringWriting/WorkspacePublicationDuringWritingTest.php index e6bfcdf7f46..c664404cee6 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Parallel/WorkspacePublicationDuringWriting/WorkspacePublicationDuringWritingTest.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Parallel/WorkspacePublicationDuringWriting/WorkspacePublicationDuringWritingTest.php @@ -195,6 +195,27 @@ public function thenConcurrentPublishLeadsToException(): void $this->log('publish started'); + + /* + // NOTE, can also be tested with PartialPublish, or PartialPublish leading to a full publish, but this test only allows one at time :) + + $nodesForAFullPublish = 5000; + $nodesForAPartialPublish = $nodesForAFullPublish - 1; + + $nodeIdToPublish = []; + for ($i = 0; $i <= $nodesForAPartialPublish; $i++) { + $nodeIdToPublish[] = new NodeIdToPublishOrDiscard( + NodeAggregateId::fromString('nody-mc-nodeface-' . $i), // see nodes created above + DimensionSpacePoint::createWithoutDimensions() + ); + } + + $this->contentRepository->handle(PublishIndividualNodesFromWorkspace::create( + WorkspaceName::fromString('user-test'), + NodeIdsToPublishOrDiscard::create(...$nodeIdToPublish) + )); + */ + $actualException = null; try { $this->contentRepository->handle(PublishWorkspace::create( diff --git a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php index 5ceb1c02076..aafa7ec0741 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php +++ b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php @@ -210,23 +210,19 @@ private function handlePublishWorkspace( ) ); - try { - yield from $this->publishWorkspace( - $workspace, - $baseWorkspace, - $baseWorkspaceContentStreamVersion, - $command->newContentStreamId, - $rebaseableCommands - ); - } catch (WorkspaceRebaseFailed|ConcurrencyException $publishFailed) { - // todo catch all? Dont catch ANY ConcurrencyException because say if forking failed we dont need to reopen? - yield $this->reopenContentStreamWithoutConstraints( - $workspace->currentContentStreamId - ); - throw $publishFailed; - } + yield from $this->publishWorkspace( + $workspace, + $baseWorkspace, + $baseWorkspaceContentStreamVersion, + $command->newContentStreamId, + $rebaseableCommands + ); } + /** + * Note that the workspaces content stream must be closed beforehand. + * It will be reopened here in case of error. + */ private function publishWorkspace( Workspace $workspace, Workspace $baseWorkspace, @@ -245,20 +241,29 @@ static function ($handle) use ($rebaseableCommands): void { ); if ($commandSimulator->hasCommandsThatFailed()) { + yield $this->reopenContentStreamWithoutConstraints( + $workspace->currentContentStreamId + ); throw WorkspaceRebaseFailed::duringPublish($commandSimulator->getCommandsThatFailed()); } - // todo throw base workspace was modified in the meantime to distinguish exception above to reopen? - $commitResult = yield new EventsToPublish( - ContentStreamEventStreamName::fromContentStreamId($baseWorkspace->currentContentStreamId) - ->getEventStreamName(), - $this->getCopiedEventsOfEventStream( - $baseWorkspace->workspaceName, - $baseWorkspace->currentContentStreamId, - $commandSimulator->eventStream(), - ), - ExpectedVersion::fromVersion($baseWorkspaceContentStreamVersion) - ); + try { + $commitResult = yield new EventsToPublish( + ContentStreamEventStreamName::fromContentStreamId($baseWorkspace->currentContentStreamId) + ->getEventStreamName(), + $this->getCopiedEventsOfEventStream( + $baseWorkspace->workspaceName, + $baseWorkspace->currentContentStreamId, + $commandSimulator->eventStream(), + ), + ExpectedVersion::fromVersion($baseWorkspaceContentStreamVersion) + ); + } catch (ConcurrencyException $concurrencyException) { + yield $this->reopenContentStreamWithoutConstraints( + $workspace->currentContentStreamId + ); + throw $concurrencyException; + } yield $this->forkContentStream( $newContentStreamId, @@ -474,22 +479,15 @@ private function handlePublishIndividualNodesFromWorkspace( } if ($remainingCommands->isEmpty()) { - try { - // do a full publish, this is simpler for the projections to handle - yield from $this->publishWorkspace( - $workspace, - $baseWorkspace, - $baseWorkspaceContentStreamVersion, - $command->contentStreamIdForRemainingPart, - $matchingCommands - ); - return; - } catch (WorkspaceRebaseFailed $workspaceRebaseFailed) { - yield $this->reopenContentStreamWithoutConstraints( - $workspace->currentContentStreamId - ); - throw $workspaceRebaseFailed; - } + // do a full publish, this is simpler for the projections to handle + yield from $this->publishWorkspace( + $workspace, + $baseWorkspace, + $baseWorkspaceContentStreamVersion, + $command->contentStreamIdForRemainingPart, + $matchingCommands + ); + return; } $commandSimulator = $this->commandSimulatorFactory->createSimulatorForWorkspace($baseWorkspace->workspaceName); diff --git a/composer.json b/composer.json index 9e2e8bca36a..8dddf5aeab1 100644 --- a/composer.json +++ b/composer.json @@ -108,8 +108,9 @@ "../../bin/phpunit --colors --stop-on-failure -c ../../Build/BuildEssentials/PhpUnit/UnitTests.xml Neos.ContentRepository.Core/Tests/Unit", "../../bin/phpunit --colors --stop-on-failure -c ../../Build/BuildEssentials/PhpUnit/UnitTests.xml Neos.ContentRepositoryRegistry/Tests/Unit" ], + "test:paratest-cli": "../../bin/paratest --debug -v --functional --processes 2 --colors --stop-on-failure -c ../../Build/BuildEssentials/PhpUnit/FunctionalTests.xml", "test:parallel": [ - "for f in Neos.ContentRepository.BehavioralTests/Tests/Parallel/**/*Test.php; do ../../bin/paratest --debug -v --functional --processes 2 --colors --stop-on-failure -c ../../Build/BuildEssentials/PhpUnit/FunctionalTests.xml $f; done" + "for f in Neos.ContentRepository.BehavioralTests/Tests/Parallel/**/*Test.php; do composer test:paratest-cli $f; done" ], "test:behat-cli": "../../bin/behat -f progress --strict --no-interaction", "test:behavioral": [ From cb34618114c9963350b85fa736c90a0b946cecad Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 9 Nov 2024 20:36:05 +0100 Subject: [PATCH 114/214] TASK: Only fetch content stream once for constraint checks --- .../CommandHandlingDependencies.php | 9 +++++- .../Feature/Common/ConstraintChecks.php | 9 ++---- .../Feature/WorkspaceCommandHandler.php | 32 ++++++++----------- 3 files changed, 23 insertions(+), 27 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHandlingDependencies.php b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHandlingDependencies.php index 9e726cacea1..629f5c01c1e 100644 --- a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHandlingDependencies.php +++ b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHandlingDependencies.php @@ -16,6 +16,7 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphReadModelInterface; +use Neos\ContentRepository\Core\SharedModel\Exception\ContentStreamDoesNotExistYet; use Neos\ContentRepository\Core\SharedModel\Exception\WorkspaceDoesNotExist; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\SharedModel\Workspace\Workspace; @@ -48,11 +49,17 @@ public function contentStreamExists(ContentStreamId $contentStreamId): bool return $this->contentGraphReadModel->findContentStreamById($contentStreamId) !== null; } + /** + * @throws ContentStreamDoesNotExistYet if there is no matching content stream + */ public function isContentStreamClosed(ContentStreamId $contentStreamId): bool { $contentStream = $this->contentGraphReadModel->findContentStreamById($contentStreamId); if ($contentStream === null) { - throw new \InvalidArgumentException(sprintf('Failed to find content stream with id "%s"', $contentStreamId->value), 1729863973); + throw new ContentStreamDoesNotExistYet( + 'Content stream "' . $contentStreamId->value . '" does not exist.', + 1521386692 + ); } return $contentStream->isClosed; } diff --git a/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php b/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php index 2be9688f065..5364907d978 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php +++ b/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php @@ -81,14 +81,9 @@ protected function requireContentStream( CommandHandlingDependencies $commandHandlingDependencies ): ContentStreamId { $contentStreamId = $commandHandlingDependencies->getContentGraph($workspaceName)->getContentStreamId(); - if (!$commandHandlingDependencies->contentStreamExists($contentStreamId)) { - throw new ContentStreamDoesNotExistYet( - 'Content stream for "' . $workspaceName->value . '" does not exist yet.', - 1521386692 - ); - } + $closedState = $commandHandlingDependencies->isContentStreamClosed($contentStreamId); - if ($commandHandlingDependencies->isContentStreamClosed($contentStreamId)) { + if ($closedState) { throw new ContentStreamIsClosed( 'Content stream "' . $contentStreamId->value . '" is closed.', 1710260081 diff --git a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php index aafa7ec0741..d1675235f85 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php +++ b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php @@ -195,8 +195,8 @@ private function handlePublishWorkspace( // no-op return; } - $workspaceContentStreamVersion = $this->requireOpenContentStreamVersion($workspace, $commandHandlingDependencies); - $baseWorkspaceContentStreamVersion = $this->requireOpenContentStreamVersion($baseWorkspace, $commandHandlingDependencies); + $workspaceContentStreamVersion = $this->requireOpenContentStreamAndVersion($workspace, $commandHandlingDependencies); + $baseWorkspaceContentStreamVersion = $this->requireOpenContentStreamAndVersion($baseWorkspace, $commandHandlingDependencies); yield $this->closeContentStream( $workspace->currentContentStreamId, @@ -348,11 +348,9 @@ private function handleRebaseWorkspace( ): \Generator { $workspace = $this->requireWorkspace($command->workspaceName, $commandHandlingDependencies); $baseWorkspace = $this->requireBaseWorkspace($workspace, $commandHandlingDependencies); - if (!$commandHandlingDependencies->contentStreamExists($workspace->currentContentStreamId)) { - throw new \RuntimeException('Cannot rebase a workspace with a stateless content stream', 1711718314); - } - $workspaceContentStreamVersion = $this->requireOpenContentStreamVersion($workspace, $commandHandlingDependencies); - $baseWorkspaceContentStreamVersion = $this->requireOpenContentStreamVersion($baseWorkspace, $commandHandlingDependencies); + + $workspaceContentStreamVersion = $this->requireOpenContentStreamAndVersion($workspace, $commandHandlingDependencies); + $baseWorkspaceContentStreamVersion = $this->requireOpenContentStreamAndVersion($baseWorkspace, $commandHandlingDependencies); if ( $workspace->status === WorkspaceStatus::UP_TO_DATE @@ -449,12 +447,8 @@ private function handlePublishIndividualNodesFromWorkspace( return; } - // todo check that fetching workspace throws if there is no content stream id for it - if (!$commandHandlingDependencies->contentStreamExists($workspace->currentContentStreamId)) { - throw new \RuntimeException('Cannot publish nodes on a workspace with a stateless content stream', 1710410114); - } - $workspaceContentStreamVersion = $this->requireOpenContentStreamVersion($workspace, $commandHandlingDependencies); - $baseWorkspaceContentStreamVersion = $this->requireOpenContentStreamVersion($baseWorkspace, $commandHandlingDependencies); + $workspaceContentStreamVersion = $this->requireOpenContentStreamAndVersion($workspace, $commandHandlingDependencies); + $baseWorkspaceContentStreamVersion = $this->requireOpenContentStreamAndVersion($baseWorkspace, $commandHandlingDependencies); yield $this->closeContentStream( $workspace->currentContentStreamId, @@ -582,8 +576,8 @@ private function handleDiscardIndividualNodesFromWorkspace( return; } - $workspaceContentStreamVersion = $this->requireOpenContentStreamVersion($workspace, $commandHandlingDependencies); - $baseWorkspaceContentStreamVersion = $this->requireOpenContentStreamVersion($baseWorkspace, $commandHandlingDependencies); + $workspaceContentStreamVersion = $this->requireOpenContentStreamAndVersion($workspace, $commandHandlingDependencies); + $baseWorkspaceContentStreamVersion = $this->requireOpenContentStreamAndVersion($baseWorkspace, $commandHandlingDependencies); yield $this->closeContentStream( $workspace->currentContentStreamId, @@ -678,8 +672,8 @@ private function handleDiscardWorkspace( return; } - $workspaceContentStreamVersion = $this->requireOpenContentStreamVersion($workspace, $commandHandlingDependencies); - $baseWorkspaceContentStreamVersion = $this->requireOpenContentStreamVersion($baseWorkspace, $commandHandlingDependencies); + $workspaceContentStreamVersion = $this->requireOpenContentStreamAndVersion($workspace, $commandHandlingDependencies); + $baseWorkspaceContentStreamVersion = $this->requireOpenContentStreamAndVersion($baseWorkspace, $commandHandlingDependencies); yield from $this->discardWorkspace( $workspace, @@ -747,7 +741,7 @@ private function handleChangeBaseWorkspace( $newBaseWorkspace = $this->requireWorkspace($command->baseWorkspaceName, $commandHandlingDependencies); $this->requireNonCircularRelationBetweenWorkspaces($workspace, $newBaseWorkspace, $commandHandlingDependencies); - $newBaseWorkspaceContentStreamVersion = $this->requireOpenContentStreamVersion($newBaseWorkspace, $commandHandlingDependencies); + $newBaseWorkspaceContentStreamVersion = $this->requireOpenContentStreamAndVersion($newBaseWorkspace, $commandHandlingDependencies); yield $this->forkContentStream( $command->newContentStreamId, @@ -844,7 +838,7 @@ private function requireWorkspaceToNotExist(WorkspaceName $workspaceName, Comman ), 1715341085); } - private function requireOpenContentStreamVersion(Workspace $workspace, CommandHandlingDependencies $commandHandlingDependencies): Version + private function requireOpenContentStreamAndVersion(Workspace $workspace, CommandHandlingDependencies $commandHandlingDependencies): Version { if ($commandHandlingDependencies->isContentStreamClosed($workspace->currentContentStreamId)) { throw new ContentStreamIsClosed( From 0273e325ec1ca408234c6aa919d55d66fc6f000d Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 9 Nov 2024 20:36:47 +0100 Subject: [PATCH 115/214] TASK: Adjust .composer json --- .composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.composer.json b/.composer.json index 1bf4b28f90c..e7f37b99667 100644 --- a/.composer.json +++ b/.composer.json @@ -25,8 +25,9 @@ "../../bin/phpunit --colors --stop-on-failure -c ../../Build/BuildEssentials/PhpUnit/UnitTests.xml Neos.ContentRepository.Core/Tests/Unit", "../../bin/phpunit --colors --stop-on-failure -c ../../Build/BuildEssentials/PhpUnit/UnitTests.xml Neos.ContentRepositoryRegistry/Tests/Unit" ], + "test:paratest-cli": "../../bin/paratest --debug -v --functional --processes 2 --colors --stop-on-failure -c ../../Build/BuildEssentials/PhpUnit/FunctionalTests.xml", "test:parallel": [ - "FLOW_CONTEXT=Testing/Behat ../../bin/paratest --debug -v --functional --group parallel --processes 2 --colors --stop-on-failure -c ../../Build/BuildEssentials/PhpUnit/FunctionalTests.xml Neos.ContentRepository.BehavioralTests/Tests/Functional/Feature/WorkspacePublication/WorkspaceWritingDuringPublication.php" + "for f in Neos.ContentRepository.BehavioralTests/Tests/Parallel/**/*Test.php; do composer test:paratest-cli $f; done" ], "test:behat-cli": "../../bin/behat -f progress --strict --no-interaction", "test:behavioral": [ From 77778f9e2cc6c2aa0ee3560a491089465bf33f04 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 9 Nov 2024 20:56:53 +0100 Subject: [PATCH 116/214] TASK: Do not send `$commitResult` to generator but calculate expected version instead Also readd lost documentation and simplifies the `handle` The ->throw logic was initially introduced via https://github.com/neos/neos-development-collection/pull/5315 but then removed again as we thought it was no longer needed. --- .../CommandHandlerInterface.php | 3 +- .../Classes/ContentRepository.php | 15 +++++--- .../Feature/WorkspaceCommandHandler.php | 35 ++++++++++--------- 3 files changed, 30 insertions(+), 23 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHandlerInterface.php b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHandlerInterface.php index 1ff7cee24eb..6e0436be8fe 100644 --- a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHandlerInterface.php +++ b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHandlerInterface.php @@ -5,14 +5,13 @@ namespace Neos\ContentRepository\Core\CommandHandler; use Neos\ContentRepository\Core\EventStore\EventsToPublish; -use Neos\EventStore\Model\EventStore\CommitResult; /** * Common interface for all Content Repository command handlers * * The {@see CommandHandlingDependencies} are available during handling to do soft-constraint checks * - * @phpstan-type YieldedEventsToPublish \Generator + * @phpstan-type YieldedEventsToPublish \Generator * @internal no public API, because commands are no extension points of the CR */ interface CommandHandlerInterface diff --git a/Neos.ContentRepository.Core/Classes/ContentRepository.php b/Neos.ContentRepository.Core/Classes/ContentRepository.php index c3277a1d4f3..45b23d1da88 100644 --- a/Neos.ContentRepository.Core/Classes/ContentRepository.php +++ b/Neos.ContentRepository.Core/Classes/ContentRepository.php @@ -110,23 +110,28 @@ public function handle(CommandInterface $command): void // control-flow aware command handling via generator try { - $yieldedEventsToPublish = $toPublish->current(); - while ($yieldedEventsToPublish !== null) { + foreach ($toPublish as $yieldedEventsToPublish) { if ($yieldedEventsToPublish->events->isEmpty()) { - $yieldedEventsToPublish = $toPublish->send(null); continue; } $eventsToPublish = $this->enrichEventsToPublishWithMetadata($yieldedEventsToPublish); try { - $commitResult = $this->eventPersister->publishWithoutCatchup($eventsToPublish); + $this->eventPersister->publishWithoutCatchup($eventsToPublish); } catch (ConcurrencyException $concurrencyException) { + // we pass the exception into the generator (->throw), so it could be try-caught and reacted upon: + // + // try { + // yield EventsToPublish(...); + // } catch (ConcurrencyException $e) { + // yield $this->reopenContentStream(); + // throw $e; + // } $yieldedErrorStrategy = $toPublish->throw($concurrencyException); if ($yieldedErrorStrategy instanceof EventsToPublish) { $this->eventPersister->publishWithoutCatchup($yieldedErrorStrategy); } throw $concurrencyException; } - $yieldedEventsToPublish = $toPublish->send($commitResult); } } finally { // We always NEED to catchup even if there was an unexpected ConcurrencyException to make sure previous commits are handled. diff --git a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php index d1675235f85..2e9d3031ea1 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php +++ b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php @@ -247,15 +247,17 @@ static function ($handle) use ($rebaseableCommands): void { throw WorkspaceRebaseFailed::duringPublish($commandSimulator->getCommandsThatFailed()); } + $eventsOfWorkspaceToPublish = $this->getCopiedEventsOfEventStream( + $baseWorkspace->workspaceName, + $baseWorkspace->currentContentStreamId, + $commandSimulator->eventStream(), + ); + try { - $commitResult = yield new EventsToPublish( + yield new EventsToPublish( ContentStreamEventStreamName::fromContentStreamId($baseWorkspace->currentContentStreamId) ->getEventStreamName(), - $this->getCopiedEventsOfEventStream( - $baseWorkspace->workspaceName, - $baseWorkspace->currentContentStreamId, - $commandSimulator->eventStream(), - ), + $eventsOfWorkspaceToPublish, ExpectedVersion::fromVersion($baseWorkspaceContentStreamVersion) ); } catch (ConcurrencyException $concurrencyException) { @@ -268,7 +270,7 @@ static function ($handle) use ($rebaseableCommands): void { yield $this->forkContentStream( $newContentStreamId, $baseWorkspace->currentContentStreamId, - $commitResult->highestCommittedVersion + Version::fromInteger($baseWorkspaceContentStreamVersion->value + $eventsOfWorkspaceToPublish->count()) ); yield new EventsToPublish( @@ -507,16 +509,18 @@ static function ($handle) use ($commandSimulator, $matchingCommands, $remainingC throw WorkspaceRebaseFailed::duringPublish($commandSimulator->getCommandsThatFailed()); } + // this could empty and a no-op for the rare case when a command returns empty events e.g. the node was already tagged with this subtree tag + $selectedEventsOfWorkspaceToPublish = $this->getCopiedEventsOfEventStream( + $baseWorkspace->workspaceName, + $baseWorkspace->currentContentStreamId, + $commandSimulator->eventStream()->withMaximumSequenceNumber($highestSequenceNumberForMatching), + ); + try { - // this could be a no-op for the rare case when a command returns empty events e.g. the node was already tagged with this subtree tag, meaning we actually just rebase - $commitResult = yield new EventsToPublish( + yield new EventsToPublish( ContentStreamEventStreamName::fromContentStreamId($baseWorkspace->currentContentStreamId) ->getEventStreamName(), - $this->getCopiedEventsOfEventStream( - $baseWorkspace->workspaceName, - $baseWorkspace->currentContentStreamId, - $commandSimulator->eventStream()->withMaximumSequenceNumber($highestSequenceNumberForMatching), - ), + $selectedEventsOfWorkspaceToPublish, ExpectedVersion::fromVersion($baseWorkspaceContentStreamVersion) ); } catch (ConcurrencyException $concurrencyException) { @@ -529,8 +533,7 @@ static function ($handle) use ($commandSimulator, $matchingCommands, $remainingC yield from $this->forkNewContentStreamAndApplyEvents( $command->contentStreamIdForRemainingPart, $baseWorkspace->currentContentStreamId, - // todo otherwise Features/W8-IndividualNodePublication/03-MoreBasicFeatures.feature:185 fails, see comment about emptiness above ... or should we manually count? - $commitResult?->highestCommittedVersion ?: $baseWorkspaceContentStreamVersion, + Version::fromInteger($baseWorkspaceContentStreamVersion->value + $selectedEventsOfWorkspaceToPublish->count()), new EventsToPublish( WorkspaceEventStreamName::fromWorkspaceName($command->workspaceName)->getEventStreamName(), Events::fromArray([ From 7b922bff95cd28c63cc6c9accc5e0beecc5ef387 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 9 Nov 2024 20:59:13 +0100 Subject: [PATCH 117/214] TASK: Inline now simplified `YieldedEventsToPublish` virtual type again --- .../Classes/CommandHandler/CommandBus.php | 3 +-- .../Classes/CommandHandler/CommandHandlerInterface.php | 3 +-- .../Classes/Feature/WorkspaceCommandHandler.php | 5 ++--- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandBus.php b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandBus.php index 1a69d3d017c..4cdc38d35af 100644 --- a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandBus.php +++ b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandBus.php @@ -11,7 +11,6 @@ * Implementation Detail of {@see ContentRepository::handle}, which does the command dispatching to the different * {@see CommandHandlerInterface} implementation. * - * @phpstan-import-type YieldedEventsToPublish from CommandHandlerInterface * @internal */ final readonly class CommandBus @@ -33,7 +32,7 @@ public function __construct( * The handler only calculate which events they want to have published, * but do not do the publishing themselves * - * @return EventsToPublish|YieldedEventsToPublish + * @return EventsToPublish|\Generator */ public function handle(CommandInterface $command): EventsToPublish|\Generator { diff --git a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHandlerInterface.php b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHandlerInterface.php index 6e0436be8fe..b36d5d3ab75 100644 --- a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHandlerInterface.php +++ b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHandlerInterface.php @@ -11,7 +11,6 @@ * * The {@see CommandHandlingDependencies} are available during handling to do soft-constraint checks * - * @phpstan-type YieldedEventsToPublish \Generator * @internal no public API, because commands are no extension points of the CR */ interface CommandHandlerInterface @@ -24,7 +23,7 @@ public function canHandle(CommandInterface $command): bool; * For the case of the workspace command handler that need to publish to many streams and "close" the content-stream directly, * it's allowed to yield the events to interact with the control flow of event publishing. * - * @return EventsToPublish|YieldedEventsToPublish + * @return EventsToPublish|\Generator */ public function handle(CommandInterface $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish|\Generator; } diff --git a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php index 2e9d3031ea1..69f64493cc7 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php +++ b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php @@ -70,7 +70,6 @@ use Neos\EventStore\Model\EventStream\ExpectedVersion; /** - * @phpstan-import-type YieldedEventsToPublish from CommandHandlerInterface * @internal from userland, you'll use ContentRepository::handle to dispatch commands */ final readonly class WorkspaceCommandHandler implements CommandHandlerInterface @@ -90,7 +89,7 @@ public function canHandle(CommandInterface $command): bool } /** - * @return YieldedEventsToPublish + * @return \Generator */ public function handle(CommandInterface $command, CommandHandlingDependencies $commandHandlingDependencies): \Generator { @@ -436,7 +435,7 @@ static function ($handle) use ($rebaseableCommands): void { /** * This method is like a combined Rebase and Publish! * - * @return YieldedEventsToPublish + * @return \Generator */ private function handlePublishIndividualNodesFromWorkspace( PublishIndividualNodesFromWorkspace $command, From d27f83f024c703c4ef3b55f315fea73fb78b5dd9 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 9 Nov 2024 21:06:18 +0100 Subject: [PATCH 118/214] TASK: Wrap rebaseable command extraction into `finally` block to ensure content stream is never left closed During the beta phase it can happen that user forget to apply a migration to migrate the stored commands in the even metadata, upon publish this would close the content stream and fail directly afterward. Applying the migration then would not be enough as the content stream is a closed state and has to be repaired manually. Event thought this is not super likely, its not unlikely as well and the case during publication were we rely on things that might not be that way. As an alternative we could discuss doing the closing after acquiring the rebaseable commands. --- .../Feature/WorkspaceCommandHandler.php | 77 ++++++++++++------- 1 file changed, 51 insertions(+), 26 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php index 69f64493cc7..e1e0ec0979b 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php +++ b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php @@ -202,12 +202,18 @@ private function handlePublishWorkspace( $workspaceContentStreamVersion ); - $rebaseableCommands = RebaseableCommands::extractFromEventStream( - $this->eventStore->load( - ContentStreamEventStreamName::fromContentStreamId($workspace->currentContentStreamId) - ->getEventStreamName() - ) - ); + try { + $rebaseableCommands = RebaseableCommands::extractFromEventStream( + $this->eventStore->load( + ContentStreamEventStreamName::fromContentStreamId($workspace->currentContentStreamId) + ->getEventStreamName() + ) + ); + } finally { + yield $this->reopenContentStreamWithoutConstraints( + $workspace->currentContentStreamId + ); + } yield from $this->publishWorkspace( $workspace, @@ -377,12 +383,18 @@ private function handleRebaseWorkspace( return; } - $rebaseableCommands = RebaseableCommands::extractFromEventStream( - $this->eventStore->load( - ContentStreamEventStreamName::fromContentStreamId($workspace->currentContentStreamId) - ->getEventStreamName() - ) - ); + try { + $rebaseableCommands = RebaseableCommands::extractFromEventStream( + $this->eventStore->load( + ContentStreamEventStreamName::fromContentStreamId($workspace->currentContentStreamId) + ->getEventStreamName() + ) + ); + } finally { + yield $this->reopenContentStreamWithoutConstraints( + $workspace->currentContentStreamId + ); + } $commandSimulator = $this->commandSimulatorFactory->createSimulatorForWorkspace($baseWorkspace->workspaceName); @@ -456,12 +468,18 @@ private function handlePublishIndividualNodesFromWorkspace( $workspaceContentStreamVersion ); - $rebaseableCommands = RebaseableCommands::extractFromEventStream( - $this->eventStore->load( - ContentStreamEventStreamName::fromContentStreamId($workspace->currentContentStreamId) - ->getEventStreamName() - ) - ); + try { + $rebaseableCommands = RebaseableCommands::extractFromEventStream( + $this->eventStore->load( + ContentStreamEventStreamName::fromContentStreamId($workspace->currentContentStreamId) + ->getEventStreamName() + ) + ); + } finally { + yield $this->reopenContentStreamWithoutConstraints( + $workspace->currentContentStreamId + ); + } [$matchingCommands, $remainingCommands] = $rebaseableCommands->separateMatchingAndRemainingCommands($command->nodesToPublish); @@ -586,14 +604,21 @@ private function handleDiscardIndividualNodesFromWorkspace( $workspaceContentStreamVersion ); - // filter commands, only keeping the ones NOT MATCHING the nodes from the command (i.e. the modifications we want to keep) - $rebaseableCommands = RebaseableCommands::extractFromEventStream( - $this->eventStore->load( - ContentStreamEventStreamName::fromContentStreamId($workspace->currentContentStreamId) - ->getEventStreamName() - ) - ); - [$commandsToDiscard, $commandsToKeep] = $rebaseableCommands->separateMatchingAndRemainingCommands($command->nodesToDiscard); + try { + $rebaseableCommands = RebaseableCommands::extractFromEventStream( + $this->eventStore->load( + ContentStreamEventStreamName::fromContentStreamId($workspace->currentContentStreamId) + ->getEventStreamName() + ) + ); + + // filter commands, only keeping the ones NOT MATCHING the nodes from the command (i.e. the modifications we want to keep) + [$commandsToDiscard, $commandsToKeep] = $rebaseableCommands->separateMatchingAndRemainingCommands($command->nodesToDiscard); + } finally { + yield $this->reopenContentStreamWithoutConstraints( + $workspace->currentContentStreamId + ); + } if ($commandsToDiscard->isEmpty()) { // if we have nothing to discard, we can just keep all. (e.g. random node ids were specified) It's almost a noop ;) From de7895e6af96631f4008af6ac3d806c71e73808b Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 9 Nov 2024 21:18:13 +0100 Subject: [PATCH 119/214] TASK: Close content stream a bit later instead of having to reopen in many edge cases Alternative fix for d27f83f024c703c4ef3b55f315fea73fb78b5dd9 Previously an error in `extractFromEventStream` because the payload was not correct and yet has to be migrated would lead to a closed content stream which is of course persisted even after fixing the events via migration. This is still save to do, as the `closeContentStream` will commit the close on the FIRSTly fetched expected version. Same guarantees, different error behaviour in rare cases. --- .../Feature/WorkspaceCommandHandler.php | 115 +++++++----------- 1 file changed, 45 insertions(+), 70 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php index e1e0ec0979b..a2205df717b 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php +++ b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php @@ -197,24 +197,18 @@ private function handlePublishWorkspace( $workspaceContentStreamVersion = $this->requireOpenContentStreamAndVersion($workspace, $commandHandlingDependencies); $baseWorkspaceContentStreamVersion = $this->requireOpenContentStreamAndVersion($baseWorkspace, $commandHandlingDependencies); + $rebaseableCommands = RebaseableCommands::extractFromEventStream( + $this->eventStore->load( + ContentStreamEventStreamName::fromContentStreamId($workspace->currentContentStreamId) + ->getEventStreamName() + ) + ); + yield $this->closeContentStream( $workspace->currentContentStreamId, $workspaceContentStreamVersion ); - try { - $rebaseableCommands = RebaseableCommands::extractFromEventStream( - $this->eventStore->load( - ContentStreamEventStreamName::fromContentStreamId($workspace->currentContentStreamId) - ->getEventStreamName() - ) - ); - } finally { - yield $this->reopenContentStreamWithoutConstraints( - $workspace->currentContentStreamId - ); - } - yield from $this->publishWorkspace( $workspace, $baseWorkspace, @@ -367,13 +361,13 @@ private function handleRebaseWorkspace( return; } - yield $this->closeContentStream( - $workspace->currentContentStreamId, - $workspaceContentStreamVersion - ); - if (!$workspace->hasPublishableChanges()) { // if we have no changes in the workspace we can fork from the base directly + yield $this->closeContentStream( + $workspace->currentContentStreamId, + $workspaceContentStreamVersion + ); + yield from $this->rebaseWorkspaceWithoutChanges( $workspace, $baseWorkspace, @@ -383,18 +377,17 @@ private function handleRebaseWorkspace( return; } - try { - $rebaseableCommands = RebaseableCommands::extractFromEventStream( - $this->eventStore->load( - ContentStreamEventStreamName::fromContentStreamId($workspace->currentContentStreamId) - ->getEventStreamName() - ) - ); - } finally { - yield $this->reopenContentStreamWithoutConstraints( - $workspace->currentContentStreamId - ); - } + $rebaseableCommands = RebaseableCommands::extractFromEventStream( + $this->eventStore->load( + ContentStreamEventStreamName::fromContentStreamId($workspace->currentContentStreamId) + ->getEventStreamName() + ) + ); + + yield $this->closeContentStream( + $workspace->currentContentStreamId, + $workspaceContentStreamVersion + ); $commandSimulator = $this->commandSimulatorFactory->createSimulatorForWorkspace($baseWorkspace->workspaceName); @@ -463,34 +456,25 @@ private function handlePublishIndividualNodesFromWorkspace( $workspaceContentStreamVersion = $this->requireOpenContentStreamAndVersion($workspace, $commandHandlingDependencies); $baseWorkspaceContentStreamVersion = $this->requireOpenContentStreamAndVersion($baseWorkspace, $commandHandlingDependencies); - yield $this->closeContentStream( - $workspace->currentContentStreamId, - $workspaceContentStreamVersion + $rebaseableCommands = RebaseableCommands::extractFromEventStream( + $this->eventStore->load( + ContentStreamEventStreamName::fromContentStreamId($workspace->currentContentStreamId) + ->getEventStreamName() + ) ); - try { - $rebaseableCommands = RebaseableCommands::extractFromEventStream( - $this->eventStore->load( - ContentStreamEventStreamName::fromContentStreamId($workspace->currentContentStreamId) - ->getEventStreamName() - ) - ); - } finally { - yield $this->reopenContentStreamWithoutConstraints( - $workspace->currentContentStreamId - ); - } - [$matchingCommands, $remainingCommands] = $rebaseableCommands->separateMatchingAndRemainingCommands($command->nodesToPublish); if ($matchingCommands->isEmpty()) { // almost a noop (e.g. random node ids were specified) ;) - yield $this->reopenContentStreamWithoutConstraints( - $workspace->currentContentStreamId - ); return; } + yield $this->closeContentStream( + $workspace->currentContentStreamId, + $workspaceContentStreamVersion + ); + if ($remainingCommands->isEmpty()) { // do a full publish, this is simpler for the projections to handle yield from $this->publishWorkspace( @@ -599,35 +583,26 @@ private function handleDiscardIndividualNodesFromWorkspace( $workspaceContentStreamVersion = $this->requireOpenContentStreamAndVersion($workspace, $commandHandlingDependencies); $baseWorkspaceContentStreamVersion = $this->requireOpenContentStreamAndVersion($baseWorkspace, $commandHandlingDependencies); - yield $this->closeContentStream( - $workspace->currentContentStreamId, - $workspaceContentStreamVersion + $rebaseableCommands = RebaseableCommands::extractFromEventStream( + $this->eventStore->load( + ContentStreamEventStreamName::fromContentStreamId($workspace->currentContentStreamId) + ->getEventStreamName() + ) ); - try { - $rebaseableCommands = RebaseableCommands::extractFromEventStream( - $this->eventStore->load( - ContentStreamEventStreamName::fromContentStreamId($workspace->currentContentStreamId) - ->getEventStreamName() - ) - ); - - // filter commands, only keeping the ones NOT MATCHING the nodes from the command (i.e. the modifications we want to keep) - [$commandsToDiscard, $commandsToKeep] = $rebaseableCommands->separateMatchingAndRemainingCommands($command->nodesToDiscard); - } finally { - yield $this->reopenContentStreamWithoutConstraints( - $workspace->currentContentStreamId - ); - } + // filter commands, only keeping the ones NOT MATCHING the nodes from the command (i.e. the modifications we want to keep) + [$commandsToDiscard, $commandsToKeep] = $rebaseableCommands->separateMatchingAndRemainingCommands($command->nodesToDiscard); if ($commandsToDiscard->isEmpty()) { // if we have nothing to discard, we can just keep all. (e.g. random node ids were specified) It's almost a noop ;) - yield $this->reopenContentStreamWithoutConstraints( - $workspace->currentContentStreamId - ); return; } + yield $this->closeContentStream( + $workspace->currentContentStreamId, + $workspaceContentStreamVersion + ); + if ($commandsToKeep->isEmpty()) { // quick path everything was discarded yield from $this->discardWorkspace( From d290047f899007bf9dccc0e16153fbe3d9905d66 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 9 Nov 2024 21:20:24 +0100 Subject: [PATCH 120/214] TASK: Add proper docs to `EventPersister` ;) --- Neos.ContentRepository.Core/Classes/ContentRepository.php | 6 +++--- .../Classes/EventStore/EventPersister.php | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/ContentRepository.php b/Neos.ContentRepository.Core/Classes/ContentRepository.php index 45b23d1da88..d40ebbb5060 100644 --- a/Neos.ContentRepository.Core/Classes/ContentRepository.php +++ b/Neos.ContentRepository.Core/Classes/ContentRepository.php @@ -103,7 +103,7 @@ public function handle(CommandInterface $command): void if ($eventsToPublish->events->isEmpty()) { return; } - $this->eventPersister->publishWithoutCatchup($eventsToPublish); + $this->eventPersister->publishWithoutKetchup($eventsToPublish); $this->catchupProjections(); return; } @@ -116,7 +116,7 @@ public function handle(CommandInterface $command): void } $eventsToPublish = $this->enrichEventsToPublishWithMetadata($yieldedEventsToPublish); try { - $this->eventPersister->publishWithoutCatchup($eventsToPublish); + $this->eventPersister->publishWithoutKetchup($eventsToPublish); } catch (ConcurrencyException $concurrencyException) { // we pass the exception into the generator (->throw), so it could be try-caught and reacted upon: // @@ -128,7 +128,7 @@ public function handle(CommandInterface $command): void // } $yieldedErrorStrategy = $toPublish->throw($concurrencyException); if ($yieldedErrorStrategy instanceof EventsToPublish) { - $this->eventPersister->publishWithoutCatchup($yieldedErrorStrategy); + $this->eventPersister->publishWithoutKetchup($yieldedErrorStrategy); } throw $concurrencyException; } diff --git a/Neos.ContentRepository.Core/Classes/EventStore/EventPersister.php b/Neos.ContentRepository.Core/Classes/EventStore/EventPersister.php index 59b102e03bd..b741b041b9c 100644 --- a/Neos.ContentRepository.Core/Classes/EventStore/EventPersister.php +++ b/Neos.ContentRepository.Core/Classes/EventStore/EventPersister.php @@ -25,6 +25,7 @@ public function __construct( } /** + * TODO Will be refactored via https://github.com/neos/neos-development-collection/pull/5321 * @throws ConcurrencyException in case the expectedVersion does not match */ public function publishEvents(ContentRepository $contentRepository, EventsToPublish $eventsToPublish): void @@ -32,14 +33,15 @@ public function publishEvents(ContentRepository $contentRepository, EventsToPubl if ($eventsToPublish->events->isEmpty()) { return; } - $this->publishWithoutCatchup($eventsToPublish); + $this->publishWithoutKetchup($eventsToPublish); $contentRepository->catchUpProjections(); } /** + * TODO Will be refactored via https://github.com/neos/neos-development-collection/pull/5321 * @throws ConcurrencyException in case the expectedVersion does not match */ - public function publishWithoutCatchup(EventsToPublish $eventsToPublish): CommitResult + public function publishWithoutKetchup(EventsToPublish $eventsToPublish): CommitResult { $normalizedEvents = Events::fromArray( $eventsToPublish->events->map($this->eventNormalizer->normalize(...)) From 8e48e7eefdbb8910fb9352e58d08ac9dc9f07b61 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 9 Nov 2024 21:24:49 +0100 Subject: [PATCH 121/214] TASK: Adjust naming of `removeContentStreamWithoutConstraintChecks` ;) --- .../Classes/Feature/ContentStreamHandling.php | 4 ++-- .../Feature/WorkspaceCommandHandler.php | 24 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/Feature/ContentStreamHandling.php b/Neos.ContentRepository.Core/Classes/Feature/ContentStreamHandling.php index d71c28f4097..a228ca7c864 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/ContentStreamHandling.php +++ b/Neos.ContentRepository.Core/Classes/Feature/ContentStreamHandling.php @@ -45,7 +45,7 @@ private function closeContentStream( * @param ContentStreamId $contentStreamId The id of the content stream to reopen * @phpstan-pure this method is pure, to persist the events they must be handled outside */ - private function reopenContentStreamWithoutConstraints( + private function reopenContentStreamWithoutConstraintChecks( ContentStreamId $contentStreamId, ): EventsToPublish { return new EventsToPublish( @@ -91,7 +91,7 @@ private function forkContentStream( * @param ContentStreamId $contentStreamId The id of the content stream to remove * @phpstan-pure this method is pure, to persist the events they must be handled outside */ - private function removeContentStreamWithoutConstraints( + private function removeContentStreamWithoutConstraintChecks( ContentStreamId $contentStreamId, ): EventsToPublish { return new EventsToPublish( diff --git a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php index a2205df717b..020f6cbbbbe 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php +++ b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php @@ -240,7 +240,7 @@ static function ($handle) use ($rebaseableCommands): void { ); if ($commandSimulator->hasCommandsThatFailed()) { - yield $this->reopenContentStreamWithoutConstraints( + yield $this->reopenContentStreamWithoutConstraintChecks( $workspace->currentContentStreamId ); throw WorkspaceRebaseFailed::duringPublish($commandSimulator->getCommandsThatFailed()); @@ -260,7 +260,7 @@ static function ($handle) use ($rebaseableCommands): void { ExpectedVersion::fromVersion($baseWorkspaceContentStreamVersion) ); } catch (ConcurrencyException $concurrencyException) { - yield $this->reopenContentStreamWithoutConstraints( + yield $this->reopenContentStreamWithoutConstraintChecks( $workspace->currentContentStreamId ); throw $concurrencyException; @@ -285,7 +285,7 @@ static function ($handle) use ($rebaseableCommands): void { ExpectedVersion::ANY() ); - yield $this->removeContentStreamWithoutConstraints($workspace->currentContentStreamId); + yield $this->removeContentStreamWithoutConstraintChecks($workspace->currentContentStreamId); } private function rebaseWorkspaceWithoutChanges( @@ -312,7 +312,7 @@ private function rebaseWorkspaceWithoutChanges( ExpectedVersion::ANY() ); - yield $this->removeContentStreamWithoutConstraints($workspace->currentContentStreamId); + yield $this->removeContentStreamWithoutConstraintChecks($workspace->currentContentStreamId); } /** @@ -403,7 +403,7 @@ static function ($handle) use ($rebaseableCommands): void { $command->rebaseErrorHandlingStrategy === RebaseErrorHandlingStrategy::STRATEGY_FAIL && $commandSimulator->hasCommandsThatFailed() ) { - yield $this->reopenContentStreamWithoutConstraints( + yield $this->reopenContentStreamWithoutConstraintChecks( $workspace->currentContentStreamId ); @@ -434,7 +434,7 @@ static function ($handle) use ($rebaseableCommands): void { ) ); - yield $this->removeContentStreamWithoutConstraints($workspace->currentContentStreamId); + yield $this->removeContentStreamWithoutConstraintChecks($workspace->currentContentStreamId); } /** @@ -503,7 +503,7 @@ static function ($handle) use ($commandSimulator, $matchingCommands, $remainingC ); if ($commandSimulator->hasCommandsThatFailed()) { - yield $this->reopenContentStreamWithoutConstraints( + yield $this->reopenContentStreamWithoutConstraintChecks( $workspace->currentContentStreamId ); @@ -525,7 +525,7 @@ static function ($handle) use ($commandSimulator, $matchingCommands, $remainingC ExpectedVersion::fromVersion($baseWorkspaceContentStreamVersion) ); } catch (ConcurrencyException $concurrencyException) { - yield $this->reopenContentStreamWithoutConstraints( + yield $this->reopenContentStreamWithoutConstraintChecks( $workspace->currentContentStreamId ); throw $concurrencyException; @@ -555,7 +555,7 @@ static function ($handle) use ($commandSimulator, $matchingCommands, $remainingC ) ); - yield $this->removeContentStreamWithoutConstraints($workspace->currentContentStreamId); + yield $this->removeContentStreamWithoutConstraintChecks($workspace->currentContentStreamId); } /** @@ -626,7 +626,7 @@ static function ($handle) use ($commandsToKeep): void { ); if ($commandSimulator->hasCommandsThatFailed()) { - yield $this->reopenContentStreamWithoutConstraints( + yield $this->reopenContentStreamWithoutConstraintChecks( $workspace->currentContentStreamId ); throw WorkspaceRebaseFailed::duringDiscard($commandSimulator->getCommandsThatFailed()); @@ -655,7 +655,7 @@ static function ($handle) use ($commandsToKeep): void { ) ); - yield $this->removeContentStreamWithoutConstraints($workspace->currentContentStreamId); + yield $this->removeContentStreamWithoutConstraintChecks($workspace->currentContentStreamId); } /** @@ -714,7 +714,7 @@ private function discardWorkspace( ExpectedVersion::ANY() ); - yield $this->removeContentStreamWithoutConstraints($workspace->currentContentStreamId); + yield $this->removeContentStreamWithoutConstraintChecks($workspace->currentContentStreamId); } /** From 59fa2e345742822932ef179ea90b9a70d5bc649e Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 10 Nov 2024 09:11:01 +0100 Subject: [PATCH 122/214] TASK: Improve assertions of WorkspacePublicationDuringWritingTest --- .../Tests/Parallel/AbstractParallelTestCase.php | 7 ++++++- .../WorkspacePublicationDuringWritingTest.php | 13 ++----------- .../WorkspaceWritingDuringRebaseTest.php | 1 + 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Parallel/AbstractParallelTestCase.php b/Neos.ContentRepository.BehavioralTests/Tests/Parallel/AbstractParallelTestCase.php index ef97e1bea36..569609ee5ce 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Parallel/AbstractParallelTestCase.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Parallel/AbstractParallelTestCase.php @@ -83,6 +83,11 @@ final protected function setUpContentRepository( final protected function log(string $message): void { - file_put_contents(self::LOGGING_PATH, substr($this::class, strrpos($this::class, '\\') + 1) . ': ' . getmypid() . ': ' . $message . PHP_EOL, FILE_APPEND); + file_put_contents(self::LOGGING_PATH, self::shortClassName($this::class) . ': ' . getmypid() . ': ' . $message . PHP_EOL, FILE_APPEND); + } + + final protected static function shortClassName(string $className): string + { + return substr($className, strrpos($className, '\\') + 1); } } diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Parallel/WorkspacePublicationDuringWriting/WorkspacePublicationDuringWritingTest.php b/Neos.ContentRepository.BehavioralTests/Tests/Parallel/WorkspacePublicationDuringWriting/WorkspacePublicationDuringWritingTest.php index c664404cee6..d96a9adddf0 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Parallel/WorkspacePublicationDuringWriting/WorkspacePublicationDuringWritingTest.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Parallel/WorkspacePublicationDuringWriting/WorkspacePublicationDuringWritingTest.php @@ -223,6 +223,7 @@ public function thenConcurrentPublishLeadsToException(): void )); } catch (\Exception $thrownException) { $actualException = $thrownException; + $this->log(sprintf('Got exception %s: %s', self::shortClassName($actualException::class), $actualException->getMessage())); } $this->log('publish finished'); @@ -231,20 +232,10 @@ public function thenConcurrentPublishLeadsToException(): void Assert::fail(sprintf('No exception was thrown')); } - if ($actualException instanceof \RuntimeException && $actualException->getCode() === 1652279016) { - // todo can be removed soon - $this->log(sprintf('got expected RuntimeException exception: %s', $actualException->getMessage())); - } elseif ($actualException instanceof ConcurrencyException) { - $this->log(sprintf('got expected ConcurrencyException exception: %s', $actualException->getMessage())); - } else { - Assert::assertInstanceOf(ConcurrencyException::class, $actualException); - } + Assert::assertInstanceOf(ConcurrencyException::class, $actualException); $this->awaitFileRemoval(self::WRITING_IS_RUNNING_FLAG_PATH); - // just to make sure were up-to-date now! - $this->contentRepository->catchupProjections(); - // writing to user works!!! try { $this->contentRepository->handle( diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Parallel/WorkspaceWritingDuringRebase/WorkspaceWritingDuringRebaseTest.php b/Neos.ContentRepository.BehavioralTests/Tests/Parallel/WorkspaceWritingDuringRebase/WorkspaceWritingDuringRebaseTest.php index b1022e2b6e6..73dee196a8d 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Parallel/WorkspaceWritingDuringRebase/WorkspaceWritingDuringRebaseTest.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Parallel/WorkspaceWritingDuringRebase/WorkspaceWritingDuringRebaseTest.php @@ -203,6 +203,7 @@ public function thenConcurrentCommandsLeadToAnException(): void )); } catch (\Exception $thrownException) { $actualException = $thrownException; + $this->log(sprintf('Got exception %s: %s', self::shortClassName($actualException::class), $actualException->getMessage())); } $this->log('write finished'); From 48e09cb7a3de3a1ce71e2774f121bef6d5e16b8d Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 10 Nov 2024 09:16:56 +0100 Subject: [PATCH 123/214] TASK: Assert that in WorkspaceWritingDuringRebaseTest that the workspace is still the original content stream --- .../WorkspaceWritingDuringRebaseTest.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Parallel/WorkspaceWritingDuringRebase/WorkspaceWritingDuringRebaseTest.php b/Neos.ContentRepository.BehavioralTests/Tests/Parallel/WorkspaceWritingDuringRebase/WorkspaceWritingDuringRebaseTest.php index 73dee196a8d..802205f2e16 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Parallel/WorkspaceWritingDuringRebase/WorkspaceWritingDuringRebaseTest.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Parallel/WorkspaceWritingDuringRebase/WorkspaceWritingDuringRebaseTest.php @@ -160,7 +160,7 @@ public function whileAWorkspaceIsBeingRebased(): void try { $this->contentRepository->handle( RebaseWorkspace::create($workspaceName) - ->withRebasedContentStreamId(ContentStreamId::create()) + ->withRebasedContentStreamId(ContentStreamId::fromString('user-cs-rebased')) ->withErrorHandlingStrategy(RebaseErrorHandlingStrategy::STRATEGY_FORCE)); } finally { unlink(self::REBASE_IS_RUNNING_FLAG_PATH); @@ -190,6 +190,11 @@ public function thenConcurrentCommandsLeadToAnException(): void $this->log('write started'); + $workspaceDuringRebase = $this->contentRepository->getContentGraph(WorkspaceName::fromString('user-test')); + Assert::assertSame('user-cs-id', $workspaceDuringRebase->getContentStreamId()->value, + 'The parallel tests expects the workspace to still point to the original cs.' + ); + $origin = OriginDimensionSpacePoint::createWithoutDimensions(); $actualException = null; try { @@ -218,7 +223,7 @@ public function thenConcurrentCommandsLeadToAnException(): void Assert::assertThat($actualException, self::logicalOr( self::isInstanceOf(ContentStreamIsClosed::class), - self::isInstanceOf(ConcurrencyException::class), + self::isInstanceOf(ConcurrencyException::class), // todo is only thrown theoretical? but not during tests here ... )); Assert::assertSame('title-original', $node?->getProperty('title')); From e12c6414aa5a89bc5835f002ef69abab4b12d76e Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 10 Nov 2024 09:29:33 +0100 Subject: [PATCH 124/214] TASK: Naming things and suggestion from code review :) --- .../Classes/ContentRepository.php | 12 ++++++------ .../Classes/EventStore/EventPersister.php | 4 ++-- .../Classes/Feature/Common/ConstraintChecks.php | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/ContentRepository.php b/Neos.ContentRepository.Core/Classes/ContentRepository.php index d40ebbb5060..054639418f5 100644 --- a/Neos.ContentRepository.Core/Classes/ContentRepository.php +++ b/Neos.ContentRepository.Core/Classes/ContentRepository.php @@ -97,13 +97,13 @@ public function handle(CommandInterface $command): void { $toPublish = $this->commandBus->handle($command); + // simple case if ($toPublish instanceof EventsToPublish) { - // simple case - $eventsToPublish = $this->enrichEventsToPublishWithMetadata($toPublish); - if ($eventsToPublish->events->isEmpty()) { + if ($toPublish->events->isEmpty()) { return; } - $this->eventPersister->publishWithoutKetchup($eventsToPublish); + $eventsToPublish = $this->enrichEventsToPublishWithMetadata($toPublish); + $this->eventPersister->publishWithoutCatchup($eventsToPublish); $this->catchupProjections(); return; } @@ -116,7 +116,7 @@ public function handle(CommandInterface $command): void } $eventsToPublish = $this->enrichEventsToPublishWithMetadata($yieldedEventsToPublish); try { - $this->eventPersister->publishWithoutKetchup($eventsToPublish); + $this->eventPersister->publishWithoutCatchup($eventsToPublish); } catch (ConcurrencyException $concurrencyException) { // we pass the exception into the generator (->throw), so it could be try-caught and reacted upon: // @@ -128,7 +128,7 @@ public function handle(CommandInterface $command): void // } $yieldedErrorStrategy = $toPublish->throw($concurrencyException); if ($yieldedErrorStrategy instanceof EventsToPublish) { - $this->eventPersister->publishWithoutKetchup($yieldedErrorStrategy); + $this->eventPersister->publishWithoutCatchup($yieldedErrorStrategy); } throw $concurrencyException; } diff --git a/Neos.ContentRepository.Core/Classes/EventStore/EventPersister.php b/Neos.ContentRepository.Core/Classes/EventStore/EventPersister.php index b741b041b9c..885fefc1c7f 100644 --- a/Neos.ContentRepository.Core/Classes/EventStore/EventPersister.php +++ b/Neos.ContentRepository.Core/Classes/EventStore/EventPersister.php @@ -33,7 +33,7 @@ public function publishEvents(ContentRepository $contentRepository, EventsToPubl if ($eventsToPublish->events->isEmpty()) { return; } - $this->publishWithoutKetchup($eventsToPublish); + $this->publishWithoutCatchup($eventsToPublish); $contentRepository->catchUpProjections(); } @@ -41,7 +41,7 @@ public function publishEvents(ContentRepository $contentRepository, EventsToPubl * TODO Will be refactored via https://github.com/neos/neos-development-collection/pull/5321 * @throws ConcurrencyException in case the expectedVersion does not match */ - public function publishWithoutKetchup(EventsToPublish $eventsToPublish): CommitResult + public function publishWithoutCatchup(EventsToPublish $eventsToPublish): CommitResult { $normalizedEvents = Events::fromArray( $eventsToPublish->events->map($this->eventNormalizer->normalize(...)) diff --git a/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php b/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php index 5364907d978..6e4ec8a7391 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php +++ b/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php @@ -81,9 +81,9 @@ protected function requireContentStream( CommandHandlingDependencies $commandHandlingDependencies ): ContentStreamId { $contentStreamId = $commandHandlingDependencies->getContentGraph($workspaceName)->getContentStreamId(); - $closedState = $commandHandlingDependencies->isContentStreamClosed($contentStreamId); + $isContentStreamClosed = $commandHandlingDependencies->isContentStreamClosed($contentStreamId); - if ($closedState) { + if ($isContentStreamClosed) { throw new ContentStreamIsClosed( 'Content stream "' . $contentStreamId->value . '" is closed.', 1710260081 From c1d64ba3f4ccf01f60fd4f3f20f93a1642ba3ff2 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 10 Nov 2024 10:46:57 +0100 Subject: [PATCH 125/214] BUGFIX: Import `Duplicate entry 'onedimension.localhost' for key 'flow_identity_neos_neos_domain_model_domain` Instead, we will now gracefully handle this case: Domain "onedimension.localhost" already exists. Adding it to site "Neos Test Site". --- .../Domain/Import/SiteCreationProcessor.php | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php b/Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php index fb6d4222e87..3ea256fe587 100644 --- a/Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php +++ b/Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php @@ -14,7 +14,6 @@ namespace Neos\Neos\Domain\Import; -use JsonException; use Neos\ContentRepository\Core\SharedModel\Node\NodeName; use Neos\ContentRepository\Export\Event\ValueObject\ExportedEvent; use Neos\ContentRepository\Export\ProcessingContext; @@ -49,7 +48,7 @@ public function run(ProcessingContext $context): void $sitesJson = $context->files->read('sites.json'); try { $sites = json_decode($sitesJson, true, 512, JSON_THROW_ON_ERROR); - } catch (JsonException $e) { + } catch (\JsonException $e) { throw new \RuntimeException("Failed to decode sites.json: {$e->getMessage()}", 1729506117, $e); } } else { @@ -59,11 +58,11 @@ public function run(ProcessingContext $context): void /** @var SiteShape $site */ foreach ($sites as $site) { - $context->dispatch(Severity::NOTICE, "Creating site \"{$site['name']}\""); + $context->dispatch(Severity::NOTICE, sprintf('Creating site "%s"', $site['name'])); $siteNodeName = !empty($site['nodeName']) ? NodeName::fromString($site['nodeName']) : NodeName::transliterateFromString($site['name']); if ($this->siteRepository->findOneByNodeName($siteNodeName->value)) { - $context->dispatch(Severity::NOTICE, "Site for node name \"{$siteNodeName->value}\" already exists, skipping"); + $context->dispatch(Severity::NOTICE, sprintf('Site for node name "%s" already exists, skipping', $siteNodeName->value)); continue; } $siteInstance = new Site($siteNodeName->value); @@ -73,13 +72,18 @@ public function run(ProcessingContext $context): void $this->siteRepository->add($siteInstance); $this->persistenceManager->persistAll(); foreach ($site['domains'] ?? [] as $domain) { - $domainInstance = new Domain(); - $domainInstance->setSite($siteInstance); - $domainInstance->setHostname($domain['hostname']); - $domainInstance->setPort($domain['port'] ?? null); - $domainInstance->setScheme($domain['scheme'] ?? null); - $domainInstance->setActive($domain['active'] ?? false); - $this->domainRepository->add($domainInstance); + $domainInstance = $this->domainRepository->findOneByHost($domain['hostname']); + if ($domainInstance) { + $context->dispatch(Severity::NOTICE, sprintf('Domain "%s" already exists. Adding it to site "%s".', $domain['hostname'], $site['name'])); + } else { + $domainInstance = new Domain(); + $domainInstance->setSite($siteInstance); + $domainInstance->setHostname($domain['hostname']); + $domainInstance->setPort($domain['port'] ?? null); + $domainInstance->setScheme($domain['scheme'] ?? null); + $domainInstance->setActive($domain['active'] ?? false); + $this->domainRepository->add($domainInstance); + } if ($domain['primary'] ?? false) { $siteInstance->setPrimaryDomain($domainInstance); $this->siteRepository->update($siteInstance); From 49234ffb1bd3a5f3391120f8eb0be72c6d831952 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 10 Nov 2024 10:49:11 +0100 Subject: [PATCH 126/214] TASK: Migration, make sure that NodeType does not exist error is thrown first --- .../Classes/Processors/EventExportProcessor.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Neos.ContentRepository.LegacyNodeMigration/Classes/Processors/EventExportProcessor.php b/Neos.ContentRepository.LegacyNodeMigration/Classes/Processors/EventExportProcessor.php index 3020baf2b2a..6d547c385bc 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Classes/Processors/EventExportProcessor.php +++ b/Neos.ContentRepository.LegacyNodeMigration/Classes/Processors/EventExportProcessor.php @@ -215,15 +215,17 @@ public function processNodeDataWithoutFallbackToEmptyDimension(ProcessingContext $nodeType = $this->nodeTypeManager->getNodeType($nodeTypeName); $isSiteNode = $nodeDataRow['parentpath'] === '/sites'; - if ($isSiteNode && !$nodeType?->isOfType(NodeTypeNameFactory::NAME_SITE)) { - throw new MigrationException(sprintf('The site node "%s" (type: "%s") must be of type "%s"', $nodeDataRow['identifier'], $nodeTypeName->value, NodeTypeNameFactory::NAME_SITE), 1695801620); - } if (!$nodeType) { $context->dispatch(Severity::ERROR, "The node type \"{$nodeTypeName->value}\" is not available. Node: \"{$nodeDataRow['identifier']}\""); return; } + if ($isSiteNode && !$nodeType->isOfType(NodeTypeNameFactory::NAME_SITE)) { + $declaredSuperTypes = array_keys($nodeType->getDeclaredSuperTypes()); + throw new MigrationException(sprintf('The site node "%s" (type: "%s") must be of type "%s". Currently declared super types: "%s"', $nodeDataRow['identifier'], $nodeTypeName->value, NodeTypeNameFactory::NAME_SITE, join(',', $declaredSuperTypes)), 1695801620); + } + $serializedPropertyValuesAndReferences = $this->extractPropertyValuesAndReferences($context, $nodeDataRow, $nodeType); if ($this->isAutoCreatedChildNode($parentNodeAggregate->nodeTypeName, $nodeName) && !$this->visitedNodes->containsNodeAggregate($nodeAggregateId)) { From 5f3d350f0e9caf41ff5bdf96af0f3b49eee4cc56 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 10 Nov 2024 11:53:26 +0100 Subject: [PATCH 127/214] TASK: Enforce file creation first (to throw possibly errors) --- .../Classes/Command/SiteCommandController.php | 2 +- Neos.Neos/Classes/Command/SiteCommandController.php | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/SiteCommandController.php b/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/SiteCommandController.php index 8f57f0dc526..11b40881f03 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/SiteCommandController.php +++ b/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/SiteCommandController.php @@ -124,6 +124,7 @@ public function migrateLegacyDataCommand(string $contentRepository = 'default', */ public function exportLegacyDataCommand(string $path, string $contentRepository = 'default', string $config = null, bool $verbose = false): void { + Files::createDirectoryRecursively($path); if ($config !== null) { try { $parsedConfig = json_decode($config, true, 512, JSON_THROW_ON_ERROR); @@ -148,7 +149,6 @@ public function exportLegacyDataCommand(string $path, string $contentRepository } $this->verifyDatabaseConnection($connection); - Files::createDirectoryRecursively($path); $legacyExportService = $this->contentRepositoryRegistry->buildService( ContentRepositoryId::fromString($contentRepository), new LegacyExportServiceFactory( diff --git a/Neos.Neos/Classes/Command/SiteCommandController.php b/Neos.Neos/Classes/Command/SiteCommandController.php index 59601e38d6f..c281add9ce3 100644 --- a/Neos.Neos/Classes/Command/SiteCommandController.php +++ b/Neos.Neos/Classes/Command/SiteCommandController.php @@ -213,9 +213,7 @@ public function exportAllCommand(string $packageKey = null, string $path = null, { $path = $this->determineTargetPath($packageKey, $path); $contentRepositoryId = ContentRepositoryId::fromString($contentRepository); - if (file_exists($path) === false) { - Files::createDirectoryRecursively($path); - } + Files::createDirectoryRecursively($path); $this->siteExportService->exportToPath( $contentRepositoryId, $path, From b6c4cd48a350d8cff827cc0ca079cda77de0a0cb Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 10 Nov 2024 11:54:03 +0100 Subject: [PATCH 128/214] TASK: Dont use `findOneByHost` because it returns not the exact result In our case we want to skip the creation of a domain if it exists based on the host name (see c1d64ba3f4ccf01f60fd4f3f20f93a1642ba3ff2) but now we always skip sub domains, if the main domain is created first. --- Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php | 4 ++-- Neos.Neos/Classes/Domain/Repository/DomainRepository.php | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php b/Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php index 3ea256fe587..9f0897ab612 100644 --- a/Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php +++ b/Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php @@ -72,8 +72,8 @@ public function run(ProcessingContext $context): void $this->siteRepository->add($siteInstance); $this->persistenceManager->persistAll(); foreach ($site['domains'] ?? [] as $domain) { - $domainInstance = $this->domainRepository->findOneByHost($domain['hostname']); - if ($domainInstance) { + $domainInstance = $this->domainRepository->findByHostname($domain['hostname'])->getFirst(); + if ($domainInstance instanceof Domain) { $context->dispatch(Severity::NOTICE, sprintf('Domain "%s" already exists. Adding it to site "%s".', $domain['hostname'], $site['name'])); } else { $domainInstance = new Domain(); diff --git a/Neos.Neos/Classes/Domain/Repository/DomainRepository.php b/Neos.Neos/Classes/Domain/Repository/DomainRepository.php index a86fe3eac93..6b1cb089ef2 100644 --- a/Neos.Neos/Classes/Domain/Repository/DomainRepository.php +++ b/Neos.Neos/Classes/Domain/Repository/DomainRepository.php @@ -84,6 +84,7 @@ public function findByHost($hostname, $onlyActive = false) public function findOneByHost($hostname, $onlyActive = false): ?Domain { $allMatchingDomains = $this->findByHost($hostname, $onlyActive); + // Fixme, requesting `onedimension.localhost` if domain `localhost` exists in the set would return the latter because of `getSortedMatches` return count($allMatchingDomains) > 0 ? $allMatchingDomains[0] : null; } From 6fc88e0b7699f4403d18e12b00035fc67cf149ca Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 10 Nov 2024 11:59:50 +0100 Subject: [PATCH 129/214] BUGFIX: Create site with correct online state (inverse condition) --- Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php b/Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php index 9f0897ab612..a06818c5fb9 100644 --- a/Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php +++ b/Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php @@ -67,7 +67,7 @@ public function run(ProcessingContext $context): void } $siteInstance = new Site($siteNodeName->value); $siteInstance->setSiteResourcesPackageKey($site['siteResourcesPackageKey']); - $siteInstance->setState(($site['online'] ?? false) ? Site::STATE_OFFLINE : Site::STATE_ONLINE); + $siteInstance->setState($site['online'] ? Site::STATE_ONLINE : Site::STATE_OFFLINE); $siteInstance->setName($site['name']); $this->siteRepository->add($siteInstance); $this->persistenceManager->persistAll(); From 02e9cdea260c3fbaea0baa3b10c7d92c2868d957 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 10 Nov 2024 12:00:11 +0100 Subject: [PATCH 130/214] TASK: Simplify `SiteShape` --- .../Classes/Domain/Export/SiteExportProcessor.php | 2 +- .../Classes/Domain/Import/SiteCreationProcessor.php | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Neos.Neos/Classes/Domain/Export/SiteExportProcessor.php b/Neos.Neos/Classes/Domain/Export/SiteExportProcessor.php index f71a91f4c65..64f765847fa 100644 --- a/Neos.Neos/Classes/Domain/Export/SiteExportProcessor.php +++ b/Neos.Neos/Classes/Domain/Export/SiteExportProcessor.php @@ -27,7 +27,7 @@ * Export processor exports Neos {@see Site} instances as json * * @phpstan-type DomainShape array{hostname: string, scheme?: ?string, port?: ?int, active?: ?bool, primary?: ?bool } - * @phpstan-type SiteShape array{name:string, siteResourcesPackageKey:string, nodeName?: string, online?:bool, domains?: ?DomainShape[] } + * @phpstan-type SiteShape array{name:string, siteResourcesPackageKey:string, nodeName: string, online:bool, domains: DomainShape[] } * */ final readonly class SiteExportProcessor implements ProcessorInterface diff --git a/Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php b/Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php index a06818c5fb9..9c785a8a6d1 100644 --- a/Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php +++ b/Neos.Neos/Classes/Domain/Import/SiteCreationProcessor.php @@ -60,7 +60,7 @@ public function run(ProcessingContext $context): void foreach ($sites as $site) { $context->dispatch(Severity::NOTICE, sprintf('Creating site "%s"', $site['name'])); - $siteNodeName = !empty($site['nodeName']) ? NodeName::fromString($site['nodeName']) : NodeName::transliterateFromString($site['name']); + $siteNodeName = NodeName::fromString($site['nodeName']); if ($this->siteRepository->findOneByNodeName($siteNodeName->value)) { $context->dispatch(Severity::NOTICE, sprintf('Site for node name "%s" already exists, skipping', $siteNodeName->value)); continue; @@ -71,7 +71,7 @@ public function run(ProcessingContext $context): void $siteInstance->setName($site['name']); $this->siteRepository->add($siteInstance); $this->persistenceManager->persistAll(); - foreach ($site['domains'] ?? [] as $domain) { + foreach ($site['domains'] as $domain) { $domainInstance = $this->domainRepository->findByHostname($domain['hostname'])->getFirst(); if ($domainInstance instanceof Domain) { $context->dispatch(Severity::NOTICE, sprintf('Domain "%s" already exists. Adding it to site "%s".', $domain['hostname'], $site['name'])); @@ -109,11 +109,16 @@ private static function extractSitesFromEventStream(ProcessingContext $context): continue; } if ($event->type === 'NodeAggregateWithNodeWasCreated' && $event->payload['parentNodeAggregateId'] === $siteRooNodeAggregateId) { + if (!isset($event->payload['nodeName'])) { + throw new \RuntimeException(sprintf('The nodeName of the site node "%s" must not be empty', $event->payload['nodeAggregateId']), 1731236316); + } $sites[] = [ 'siteResourcesPackageKey' => self::extractPackageKeyFromNodeTypeName($event->payload['nodeTypeName']), 'name' => $event->payload['initialPropertyValues']['title']['value'] ?? $event->payload['nodeTypeName'], 'nodeTypeName' => $event->payload['nodeTypeName'], - 'nodeName' => $event->payload['nodeName'] ?? null, + 'nodeName' => $event->payload['nodeName'], + 'domains' => [], + 'online' => true ]; } }; From b11bf2e4a5418e76a28836a397c4db4ce3d83e10 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 10 Nov 2024 12:02:02 +0100 Subject: [PATCH 131/214] TASK: Remove `site:migrateLegacyData` (previously `cr:migrateLegacyData`) replaced with ``` ./flow site:exportLegacyDataCommand --path ./migratedContent ./flow site:importAll --path ./migratedContent ``` --- .../Classes/Command/SiteCommandController.php | 82 ++----------------- README.md | 4 +- 2 files changed, 10 insertions(+), 76 deletions(-) diff --git a/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/SiteCommandController.php b/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/SiteCommandController.php index 11b40881f03..6b887ce0a28 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/SiteCommandController.php +++ b/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/SiteCommandController.php @@ -25,19 +25,15 @@ use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Cli\CommandController; use Neos\Flow\Property\PropertyMapper; -use Neos\Flow\Utility\Environment; -use Neos\Neos\Domain\Service\SiteImportService; -use Neos\Utility\Files; use Neos\Neos\Domain\Service\NodeTypeNameFactory; +use Neos\Utility\Files; class SiteCommandController extends CommandController { public function __construct( private readonly Connection $connection, - private readonly Environment $environment, private readonly PropertyMapper $propertyMapper, private readonly ContentRepositoryRegistry $contentRepositoryRegistry, - private readonly SiteImportService $siteImportService, ) { parent::__construct(); } @@ -45,81 +41,17 @@ public function __construct( /** * Migrate from the Legacy CR * - * Note that the dimension configuration and the node type schema must be migrated of the content repository to import to and it must be setup. + * This command creates a Neos 9 export format based on the data from the specified legacy content repository database connection + * The export will be placed in the specified directory path, and can be imported via "site:importAll": * - * @param string $contentRepository The target content repository that will be used for importing into - * @param string|null $config JSON encoded configuration, for example '{"dbal": {"dbname": "some-other-db"}, "resourcesPath": "/some/absolute/path", "rootNodes": {"/sites": "Neos.Neos:Sites", "/other": "My.Package:SomeOtherRoot"}}' - * @throws \Exception - */ - public function migrateLegacyDataCommand(string $contentRepository = 'default', string $config = null, bool $verbose = false, ): void - { - if ($config !== null) { - try { - $parsedConfig = json_decode($config, true, 512, JSON_THROW_ON_ERROR); - } catch (\JsonException $e) { - throw new \InvalidArgumentException(sprintf('Failed to parse --config parameter: %s', $e->getMessage()), 1659526855, $e); - } - $resourcesPath = $parsedConfig['resourcesPath'] ?? self::defaultResourcesPath(); - $rootNodes = isset($parsedConfig['rootNodes']) ? RootNodeTypeMapping::fromArray($parsedConfig['rootNodes']) : $this->getDefaultRootNodes(); - try { - $connection = isset($parsedConfig['dbal']) ? DriverManager::getConnection(array_merge($this->connection->getParams(), $parsedConfig['dbal']), new Configuration()) : $this->connection; - } catch (DBALException $e) { - throw new \InvalidArgumentException(sprintf('Failed to get database connection, check the --config parameter: %s', $e->getMessage()), 1659527201, $e); - } - } else { - $resourcesPath = $this->determineResourcesPath(); - $rootNodes = $this->getDefaultRootNodes(); - if (!$this->output->askConfirmation(sprintf('Do you want to migrate nodes from the current database "%s@%s" (y/n)? ', $this->connection->getParams()['dbname'] ?? '?', $this->connection->getParams()['host'] ?? '?'))) { - $connection = $this->adjustDatabaseConnection($this->connection); - } else { - $connection = $this->connection; - } - } - $this->verifyDatabaseConnection($connection); - - $contentRepositoryId = ContentRepositoryId::fromString($contentRepository); - $temporaryFilePath = $this->environment->getPathToTemporaryDirectory() . uniqid('Export', true); - Files::createDirectoryRecursively($temporaryFilePath); - - $legacyExportService = $this->contentRepositoryRegistry->buildService( - $contentRepositoryId, - new LegacyExportServiceFactory( - $connection, - $resourcesPath, - $this->propertyMapper, - $rootNodes - ) - ); - - $legacyExportService->exportToPath( - $temporaryFilePath, - $this->createOnProcessorClosure(), - $this->createOnMessageClosure($verbose) - ); - - $this->outputLine('Migrated data. Importing into new content repository ...'); - - // todo check if cr is setup before!!! do not fail here!!! - $this->siteImportService->importFromPath( - $contentRepositoryId, - $temporaryFilePath, - $this->createOnProcessorClosure(), - $this->createOnMessageClosure($verbose) - ); - - Files::unlink($temporaryFilePath); - - $this->outputLine('Done'); - } - - /** - * Export from the Legacy CR into a specified directory path + * ./flow site:exportLegacyDataCommand --path ./migratedContent + * ./flow site:importAll --path ./migratedContent * * Note that the dimension configuration and the node type schema must be migrated of the reference content repository * * @param string $contentRepository The reference content repository that can later be used for importing into - * @param string $path The path to the directory, will be created if missing - * @param string|null $config JSON encoded configuration, for example '{"dbal": {"dbname": "some-other-db"}, "resourcesPath": "/some/absolute/path", "rootNodes": {"/sites": "Neos.Neos:Sites", "/other": "My.Package:SomeOtherRoot"}}' + * @param string $path The path to the directory to export to, will be created if missing + * @param string|null $config JSON encoded configuration, for example --config '{"dbal": {"dbname": "some-other-db"}, "resourcesPath": "/absolute-path/Data/Persistent/Resources", "rootNodes": {"/sites": "Neos.Neos:Sites", "/other": "My.Package:SomeOtherRoot"}}' * @throws \Exception */ public function exportLegacyDataCommand(string $path, string $contentRepository = 'default', string $config = null, bool $verbose = false): void diff --git a/README.md b/README.md index 5df252f9b2e..6b4d49d0d3c 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,9 @@ You can chose from one of the following options: ``` bash # the following config points to a Neos 8.0 database (adjust to your needs) -./flow site:migrateLegacyData --config '{"dbal": {"dbname": "neos80"}, "resourcesPath": "/path/to/neos-8.0/Data/Persistent/Resources"}' +./flow site:exportLegacyDataCommand --path ./migratedContent --config '{"dbal": {"dbname": "neos80"}, "resourcesPath": "/path/to/neos-8.0/Data/Persistent/Resources"}' +# import the migrated data +./flow site:importAll --path ./migratedContent ``` #### Importing an existing (Neos >= 9.0) Site from an Export From 41e472af735a37a34d766ad1d5d3736a336542b1 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 10 Nov 2024 13:04:52 +0100 Subject: [PATCH 132/214] BUGFIX: Export correctly export `primary` domain previously `primary` was always true if there is only one domain --- .../Classes/Processors/SitesExportProcessor.php | 2 +- Neos.Neos/Classes/Domain/Export/SiteExportProcessor.php | 2 +- Neos.Neos/Classes/Domain/Model/Site.php | 6 +++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Neos.ContentRepository.LegacyNodeMigration/Classes/Processors/SitesExportProcessor.php b/Neos.ContentRepository.LegacyNodeMigration/Classes/Processors/SitesExportProcessor.php index 9b66a0f0f94..0f2510586a3 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Classes/Processors/SitesExportProcessor.php +++ b/Neos.ContentRepository.LegacyNodeMigration/Classes/Processors/SitesExportProcessor.php @@ -52,7 +52,7 @@ function(array $domainRow) use ($siteRow) { 'hostname' => $domainRow['hostname'], 'scheme' => $domainRow['scheme'], 'port' => $domainRow['port'], - 'active' => $domainRow['active'], + 'active' => (bool)$domainRow['active'], 'primary' => $domainRow['persistence_object_identifier'] === $siteRow['primarydomain'], ]; }, diff --git a/Neos.Neos/Classes/Domain/Export/SiteExportProcessor.php b/Neos.Neos/Classes/Domain/Export/SiteExportProcessor.php index 64f765847fa..a0c4beb5b53 100644 --- a/Neos.Neos/Classes/Domain/Export/SiteExportProcessor.php +++ b/Neos.Neos/Classes/Domain/Export/SiteExportProcessor.php @@ -67,7 +67,7 @@ private function getSiteData(): array 'scheme' => $domain->getScheme(), 'port' => $domain->getPort(), 'active' => $domain->getActive(), - 'primary' => $domain === $site->getPrimaryDomain(), + 'primary' => $domain === $site->getPrimaryDomain(fallbackToActive: false), ], $site->getDomains()->toArray() ) diff --git a/Neos.Neos/Classes/Domain/Model/Site.php b/Neos.Neos/Classes/Domain/Model/Site.php index eb6cedae3c0..98d3cf8f7c5 100644 --- a/Neos.Neos/Classes/Domain/Model/Site.php +++ b/Neos.Neos/Classes/Domain/Model/Site.php @@ -331,11 +331,15 @@ public function setPrimaryDomain(Domain $domain = null) /** * Returns the primary domain, if one has been defined. * + * @param boolean $fallbackToActive if true falls back to the first active domain instead returning null if no primary domain was explicitly set * @return ?Domain The primary domain or NULL * @api */ - public function getPrimaryDomain(): ?Domain + public function getPrimaryDomain(bool $fallbackToActive = true): ?Domain { + if (!$fallbackToActive) { + return $this->primaryDomain; + } return $this->primaryDomain instanceof Domain && $this->primaryDomain->getActive() ? $this->primaryDomain : $this->getFirstActiveDomain(); From aa713471dec285d4123a3befc4a9371efde78b49 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 10 Nov 2024 13:05:40 +0100 Subject: [PATCH 133/214] BUGFIX: `site:importAll` import `copyrightNotice` of assets previously we didnt use the value that was in the export format. --- .../src/Processors/AssetRepositoryImportProcessor.php | 1 + 1 file changed, 1 insertion(+) diff --git a/Neos.ContentRepository.Export/src/Processors/AssetRepositoryImportProcessor.php b/Neos.ContentRepository.Export/src/Processors/AssetRepositoryImportProcessor.php index 2021098fb19..c7f7535069f 100644 --- a/Neos.ContentRepository.Export/src/Processors/AssetRepositoryImportProcessor.php +++ b/Neos.ContentRepository.Export/src/Processors/AssetRepositoryImportProcessor.php @@ -101,6 +101,7 @@ private function importAsset(ProcessingContext $context, StorageAttributes $file ObjectAccess::setProperty($asset, 'Persistence_Object_Identifier', $serializedAsset->identifier, true); $asset->setTitle($serializedAsset->title); $asset->setCaption($serializedAsset->caption); + $asset->setCopyrightNotice($serializedAsset->copyrightNotice); $this->assetRepository->add($asset); $this->persistenceManager->persistAll(); } From c4541173cd3587691b20196ef2f4dab3f3289261 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 10 Nov 2024 13:30:43 +0100 Subject: [PATCH 134/214] TASK: Trivial cosmetic changes --- .../Classes/Command/SiteCommandController.php | 2 +- Neos.Neos/Classes/Command/SiteCommandController.php | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/SiteCommandController.php b/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/SiteCommandController.php index 6b887ce0a28..c29da54c1d7 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/SiteCommandController.php +++ b/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/SiteCommandController.php @@ -168,7 +168,7 @@ protected function createOnMessageClosure(bool $verbose): \Closure } $this->outputLine(match ($severity) { Severity::NOTICE => $message, - Severity::WARNING => sprintf('Warning: %s', $message), + Severity::WARNING => sprintf('Warning: %s', $message), Severity::ERROR => sprintf('Error: %s', $message), }); }; diff --git a/Neos.Neos/Classes/Command/SiteCommandController.php b/Neos.Neos/Classes/Command/SiteCommandController.php index c281add9ce3..24037d9b95e 100644 --- a/Neos.Neos/Classes/Command/SiteCommandController.php +++ b/Neos.Neos/Classes/Command/SiteCommandController.php @@ -174,10 +174,11 @@ public function createCommand($name, $packageKey, $nodeType, $nodeName = null, $ */ public function importAllCommand(string $packageKey = null, string $path = null, string $contentRepository = 'default', bool $verbose = false): void { + // TODO check if this warning is still necessary with Neos 9 // Since this command uses a lot of memory when large sites are imported, we warn the user to watch for // the confirmation of a successful import. $this->outputLine('This command can use a lot of memory when importing sites with many resources.'); - $this->outputLine('If the import is successful, you will see a message saying "Import of site ... finished".'); + $this->outputLine('If the import is successful, you will see a message saying "Import finished".'); $this->outputLine('If you do not see this message, the import failed, most likely due to insufficient memory.'); $this->outputLine('Increase the memory_limit configuration parameter of your php CLI to attempt to fix this.'); $this->outputLine('Starting import...'); @@ -193,6 +194,8 @@ public function importAllCommand(string $packageKey = null, string $path = null, $this->createOnProcessorClosure(), $this->createOnMessageClosure($verbose) ); + + $this->outputLine('Import finished.'); } /** @@ -230,7 +233,7 @@ public function exportAllCommand(string $packageKey = null, string $path = null, */ public function pruneAllCommand(string $contentRepository = 'default', bool $force = false, bool $verbose = false): void { - if (!$force && !$this->output->askConfirmation(sprintf('> This will prune your content repository "%s". Are you sure to proceed? (y/n) ', $contentRepository), false)) { + if (!$force && !$this->output->askConfirmation(sprintf('> This will prune your content repository "%s" and all its attached sites. Are you sure to proceed? (y/n) ', $contentRepository), false)) { $this->outputLine('Abort.'); return; } @@ -367,7 +370,7 @@ protected function createOnMessageClosure(bool $verbose): \Closure } $this->outputLine(match ($severity) { Severity::NOTICE => $message, - Severity::WARNING => sprintf('Warning: %s', $message), + Severity::WARNING => sprintf('Warning: %s', $message), Severity::ERROR => sprintf('Error: %s', $message), }); }; From b8670cf6fbbf69a94339a239bd8b017d7f541a91 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 10 Nov 2024 13:31:44 +0100 Subject: [PATCH 135/214] TASK: Make pruning a noop if the workspace does not exist --- .../Domain/Pruning/SitePruningProcessor.php | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/Neos.Neos/Classes/Domain/Pruning/SitePruningProcessor.php b/Neos.Neos/Classes/Domain/Pruning/SitePruningProcessor.php index fcb6b9c7a89..d393d13f828 100644 --- a/Neos.Neos/Classes/Domain/Pruning/SitePruningProcessor.php +++ b/Neos.Neos/Classes/Domain/Pruning/SitePruningProcessor.php @@ -15,9 +15,12 @@ namespace Neos\Neos\Domain\Pruning; use Neos\ContentRepository\Core\ContentRepository; +use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphInterface; +use Neos\ContentRepository\Core\SharedModel\Exception\WorkspaceDoesNotExist; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepository\Export\ProcessingContext; use Neos\ContentRepository\Export\ProcessorInterface; +use Neos\ContentRepository\Export\Severity; use Neos\Flow\Persistence\PersistenceManagerInterface; use Neos\Neos\Domain\Model\Site; use Neos\Neos\Domain\Repository\DomainRepository; @@ -40,7 +43,13 @@ public function __construct( public function run(ProcessingContext $context): void { - $sites = $this->findAllSites(); + try { + $contentGraph = $this->contentRepository->getContentGraph($this->workspaceName); + } catch (WorkspaceDoesNotExist) { + $context->dispatch(Severity::NOTICE, sprintf('Could not find any matching sites, because the workspace "%s" does not exist.', $this->workspaceName->value)); + return; + } + $sites = $this->findAllSites($contentGraph); foreach ($sites as $site) { $domains = $site->getDomains(); if ($site->getPrimaryDomain() !== null) { @@ -59,9 +68,8 @@ public function run(ProcessingContext $context): void /** * @return Site[] */ - protected function findAllSites(): array + protected function findAllSites(ContentGraphInterface $contentGraph): array { - $contentGraph = $this->contentRepository->getContentGraph($this->workspaceName); $sitesNodeAggregate = $contentGraph->findRootNodeAggregateByType(NodeTypeNameFactory::forSites()); if ($sitesNodeAggregate === null) { return []; From 17db2eb1f134edb551bc837b292d1509a7a60f95 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 10 Nov 2024 13:41:33 +0100 Subject: [PATCH 136/214] TASK: Adjust documentation to reference correct command --- .../Classes/Command/SiteCommandController.php | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/SiteCommandController.php b/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/SiteCommandController.php index c29da54c1d7..9f2cc2d4a93 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/SiteCommandController.php +++ b/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/SiteCommandController.php @@ -44,7 +44,7 @@ public function __construct( * This command creates a Neos 9 export format based on the data from the specified legacy content repository database connection * The export will be placed in the specified directory path, and can be imported via "site:importAll": * - * ./flow site:exportLegacyDataCommand --path ./migratedContent + * ./flow site:exportLegacyData --path ./migratedContent * ./flow site:importAll --path ./migratedContent * * Note that the dimension configuration and the node type schema must be migrated of the reference content repository diff --git a/README.md b/README.md index 6b4d49d0d3c..4183dd4275c 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ You can chose from one of the following options: ``` bash # the following config points to a Neos 8.0 database (adjust to your needs) -./flow site:exportLegacyDataCommand --path ./migratedContent --config '{"dbal": {"dbname": "neos80"}, "resourcesPath": "/path/to/neos-8.0/Data/Persistent/Resources"}' +./flow site:exportLegacyData --path ./migratedContent --config '{"dbal": {"dbname": "neos80"}, "resourcesPath": "/path/to/neos-8.0/Data/Persistent/Resources"}' # import the migrated data ./flow site:importAll --path ./migratedContent ``` From 5696f369130fa20853011945f9f1f3725645cf2d Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 10 Nov 2024 13:43:46 +0100 Subject: [PATCH 137/214] TASK: Adjust test to 49234ffb1bd3a5f3391120f8eb0be72c6d831952 --- .../Tests/Behavior/Features/Errors.feature | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/Errors.feature b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/Errors.feature index c41e915c647..3dc37a50acc 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/Errors.feature +++ b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/Errors.feature @@ -5,6 +5,7 @@ Feature: Exceptional cases during migrations Given using no content dimensions And using the following node types: """yaml + 'unstructured': {} 'Neos.Neos:Site': {} 'Some.Package:Homepage': superTypes: @@ -146,5 +147,5 @@ Feature: Exceptional cases during migrations And I run the event migration Then I expect a migration exception with the message """ - The site node "site-node-id" (type: "unstructured") must be of type "Neos.Neos:Site" + The site node "site-node-id" (type: "unstructured") must be of type "Neos.Neos:Site". Currently declared super types: "" """ From feb40b056ad6a6944f36142570d754a5eeccc6ec Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Sun, 10 Nov 2024 13:54:10 +0100 Subject: [PATCH 138/214] Mark CommandHooks and factories `@api` and tweak doc comments --- .../Classes/CommandHandler/CommandHooks.php | 4 ++-- .../Classes/Factory/CommandHookFactoryInterface.php | 2 +- .../Classes/Factory/CommandHooksFactoryDependencies.php | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHooks.php b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHooks.php index fec4fc25750..0f96412fc3d 100644 --- a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHooks.php +++ b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHooks.php @@ -5,10 +5,10 @@ namespace Neos\ContentRepository\Core\CommandHandler; /** - * Collection of {@see CommandHookInterface} instances + * Collection of {@see CommandHookInterface} instances, functioning as a delegating command hook implementation * * @implements \IteratorAggregate - * @internal + * @api */ final readonly class CommandHooks implements CommandHookInterface, \IteratorAggregate, \Countable { diff --git a/Neos.ContentRepository.Core/Classes/Factory/CommandHookFactoryInterface.php b/Neos.ContentRepository.Core/Classes/Factory/CommandHookFactoryInterface.php index c9eebfba86c..2418556ff5c 100644 --- a/Neos.ContentRepository.Core/Classes/Factory/CommandHookFactoryInterface.php +++ b/Neos.ContentRepository.Core/Classes/Factory/CommandHookFactoryInterface.php @@ -7,7 +7,7 @@ use Neos\ContentRepository\Core\CommandHandler\CommandHookInterface; /** - * @internal + * @api for implementers of custom {@see CommandHookInterface}s */ interface CommandHookFactoryInterface { diff --git a/Neos.ContentRepository.Core/Classes/Factory/CommandHooksFactoryDependencies.php b/Neos.ContentRepository.Core/Classes/Factory/CommandHooksFactoryDependencies.php index 083fa9a10f6..5149682ff4b 100644 --- a/Neos.ContentRepository.Core/Classes/Factory/CommandHooksFactoryDependencies.php +++ b/Neos.ContentRepository.Core/Classes/Factory/CommandHooksFactoryDependencies.php @@ -14,11 +14,12 @@ namespace Neos\ContentRepository\Core\Factory; +use Neos\ContentRepository\Core\CommandHandler\CommandHookInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphReadModelInterface; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; /** - * @internal + * @api for implementers of custom {@see CommandHookInterface}s */ final readonly class CommandHooksFactoryDependencies { From 85698043954b9a18ca7f0f903a9fdfbcaec83a99 Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Sun, 10 Nov 2024 14:21:08 +0100 Subject: [PATCH 139/214] Fix `test_parallel` CR settings --- .../Configuration/Settings.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Neos.ContentRepository.BehavioralTests/Configuration/Settings.yaml b/Neos.ContentRepository.BehavioralTests/Configuration/Settings.yaml index 212fb010b63..d38e376fd54 100644 --- a/Neos.ContentRepository.BehavioralTests/Configuration/Settings.yaml +++ b/Neos.ContentRepository.BehavioralTests/Configuration/Settings.yaml @@ -27,8 +27,8 @@ Neos: factoryObjectName: Neos\ContentRepository\BehavioralTests\TestSuite\Behavior\GherkinPyStringNodeBasedNodeTypeManagerFactory contentDimensionSource: factoryObjectName: Neos\ContentRepository\BehavioralTests\TestSuite\Behavior\GherkinTableNodeBasedContentDimensionSourceFactory - userIdProvider: - factoryObjectName: Neos\ContentRepositoryRegistry\Factory\UserIdProvider\StaticUserIdProviderFactory + authProvider: + factoryObjectName: Neos\ContentRepositoryRegistry\Factory\AuthProvider\StaticAuthProviderFactory clock: factoryObjectName: Neos\ContentRepositoryRegistry\Factory\Clock\SystemClockFactory propertyConverters: {} From 8edd96fbed65d99c0bd7284b6fa7582f9feec75c Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 11 Nov 2024 10:46:45 +0100 Subject: [PATCH 140/214] TASK: Improve assertion of `WorkspaceRebaseFailed` in test --- .../W8-IndividualNodePublication/03-MoreBasicFeatures.feature | 4 +++- .../Bootstrap/GenericCommandExecutionAndEventPublication.php | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/03-MoreBasicFeatures.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/03-MoreBasicFeatures.feature index 545b56cfea8..b65bee35200 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/03-MoreBasicFeatures.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/03-MoreBasicFeatures.feature @@ -200,7 +200,9 @@ Feature: Publishing individual nodes (basics) | workspaceName | "user-test" | | nodesToPublish | [{"dimensionSpacePoint": {}, "nodeAggregateId": "sir-unchanged"}] | | contentStreamIdForRemainingPart | "user-cs-identifier-remaining" | - Then the last command should have thrown an exception of type "WorkspaceRebaseFailed" + Then the last command should have thrown the WorkspaceRebaseFailed exception with: + | SequenceNumber | Command | Exception | + | 14 | TagSubtree | SubtreeIsAlreadyTagged | Scenario: It is possible to publish all nodes When the command PublishIndividualNodesFromWorkspace is executed with payload: diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php index dd899ee4e24..5cffba5ba4f 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php @@ -151,6 +151,10 @@ protected function publishEvent(string $eventType, StreamName $streamName, array */ public function theLastCommandShouldHaveThrown(string $shortExceptionName, ?int $expectedCode = null): void { + if ($shortExceptionName === 'WorkspaceRebaseFailed') { + throw new \RuntimeException('Please use the assertion "the last command should have thrown the WorkspaceRebaseFailed exception with" instead.'); + } + Assert::assertNotNull($this->lastCommandException, 'Command did not throw exception'); $lastCommandExceptionShortName = (new \ReflectionClass($this->lastCommandException))->getShortName(); Assert::assertSame($shortExceptionName, $lastCommandExceptionShortName, sprintf('Actual exception: %s (%s): %s', get_class($this->lastCommandException), $this->lastCommandException->getCode(), $this->lastCommandException->getMessage())); From 466c89e583dbbc9bfdf356e256f7f5cea69ec28f Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Mon, 11 Nov 2024 15:40:31 +0100 Subject: [PATCH 141/214] Simplify `ContentRepositoryAuthorizationService` API according to review comments --- .../Classes/Controller/UsageController.php | 10 +- .../Controller/Frontend/NodeController.php | 7 +- Neos.Neos/Classes/Domain/Model/User.php | 12 +- .../Domain/Service/WorkspaceService.php | 4 +- .../ContentRepositoryAuthorizationService.php | 125 ++++-------------- .../ContentRepositoryAuthProvider.php | 40 ++---- .../Bootstrap/WorkspaceServiceTrait.php | 23 ++-- .../Controller/WorkspaceController.php | 30 ++--- 8 files changed, 77 insertions(+), 174 deletions(-) diff --git a/Neos.Media.Browser/Classes/Controller/UsageController.php b/Neos.Media.Browser/Classes/Controller/UsageController.php index a62d36b6298..d3634d48125 100644 --- a/Neos.Media.Browser/Classes/Controller/UsageController.php +++ b/Neos.Media.Browser/Classes/Controller/UsageController.php @@ -22,13 +22,13 @@ use Neos\Flow\Security\Context as SecurityContext; use Neos\Media\Domain\Model\AssetInterface; use Neos\Media\Domain\Service\AssetService; +use Neos\Neos\AssetUsage\Dto\AssetUsageReference; use Neos\Neos\Domain\Repository\SiteRepository; use Neos\Neos\Domain\Service\NodeTypeNameFactory; use Neos\Neos\Domain\Service\WorkspaceService; use Neos\Neos\FrontendRouting\SiteDetection\SiteDetectionResult; use Neos\Neos\Security\Authorization\ContentRepositoryAuthorizationService; use Neos\Neos\Service\UserService; -use Neos\Neos\AssetUsage\Dto\AssetUsageReference; /** * Controller for asset usage handling @@ -117,13 +117,7 @@ public function relatedNodesAction(AssetInterface $asset) ); $nodeType = $nodeAggregate ? $contentRepository->getNodeTypeManager()->getNodeType($nodeAggregate->nodeTypeName) : null; - $authenticatedAccount = $this->securityContext->getAccount(); - if ($authenticatedAccount !== null) { - $workspacePermissions = $this->contentRepositoryAuthorizationService->getWorkspacePermissionsForAccount($currentContentRepositoryId, $usage->getWorkspaceName(), $authenticatedAccount); - } else { - $workspacePermissions = $this->contentRepositoryAuthorizationService->getWorkspacePermissionsForAnonymousUser($currentContentRepositoryId, $usage->getWorkspaceName()); - } - + $workspacePermissions = $this->contentRepositoryAuthorizationService->getWorkspacePermissions($currentContentRepositoryId, $usage->getWorkspaceName(), $this->securityContext->getRoles(), $this->userService->getBackendUser()?->getId()); $workspace = $contentRepository->findWorkspaceByName($usage->getWorkspaceName()); $inaccessibleRelation['nodeIdentifier'] = $usage->getNodeAggregateId()->value; diff --git a/Neos.Neos/Classes/Controller/Frontend/NodeController.php b/Neos.Neos/Classes/Controller/Frontend/NodeController.php index 7191c4e496f..19300dea675 100644 --- a/Neos.Neos/Classes/Controller/Frontend/NodeController.php +++ b/Neos.Neos/Classes/Controller/Frontend/NodeController.php @@ -200,12 +200,7 @@ public function showAction(string $node): void } $contentRepository = $this->contentRepositoryRegistry->get($nodeAddress->contentRepositoryId); - $authenticatedAccount = $this->securityContext->getAccount(); - if ($authenticatedAccount !== null) { - $visibilityConstraints = $this->contentRepositoryAuthorizationService->getVisibilityConstraintsForAccount($contentRepository->id, $authenticatedAccount); - } else { - $visibilityConstraints = $this->contentRepositoryAuthorizationService->getVisibilityConstraintsForAnonymousUser($contentRepository->id); - } + $visibilityConstraints = $this->contentRepositoryAuthorizationService->getVisibilityConstraints($contentRepository->id, $this->securityContext->getRoles()); // By default, the visibility constraints only contain the SubtreeTags the authenticated user has _no_ access to // Neos backend users have access to the "disabled" SubtreeTag so that they can see/edit disabled nodes. // In this showAction (= "frontend") we have to explicitly remove those disabled nodes, even if the user was authenticated, diff --git a/Neos.Neos/Classes/Domain/Model/User.php b/Neos.Neos/Classes/Domain/Model/User.php index 33abe790644..24db0c6cfc3 100644 --- a/Neos.Neos/Classes/Domain/Model/User.php +++ b/Neos.Neos/Classes/Domain/Model/User.php @@ -92,21 +92,13 @@ public function setPreferences(UserPreferences $preferences) * @api */ public function isActive() - { - return $this->getFirstActiveAccount() !== null; - } - - /** - * @api - */ - public function getFirstActiveAccount(): ?Account { foreach ($this->accounts as $account) { /** @var Account $account */ if ($account->isActive()) { - return $account; + return true; } } - return null; + return false; } } diff --git a/Neos.Neos/Classes/Domain/Service/WorkspaceService.php b/Neos.Neos/Classes/Domain/Service/WorkspaceService.php index 3f7523c94e1..228605c322b 100644 --- a/Neos.Neos/Classes/Domain/Service/WorkspaceService.php +++ b/Neos.Neos/Classes/Domain/Service/WorkspaceService.php @@ -236,7 +236,7 @@ public function unassignWorkspaceRole(ContentRepositoryId $contentRepositoryId, /** * Get all role assignments for the specified workspace * - * NOTE: This should never be used to evaluate permissions, instead {@see ContentRepositoryAuthorizationService::getWorkspacePermissionsForAccount()} and {@see ContentRepositoryAuthorizationService::getWorkspacePermissionsForAnonymousUser()} should be used! + * NOTE: This should never be used to evaluate permissions, instead {@see ContentRepositoryAuthorizationService::getWorkspacePermissions()} should be used! */ public function getWorkspaceRoleAssignments(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName): WorkspaceRoleAssignments { @@ -272,7 +272,7 @@ public function getWorkspaceRoleAssignments(ContentRepositoryId $contentReposito /** * Get the role with the most privileges for the specified {@see WorkspaceRoleSubjects} on workspace $workspaceName * - * NOTE: This should never be used to evaluate permissions, instead {@see ContentRepositoryAuthorizationService::getWorkspacePermissionsForAccount()} and {@see ContentRepositoryAuthorizationService::getWorkspacePermissionsForAnonymousUser()} should be used! + * NOTE: This should never be used to evaluate permissions, instead {@see ContentRepositoryAuthorizationService::getWorkspacePermissions()} should be used! */ public function getMostPrivilegedWorkspaceRoleForSubjects(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, WorkspaceRoleSubjects $subjects): ?WorkspaceRole { diff --git a/Neos.Neos/Classes/Security/Authorization/ContentRepositoryAuthorizationService.php b/Neos.Neos/Classes/Security/Authorization/ContentRepositoryAuthorizationService.php index 2f6cc32cf1f..d0027c076a7 100644 --- a/Neos.Neos/Classes/Security/Authorization/ContentRepositoryAuthorizationService.php +++ b/Neos.Neos/Classes/Security/Authorization/ContentRepositoryAuthorizationService.php @@ -10,12 +10,12 @@ use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\Flow\Annotations as Flow; -use Neos\Flow\Security\Account; use Neos\Flow\Security\Authorization\PrivilegeManagerInterface; +use Neos\Flow\Security\Context; use Neos\Flow\Security\Policy\PolicyService; use Neos\Flow\Security\Policy\Role; use Neos\Neos\Domain\Model\NodePermissions; -use Neos\Neos\Domain\Model\User; +use Neos\Neos\Domain\Model\UserId; use Neos\Neos\Domain\Model\WorkspacePermissions; use Neos\Neos\Domain\Model\WorkspaceRole; use Neos\Neos\Domain\Model\WorkspaceRoleSubject; @@ -24,7 +24,6 @@ use Neos\Neos\Security\Authorization\Privilege\EditNodePrivilege; use Neos\Neos\Security\Authorization\Privilege\ReadNodePrivilege; use Neos\Neos\Security\Authorization\Privilege\SubtreeTagPrivilegeSubject; -use Neos\Party\Domain\Service\PartyService; /** * Central point which does ContentRepository authorization decisions within Neos. @@ -34,13 +33,9 @@ #[Flow\Scope('singleton')] final readonly class ContentRepositoryAuthorizationService { - private const FLOW_ROLE_EVERYBODY = 'Neos.Flow:Everybody'; - private const FLOW_ROLE_ANONYMOUS = 'Neos.Flow:Anonymous'; - private const FLOW_ROLE_AUTHENTICATED_USER = 'Neos.Flow:AuthenticatedUser'; - private const FLOW_ROLE_NEOS_ADMINISTRATOR = 'Neos.Neos:Administrator'; + private const ROLE_NEOS_ADMINISTRATOR = 'Neos.Neos:Administrator'; public function __construct( - private PartyService $partyService, private WorkspaceService $workspaceService, private PolicyService $policyService, private PrivilegeManagerInterface $privilegeManager, @@ -48,119 +43,61 @@ public function __construct( } /** - * Determines the {@see WorkspacePermissions} an anonymous user has for the specified workspace (aka "public access") + * Determines the {@see WorkspacePermissions} a user with the specified {@see Role}s has for the specified workspace + * + * @param array $roles The {@see Role} instances to check access for. Note: These have to be the expanded roles auf the authenticated tokens {@see Context::getRoles()} + * @param UserId|null $userId Optional ID of the authenticated Neos user. If set the workspace owner is evaluated since owners always have all permissions on their workspace */ - public function getWorkspacePermissionsForAnonymousUser(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName): WorkspacePermissions - { - $subjects = [WorkspaceRoleSubject::createForGroup(self::FLOW_ROLE_EVERYBODY), WorkspaceRoleSubject::createForGroup(self::FLOW_ROLE_ANONYMOUS)]; - $userWorkspaceRole = $this->workspaceService->getMostPrivilegedWorkspaceRoleForSubjects($contentRepositoryId, $workspaceName, WorkspaceRoleSubjects::fromArray($subjects)); - if ($userWorkspaceRole === null) { - return WorkspacePermissions::none(sprintf('Anonymous user has no explicit role for workspace "%s"', $workspaceName->value)); - } - return WorkspacePermissions::create( - read: $userWorkspaceRole->isAtLeast(WorkspaceRole::VIEWER), - write: $userWorkspaceRole->isAtLeast(WorkspaceRole::COLLABORATOR), - manage: $userWorkspaceRole->isAtLeast(WorkspaceRole::MANAGER), - reason: sprintf('Anonymous user has role "%s" for workspace "%s"', $userWorkspaceRole->value, $workspaceName->value), - ); - } - - /** - * Determines the {@see WorkspacePermissions} the given user has for the specified workspace - */ - public function getWorkspacePermissionsForAccount(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, Account $account): WorkspacePermissions + public function getWorkspacePermissions(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, array $roles, UserId|null $userId): WorkspacePermissions { $workspaceMetadata = $this->workspaceService->getWorkspaceMetadata($contentRepositoryId, $workspaceName); - $neosUser = $this->neosUserFromAccount($account); - if ($workspaceMetadata->ownerUserId !== null && $neosUser !== null && $neosUser->getId()->equals($workspaceMetadata->ownerUserId)) { - return WorkspacePermissions::all(sprintf('User "%s" (id: %s is the owner of workspace "%s"', $neosUser->getLabel(), $neosUser->getId()->value, $workspaceName->value)); + if ($userId !== null && $workspaceMetadata->ownerUserId !== null && $userId->equals($workspaceMetadata->ownerUserId)) { + return WorkspacePermissions::all(sprintf('User with id "%s" is the owner of workspace "%s"', $userId->value, $workspaceName->value)); } - $userRoles = $this->expandAccountRoles($account); - $userIsAdministrator = array_key_exists(self::FLOW_ROLE_NEOS_ADMINISTRATOR, $userRoles); - $subjects = array_map(WorkspaceRoleSubject::createForGroup(...), array_keys($userRoles)); - - if ($neosUser !== null) { - $subjects[] = WorkspaceRoleSubject::createForUser($neosUser->getId()); + $roleIdentifiers = array_map(static fn (Role $role) => $role->getIdentifier(), $roles); + $subjects = array_map(WorkspaceRoleSubject::createForGroup(...), $roleIdentifiers); + if ($userId !== null) { + $subjects[] = WorkspaceRoleSubject::createForUser($userId); } + $userIsAdministrator = array_key_exists(self::ROLE_NEOS_ADMINISTRATOR, $roleIdentifiers); + $userWorkspaceRole = $this->workspaceService->getMostPrivilegedWorkspaceRoleForSubjects($contentRepositoryId, $workspaceName, WorkspaceRoleSubjects::fromArray($subjects)); if ($userWorkspaceRole === null) { if ($userIsAdministrator) { - return WorkspacePermissions::manage(sprintf('Account "%s" is a Neos Administrator without explicit role for workspace "%s"', $account->getAccountIdentifier(), $workspaceName->value)); + return WorkspacePermissions::manage(sprintf('User is a Neos Administrator without explicit role for workspace "%s"', $workspaceName->value)); } - return WorkspacePermissions::none(sprintf('Account "%s" is no Neos Administrator and has no explicit role for workspace "%s"', $account->getAccountIdentifier(), $workspaceName->value)); + return WorkspacePermissions::none(sprintf('User is no Neos Administrator and has no explicit role for workspace "%s"', $workspaceName->value)); } return WorkspacePermissions::create( read: $userWorkspaceRole->isAtLeast(WorkspaceRole::VIEWER), write: $userWorkspaceRole->isAtLeast(WorkspaceRole::COLLABORATOR), manage: $userIsAdministrator || $userWorkspaceRole->isAtLeast(WorkspaceRole::MANAGER), - reason: sprintf('Account "%s" is %s Neos Administrator and has role "%s" for workspace "%s"', $account->getAccountIdentifier(), $userIsAdministrator ? 'a' : 'no', $userWorkspaceRole->value, $workspaceName->value), + reason: sprintf('User is %s Neos Administrator and has role "%s" for workspace "%s"', $userIsAdministrator ? 'a' : 'no', $userWorkspaceRole->value, $workspaceName->value), ); } - public function getNodePermissionsForAnonymousUser(Node $node): NodePermissions - { - $roles = $this->rolesOfAnonymousUser(); - return $this->nodePermissionsForRoles($node, $roles); - } - - public function getNodePermissionsForAccount(Node $node, Account $account): NodePermissions - { - $roles = $this->expandAccountRoles($account); - return $this->nodePermissionsForRoles($node, $roles); - } - /** - * Determines the default {@see VisibilityConstraints} for an anonymous user (aka "public access") + * Determines the {@see NodePermissions} a user with the specified {@see Role}s has on the given {@see Node} + * + * @param array $roles */ - public function getVisibilityConstraintsForAnonymousUser(ContentRepositoryId $contentRepositoryId): VisibilityConstraints + public function getNodePermissions(Node $node, array $roles): NodePermissions { - $roles = $this->rolesOfAnonymousUser(); - return VisibilityConstraints::fromTagConstraints($this->tagConstraintsForRoles($contentRepositoryId, $roles)); + return $this->nodePermissionsForRoles($node, $roles); } /** - * Determines the default {@see VisibilityConstraints} for the specified account + * Determines the default {@see VisibilityConstraints} for the specified {@see Role}s + * + * @param array $roles */ - public function getVisibilityConstraintsForAccount(ContentRepositoryId $contentRepositoryId, Account $account): VisibilityConstraints + public function getVisibilityConstraints(ContentRepositoryId $contentRepositoryId, array $roles): VisibilityConstraints { - $roles = $this->expandAccountRoles($account); return VisibilityConstraints::fromTagConstraints($this->tagConstraintsForRoles($contentRepositoryId, $roles)); } // ------------------------------ - /** - * @return array - */ - private function rolesOfAnonymousUser(): array - { - return [ - self::FLOW_ROLE_EVERYBODY => $this->policyService->getRole(self::FLOW_ROLE_EVERYBODY), - self::FLOW_ROLE_ANONYMOUS => $this->policyService->getRole(self::FLOW_ROLE_ANONYMOUS), - ]; - } - - /** - * @return array - */ - private function expandAccountRoles(Account $account): array - { - $roles = [ - self::FLOW_ROLE_EVERYBODY => $this->policyService->getRole(self::FLOW_ROLE_EVERYBODY), - self::FLOW_ROLE_AUTHENTICATED_USER => $this->policyService->getRole(self::FLOW_ROLE_AUTHENTICATED_USER), - ]; - foreach ($account->getRoles() as $currentRole) { - if (!array_key_exists($currentRole->getIdentifier(), $roles)) { - $roles[$currentRole->getIdentifier()] = $currentRole; - } - foreach ($currentRole->getAllParentRoles() as $currentParentRole) { - if (!array_key_exists($currentParentRole->getIdentifier(), $roles)) { - $roles[$currentParentRole->getIdentifier()] = $currentParentRole; - } - } - } - return $roles; - } /** * @param array $roles @@ -177,12 +114,6 @@ private function tagConstraintsForRoles(ContentRepositoryId $contentRepositoryId return $restrictedSubtreeTags; } - private function neosUserFromAccount(Account $account): ?User - { - $user = $this->partyService->getAssignedPartyOfAccount($account); - return $user instanceof User ? $user : null; - } - /** * @param array $roles */ diff --git a/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php index 95ff0a58ece..a84657c5aaa 100644 --- a/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php +++ b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php @@ -78,11 +78,7 @@ public function getAuthenticatedUserId(): ?UserId public function getVisibilityConstraints(WorkspaceName $workspaceName): VisibilityConstraints { - $authenticatedAccount = $this->securityContext->getAccount(); - if ($authenticatedAccount) { - return $this->authorizationService->getVisibilityConstraintsForAccount($this->contentRepositoryId, $authenticatedAccount); - } - return $this->authorizationService->getVisibilityConstraintsForAnonymousUser($this->contentRepositoryId); + return $this->authorizationService->getVisibilityConstraints($this->contentRepositoryId, $this->securityContext->getRoles()); } public function canReadNodesFromWorkspace(WorkspaceName $workspaceName): Privilege @@ -90,12 +86,12 @@ public function canReadNodesFromWorkspace(WorkspaceName $workspaceName): Privile if ($this->securityContext->areAuthorizationChecksDisabled()) { return Privilege::granted('Authorization checks are disabled'); } - $authenticatedAccount = $this->securityContext->getAccount(); - if ($authenticatedAccount === null) { - $workspacePermissions = $this->authorizationService->getWorkspacePermissionsForAnonymousUser($this->contentRepositoryId, $workspaceName); - } else { - $workspacePermissions = $this->authorizationService->getWorkspacePermissionsForAccount($this->contentRepositoryId, $workspaceName, $authenticatedAccount); - } + $workspacePermissions = $this->authorizationService->getWorkspacePermissions( + $this->contentRepositoryId, + $workspaceName, + $this->securityContext->getRoles(), + $this->userService->getCurrentUser()?->getId(), + ); return $workspacePermissions->read ? Privilege::granted($workspacePermissions->getReason()) : Privilege::denied($workspacePermissions->getReason()); } @@ -117,7 +113,7 @@ public function canExecuteCommand(CommandInterface $command): Privilege if ($node === null) { return Privilege::denied(sprintf('Failed to load node "%s" in workspace "%s"', $nodeThatRequiresEditPrivilege->aggregateId->value, $nodeThatRequiresEditPrivilege->workspaceName->value)); } - $nodePermissions = $this->getNodePermissionsForCurrentUser($node); + $nodePermissions = $this->authorizationService->getNodePermissions($node, $this->securityContext->getRoles()); if (!$nodePermissions->edit) { return Privilege::denied(sprintf('No edit permissions for node "%s" in workspace "%s": %s', $nodeThatRequiresEditPrivilege->aggregateId->value, $nodeThatRequiresEditPrivilege->workspaceName->value, $nodePermissions->getReason())); } @@ -199,19 +195,11 @@ private function requireWorkspaceManagePermission(WorkspaceName $workspaceName): private function getWorkspacePermissionsForCurrentUser(WorkspaceName $workspaceName): WorkspacePermissions { - $authenticatedAccount = $this->securityContext->getAccount(); - if ($authenticatedAccount === null) { - return $this->authorizationService->getWorkspacePermissionsForAnonymousUser($this->contentRepositoryId, $workspaceName); - } - return $this->authorizationService->getWorkspacePermissionsForAccount($this->contentRepositoryId, $workspaceName, $authenticatedAccount); - } - - private function getNodePermissionsForCurrentUser(Node $node): NodePermissions - { - $authenticatedAccount = $this->securityContext->getAccount(); - if ($authenticatedAccount === null) { - return $this->authorizationService->getNodePermissionsForAnonymousUser($node); - } - return $this->authorizationService->getNodePermissionsForAccount($node, $authenticatedAccount); + return $this->authorizationService->getWorkspacePermissions( + $this->contentRepositoryId, + $workspaceName, + $this->securityContext->getRoles(), + $this->userService->getCurrentUser()?->getId(), + ); } } diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/WorkspaceServiceTrait.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/WorkspaceServiceTrait.php index 416b6d4a8d3..74ba8b77b53 100644 --- a/Neos.Neos/Tests/Behavior/Features/Bootstrap/WorkspaceServiceTrait.php +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/WorkspaceServiceTrait.php @@ -18,6 +18,7 @@ use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Command\CreateWorkspace; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; +use Neos\Flow\Security\Context; use Neos\Neos\Domain\Model\UserId; use Neos\Neos\Domain\Model\WorkspaceDescription; use Neos\Neos\Domain\Model\WorkspaceRole; @@ -212,14 +213,15 @@ public function theWorkspaceShouldHaveTheFollowingRoleAssignments($workspaceName */ public function theNeosUserShouldHaveThePermissionsForWorkspace(string $username, string $expectedPermissions, string $workspaceName): void { - $user = $this->getObject(UserService::class)->getUser($username); + $userService = $this->getObject(UserService::class); + $user = $userService->getUser($username); Assert::assertNotNull($user); - $account = $user->getAccounts()->first(); - Assert::assertNotNull($account); - $permissions = $this->getObject(ContentRepositoryAuthorizationService::class)->getWorkspacePermissionsForAccount( + $roles = $userService->getAllRoles($user); + $permissions = $this->getObject(ContentRepositoryAuthorizationService::class)->getWorkspacePermissions( $this->currentContentRepository->id, WorkspaceName::fromString($workspaceName), - $account, + $roles, + $user->getId(), ); Assert::assertSame($expectedPermissions, implode(',', array_keys(array_filter(get_object_vars($permissions))))); } @@ -229,14 +231,15 @@ public function theNeosUserShouldHaveThePermissionsForWorkspace(string $username */ public function theNeosUserShouldHaveNoPermissionsForWorkspace(string $username, string $workspaceName): void { - $user = $this->getObject(UserService::class)->getUser($username); + $userService = $this->getObject(UserService::class); + $user = $userService->getUser($username); Assert::assertNotNull($user); - $account = $user->getAccounts()->first(); - Assert::assertNotNull($account); - $permissions = $this->getObject(ContentRepositoryAuthorizationService::class)->getWorkspacePermissionsForAccount( + $roles = $userService->getAllRoles($user); + $permissions = $this->getObject(ContentRepositoryAuthorizationService::class)->getWorkspacePermissions( $this->currentContentRepository->id, WorkspaceName::fromString($workspaceName), - $account, + $roles, + $user->getId(), ); Assert::assertFalse($permissions->read); Assert::assertFalse($permissions->write); diff --git a/Neos.Workspace.Ui/Classes/Controller/WorkspaceController.php b/Neos.Workspace.Ui/Classes/Controller/WorkspaceController.php index 3b45f907c58..e8f02988acb 100644 --- a/Neos.Workspace.Ui/Classes/Controller/WorkspaceController.php +++ b/Neos.Workspace.Ui/Classes/Controller/WorkspaceController.php @@ -43,6 +43,7 @@ use Neos\Flow\Mvc\Exception\StopActionException; use Neos\Flow\Package\PackageManager; use Neos\Flow\Property\PropertyMapper; +use Neos\Flow\Security\Context; use Neos\Flow\Security\Exception\AccessDeniedException; use Neos\Media\Domain\Model\AssetInterface; use Neos\Media\Domain\Model\ImageInterface; @@ -90,6 +91,9 @@ class WorkspaceController extends AbstractModuleController #[Flow\Inject] protected PropertyMapper $propertyMapper; + #[Flow\Inject] + protected Context $securityContext; + #[Flow\Inject] protected UserService $userService; @@ -110,8 +114,8 @@ class WorkspaceController extends AbstractModuleController */ public function indexAction(): void { - $authenticatedAccount = $this->userService->getCurrentUser()?->getFirstActiveAccount(); - if ($authenticatedAccount === null) { + $currentUser = $this->userService->getCurrentUser(); + if ($currentUser === null) { throw new AccessDeniedException('No user authenticated', 1718308216); } @@ -140,7 +144,7 @@ public function indexAction(): void continue; } $workspaceMetadata = $this->workspaceService->getWorkspaceMetadata($contentRepositoryId, $workspace->workspaceName); - $permissions = $this->contentRepositoryAuthorizationService->getWorkspacePermissionsForAccount($contentRepositoryId, $workspace->workspaceName, $authenticatedAccount); + $permissions = $this->contentRepositoryAuthorizationService->getWorkspacePermissions($contentRepositoryId, $workspace->workspaceName, $this->securityContext->getRoles(), $currentUser->getId()); if (!$permissions->read) { continue; } @@ -160,8 +164,8 @@ classification: $workspaceMetadata->classification->name, public function showAction(WorkspaceName $workspace): void { - $authenticatedAccount = $this->userService->getCurrentUser()?->getFirstActiveAccount(); - if ($authenticatedAccount === null) { + $currentUser = $this->userService->getCurrentUser(); + if ($currentUser === null) { throw new AccessDeniedException('No user authenticated', 1720371024); } $contentRepositoryId = SiteDetectionResult::fromRequest($this->request->getHttpRequest())->contentRepositoryId; @@ -179,7 +183,7 @@ public function showAction(WorkspaceName $workspace): void $baseWorkspace = $contentRepository->findWorkspaceByName($workspaceObj->baseWorkspaceName); assert($baseWorkspace !== null); $baseWorkspaceMetadata = $this->workspaceService->getWorkspaceMetadata($contentRepositoryId, $baseWorkspace->workspaceName); - $baseWorkspacePermissions = $this->contentRepositoryAuthorizationService->getWorkspacePermissionsForAccount($contentRepositoryId, $baseWorkspace->workspaceName, $authenticatedAccount); + $baseWorkspacePermissions = $this->contentRepositoryAuthorizationService->getWorkspacePermissions($contentRepositoryId, $baseWorkspace->workspaceName, $this->securityContext->getRoles(), $currentUser->getId()); } $this->view->assignMultiple([ 'selectedWorkspace' => $workspaceObj, @@ -289,11 +293,11 @@ public function updateAction( $contentRepositoryId = SiteDetectionResult::fromRequest($this->request->getHttpRequest())->contentRepositoryId; $contentRepository = $this->contentRepositoryRegistry->get($contentRepositoryId); - $authenticatedAccount = $this->userService->getCurrentUser()?->getFirstActiveAccount(); - if ($authenticatedAccount === null) { + $currentUser = $this->userService->getCurrentUser(); + if ($currentUser === null) { throw new AccessDeniedException('No user is authenticated', 1729620262); } - $workspacePermissions = $this->contentRepositoryAuthorizationService->getWorkspacePermissionsForAccount($contentRepository->id, $workspaceName, $authenticatedAccount); + $workspacePermissions = $this->contentRepositoryAuthorizationService->getWorkspacePermissions($contentRepository->id, $workspaceName, $this->securityContext->getRoles(), $currentUser->getId()); if (!$workspacePermissions->manage) { throw new AccessDeniedException(sprintf('The authenticated user does not have manage permissions for workspace "%s"', $workspaceName->value), 1729620297); } @@ -1008,7 +1012,7 @@ protected function prepareBaseWorkspaceOptions( ContentRepository $contentRepository, WorkspaceName $excludedWorkspace = null, ): array { - $authenticatedAccount = $this->userService->getCurrentUser()?->getFirstActiveAccount(); + $currentUser = $this->userService->getCurrentUser(); $baseWorkspaceOptions = []; $workspaces = $contentRepository->findWorkspaces(); foreach ($workspaces as $workspace) { @@ -1024,11 +1028,7 @@ protected function prepareBaseWorkspaceOptions( if (!in_array($workspaceMetadata->classification, [WorkspaceClassification::SHARED, WorkspaceClassification::ROOT], true)) { continue; } - if ($authenticatedAccount !== null) { - $permissions = $this->contentRepositoryAuthorizationService->getWorkspacePermissionsForAccount($contentRepository->id, $workspace->workspaceName, $authenticatedAccount); - } else { - $permissions = $this->contentRepositoryAuthorizationService->getWorkspacePermissionsForAnonymousUser($contentRepository->id, $workspace->workspaceName); - } + $permissions = $this->contentRepositoryAuthorizationService->getWorkspacePermissions($contentRepository->id, $workspace->workspaceName, $this->securityContext->getRoles(), $currentUser?->getId()); if (!$permissions->manage) { continue; } From 637a9c653df1c4e98103240e461369512ce4e38e Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Mon, 11 Nov 2024 15:55:51 +0100 Subject: [PATCH 142/214] Fix role conversion in `ContentRepositoryAuthorizationService` --- .../Authorization/ContentRepositoryAuthorizationService.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Neos.Neos/Classes/Security/Authorization/ContentRepositoryAuthorizationService.php b/Neos.Neos/Classes/Security/Authorization/ContentRepositoryAuthorizationService.php index d0027c076a7..0afdb315f02 100644 --- a/Neos.Neos/Classes/Security/Authorization/ContentRepositoryAuthorizationService.php +++ b/Neos.Neos/Classes/Security/Authorization/ContentRepositoryAuthorizationService.php @@ -54,13 +54,12 @@ public function getWorkspacePermissions(ContentRepositoryId $contentRepositoryId if ($userId !== null && $workspaceMetadata->ownerUserId !== null && $userId->equals($workspaceMetadata->ownerUserId)) { return WorkspacePermissions::all(sprintf('User with id "%s" is the owner of workspace "%s"', $userId->value, $workspaceName->value)); } - $roleIdentifiers = array_map(static fn (Role $role) => $role->getIdentifier(), $roles); + $roleIdentifiers = array_map(static fn (Role $role) => $role->getIdentifier(), array_values($roles)); $subjects = array_map(WorkspaceRoleSubject::createForGroup(...), $roleIdentifiers); if ($userId !== null) { $subjects[] = WorkspaceRoleSubject::createForUser($userId); } - $userIsAdministrator = array_key_exists(self::ROLE_NEOS_ADMINISTRATOR, $roleIdentifiers); - + $userIsAdministrator = in_array(self::ROLE_NEOS_ADMINISTRATOR, $roleIdentifiers, true); $userWorkspaceRole = $this->workspaceService->getMostPrivilegedWorkspaceRoleForSubjects($contentRepositoryId, $workspaceName, WorkspaceRoleSubjects::fromArray($subjects)); if ($userWorkspaceRole === null) { if ($userIsAdministrator) { From 6397ff4ec27caa004cc796122cea44e7bbb8c762 Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Mon, 11 Nov 2024 17:11:04 +0100 Subject: [PATCH 143/214] Mark `NodePermissions` and `WorkspacePermissions` `@api` --- Neos.Neos/Classes/Domain/Model/NodePermissions.php | 2 +- Neos.Neos/Classes/Domain/Model/WorkspacePermissions.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Neos.Neos/Classes/Domain/Model/NodePermissions.php b/Neos.Neos/Classes/Domain/Model/NodePermissions.php index 5a2ff8733a8..28300b92af7 100644 --- a/Neos.Neos/Classes/Domain/Model/NodePermissions.php +++ b/Neos.Neos/Classes/Domain/Model/NodePermissions.php @@ -13,7 +13,7 @@ * - read: Permission to read the node and its properties and references * - edit: Permission to change the node * - * @internal + * @api because it is returned by the {@see ContentRepositoryAuthorizationService} */ #[Flow\Proxy(false)] final readonly class NodePermissions diff --git a/Neos.Neos/Classes/Domain/Model/WorkspacePermissions.php b/Neos.Neos/Classes/Domain/Model/WorkspacePermissions.php index 421ed38c2cb..5761d61a74a 100644 --- a/Neos.Neos/Classes/Domain/Model/WorkspacePermissions.php +++ b/Neos.Neos/Classes/Domain/Model/WorkspacePermissions.php @@ -14,7 +14,7 @@ * - write: Permission to write to the corresponding workspace, including publishing a derived workspace to it * - manage: Permission to change the metadata and roles of the corresponding workspace (e.g. change description/title or add/remove workspace roles) * - * @internal + * @api because it is returned by the {@see ContentRepositoryAuthorizationService} */ #[Flow\Proxy(false)] final readonly class WorkspacePermissions From 952932700344fbe63c1a516404582c1a054433ea Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 11 Nov 2024 17:38:13 +0100 Subject: [PATCH 144/214] TASK: Split mighty `WorkspaceService` into `WorkspaceMetadataAndRoleRepository` By moving the db implementation logic in a separate class, the dependency chain is cleaned up, as the `WorkspaceService` would previously hold the cr which would hold the `ContentRepositoryAuthorizationService` which has the `WorkspaceService` Also we didnt want to add further zickzack and previously it would not have been able to acquire the `ContentRepositoryAuthorizationService` without hacks in the `WorkspaceService` to evaluate permissions for `setWorkspaceTitle` or `assignWorkspaceRole` --- .../WorkspaceMetadataAndRoleRepository.php | 314 ++++++++++++++++++ .../Domain/Service/WorkspaceService.php | 265 +-------------- .../ContentRepositoryAuthorizationService.php | 10 +- 3 files changed, 336 insertions(+), 253 deletions(-) create mode 100644 Neos.Neos/Classes/Domain/Repository/WorkspaceMetadataAndRoleRepository.php diff --git a/Neos.Neos/Classes/Domain/Repository/WorkspaceMetadataAndRoleRepository.php b/Neos.Neos/Classes/Domain/Repository/WorkspaceMetadataAndRoleRepository.php new file mode 100644 index 00000000000..ed6c928f6ca --- /dev/null +++ b/Neos.Neos/Classes/Domain/Repository/WorkspaceMetadataAndRoleRepository.php @@ -0,0 +1,314 @@ +dbal->insert(self::TABLE_NAME_WORKSPACE_ROLE, [ + 'content_repository_id' => $contentRepositoryId->value, + 'workspace_name' => $workspaceName->value, + 'subject_type' => $assignment->subject->type->value, + 'subject' => $assignment->subject->value, + 'role' => $assignment->role->value, + ]); + } catch (UniqueConstraintViolationException $e) { + throw new \RuntimeException(sprintf('Failed to assign role for workspace "%s" to subject "%s" (Content Repository "%s"): There is already a role assigned for that user/group, please unassign that first', $workspaceName->value, $assignment->subject->value, $contentRepositoryId->value), 1728476154, $e); + } catch (DbalException $e) { + throw new \RuntimeException(sprintf('Failed to assign role for workspace "%s" to subject "%s" (Content Repository "%s"): %s', $workspaceName->value, $assignment->subject->value, $contentRepositoryId->value, $e->getMessage()), 1728396138, $e); + } + } + + /** + * The public and documented API is {@see WorkspaceService::unassignWorkspaceRole} + */ + public function unassignWorkspaceRole(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, WorkspaceRoleSubject $subject): void + { + try { + $affectedRows = $this->dbal->delete(self::TABLE_NAME_WORKSPACE_ROLE, [ + 'content_repository_id' => $contentRepositoryId->value, + 'workspace_name' => $workspaceName->value, + 'subject_type' => $subject->type->value, + 'subject' => $subject->value, + ]); + } catch (DbalException $e) { + throw new \RuntimeException(sprintf('Failed to unassign role for subject "%s" from workspace "%s" (Content Repository "%s"): %s', $subject->value, $workspaceName->value, $contentRepositoryId->value, $e->getMessage()), 1728396169, $e); + } + if ($affectedRows === 0) { + throw new \RuntimeException(sprintf('Failed to unassign role for subject "%s" from workspace "%s" (Content Repository "%s"): No role assignment exists for this user/group', $subject->value, $workspaceName->value, $contentRepositoryId->value), 1728477071); + } + } + + public function getWorkspaceRoleAssignments(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName): WorkspaceRoleAssignments + { + $table = self::TABLE_NAME_WORKSPACE_ROLE; + $query = <<dbal->fetchAllAssociative($query, [ + 'contentRepositoryId' => $contentRepositoryId->value, + 'workspaceName' => $workspaceName->value, + ]); + } catch (DbalException $e) { + throw new \RuntimeException(sprintf('Failed to fetch workspace role assignments for workspace "%s" (Content Repository "%s"): %s', $workspaceName->value, $contentRepositoryId->value, $e->getMessage()), 1728474440, $e); + } + return WorkspaceRoleAssignments::fromArray( + array_map(static fn (array $row) => WorkspaceRoleAssignment::create( + WorkspaceRoleSubject::create( + WorkspaceRoleSubjectType::from($row['subject_type']), + $row['subject'], + ), + WorkspaceRole::from($row['role']), + ), $rows) + ); + } + + public function getMostPrivilegedWorkspaceRoleForSubjects(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, WorkspaceRoleSubjects $subjects): ?WorkspaceRole + { + $tableRole = self::TABLE_NAME_WORKSPACE_ROLE; + $roleCasesBySpecificity = implode("\n", array_map(static fn (WorkspaceRole $role) => "WHEN role='{$role->value}' THEN {$role->specificity()}\n", WorkspaceRole::cases())); + $query = <<type === WorkspaceRoleSubjectType::GROUP) { + $groupSubjectValues[] = $subject->value; + } else { + $userSubjectValues[] = $subject->value; + } + } + try { + $role = $this->dbal->fetchOne($query, [ + 'contentRepositoryId' => $contentRepositoryId->value, + 'workspaceName' => $workspaceName->value, + 'userSubjectType' => WorkspaceRoleSubjectType::USER->value, + 'userSubjectValues' => $userSubjectValues, + 'groupSubjectType' => WorkspaceRoleSubjectType::GROUP->value, + 'groupSubjectValues' => $groupSubjectValues, + ], [ + 'userSubjectValues' => ArrayParameterType::STRING, + 'groupSubjectValues' => ArrayParameterType::STRING, + ]); + } catch (DbalException $e) { + throw new \RuntimeException(sprintf('Failed to load role for workspace "%s" (content repository "%s"): %e', $workspaceName->value, $contentRepositoryId->value, $e->getMessage()), 1729325871, $e); + } + if ($role === false) { + return null; + } + return WorkspaceRole::from($role); + } + + /** + * Removes all workspace metadata records for the specified content repository id + */ + public function pruneWorkspaceMetadata(ContentRepositoryId $contentRepositoryId): void + { + try { + $this->dbal->delete(self::TABLE_NAME_WORKSPACE_METADATA, [ + 'content_repository_id' => $contentRepositoryId->value, + ]); + } catch (DbalException $e) { + throw new \RuntimeException(sprintf('Failed to prune workspace metadata Content Repository "%s": %s', $contentRepositoryId->value, $e->getMessage()), 1729512100, $e); + } + } + + /** + * Removes all workspace role assignments for the specified content repository id + */ + public function pruneRoleAssignments(ContentRepositoryId $contentRepositoryId): void + { + try { + $this->dbal->delete(self::TABLE_NAME_WORKSPACE_ROLE, [ + 'content_repository_id' => $contentRepositoryId->value, + ]); + } catch (DbalException $e) { + throw new \RuntimeException(sprintf('Failed to prune workspace roles for Content Repository "%s": %s', $contentRepositoryId->value, $e->getMessage()), 1729512142, $e); + } + } + + /** + * The public and documented API is {@see WorkspaceService::getWorkspaceMetadata()} + */ + public function loadWorkspaceMetadata(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName): ?WorkspaceMetadata + { + $table = self::TABLE_NAME_WORKSPACE_METADATA; + $query = <<dbal->fetchAssociative($query, [ + 'contentRepositoryId' => $contentRepositoryId->value, + 'workspaceName' => $workspaceName->value, + ]); + } catch (DbalException $e) { + throw new \RuntimeException(sprintf( + 'Failed to fetch metadata for workspace "%s" (Content Repository "%s), please ensure the database schema is up to date. %s', + $workspaceName->value, + $contentRepositoryId->value, + $e->getMessage() + ), 1727782164, $e); + } + if (!is_array($metadataRow)) { + return null; + } + return new WorkspaceMetadata( + WorkspaceTitle::fromString($metadataRow['title']), + WorkspaceDescription::fromString($metadataRow['description']), + WorkspaceClassification::from($metadataRow['classification']), + $metadataRow['owner_user_id'] !== null ? UserId::fromString($metadataRow['owner_user_id']) : null, + ); + } + + /** + * The public and documented API is {@see WorkspaceService::setWorkspaceTitle()} and {@see WorkspaceService::setWorkspaceDescription()} + */ + public function updateWorkspaceMetadata(ContentRepositoryId $contentRepositoryId, Workspace $workspace, string|null $title, string|null $description): void + { + $data = array_filter([ + 'title' => $title, + 'description' => $description, + ], fn ($value) => $value !== null); + + try { + $affectedRows = $this->dbal->update(self::TABLE_NAME_WORKSPACE_METADATA, $data, [ + 'content_repository_id' => $contentRepositoryId->value, + 'workspace_name' => $workspace->workspaceName->value, + ]); + if ($affectedRows === 0) { + $this->dbal->insert(self::TABLE_NAME_WORKSPACE_METADATA, [ + 'content_repository_id' => $contentRepositoryId->value, + 'workspace_name' => $workspace->workspaceName->value, + 'description' => '', + 'title' => $workspace->workspaceName->value, + 'classification' => $workspace->isRootWorkspace() ? WorkspaceClassification::ROOT->value : WorkspaceClassification::UNKNOWN->value, + ...$data, + ]); + } + } catch (DbalException $e) { + throw new \RuntimeException(sprintf('Failed to update metadata for workspace "%s" (Content Repository "%s"): %s', $workspace->workspaceName->value, $contentRepositoryId->value, $e->getMessage()), 1726821159, $e); + } + } + + public function addWorkspaceMetadata(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, WorkspaceTitle $title, WorkspaceDescription $description, WorkspaceClassification $classification, UserId|null $ownerUserId): void + { + try { + $this->dbal->insert(self::TABLE_NAME_WORKSPACE_METADATA, [ + 'content_repository_id' => $contentRepositoryId->value, + 'workspace_name' => $workspaceName->value, + 'title' => $title->value, + 'description' => $description->value, + 'classification' => $classification->value, + 'owner_user_id' => $ownerUserId?->value, + ]); + } catch (DbalException $e) { + throw new \RuntimeException(sprintf('Failed to add metadata for workspace "%s" (Content Repository "%s"): %s', $workspaceName->value, $contentRepositoryId->value, $e->getMessage()), 1727084068, $e); + } + } + + public function findPrimaryWorkspaceNameForUser(ContentRepositoryId $contentRepositoryId, UserId $userId): ?WorkspaceName + { + $tableMetadata = self::TABLE_NAME_WORKSPACE_METADATA; + $query = <<dbal->fetchOne($query, [ + 'contentRepositoryId' => $contentRepositoryId->value, + 'personalWorkspaceClassification' => WorkspaceClassification::PERSONAL->value, + 'userId' => $userId->value, + ]); + return $workspaceName === false ? null : WorkspaceName::fromString($workspaceName); + } +} diff --git a/Neos.Neos/Classes/Domain/Service/WorkspaceService.php b/Neos.Neos/Classes/Domain/Service/WorkspaceService.php index 228605c322b..9aa2c14670c 100644 --- a/Neos.Neos/Classes/Domain/Service/WorkspaceService.php +++ b/Neos.Neos/Classes/Domain/Service/WorkspaceService.php @@ -14,10 +14,6 @@ namespace Neos\Neos\Domain\Service; -use Doctrine\DBAL\ArrayParameterType; -use Doctrine\DBAL\Connection; -use Doctrine\DBAL\Exception as DbalException; -use Doctrine\DBAL\Exception\UniqueConstraintViolationException; use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Command\CreateRootWorkspace; use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Command\CreateWorkspace; use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Exception\WorkspaceAlreadyExists; @@ -36,9 +32,8 @@ use Neos\Neos\Domain\Model\WorkspaceRoleAssignment; use Neos\Neos\Domain\Model\WorkspaceRoleAssignments; use Neos\Neos\Domain\Model\WorkspaceRoleSubject; -use Neos\Neos\Domain\Model\WorkspaceRoleSubjects; -use Neos\Neos\Domain\Model\WorkspaceRoleSubjectType; use Neos\Neos\Domain\Model\WorkspaceTitle; +use Neos\Neos\Domain\Repository\WorkspaceMetadataAndRoleRepository; use Neos\Neos\Security\Authorization\ContentRepositoryAuthorizationService; /** @@ -49,12 +44,9 @@ #[Flow\Scope('singleton')] final readonly class WorkspaceService { - private const TABLE_NAME_WORKSPACE_METADATA = 'neos_neos_workspace_metadata'; - private const TABLE_NAME_WORKSPACE_ROLE = 'neos_neos_workspace_role'; - public function __construct( private ContentRepositoryRegistry $contentRepositoryRegistry, - private Connection $dbal, + private WorkspaceMetadataAndRoleRepository $metadataAndRoleRepository ) { } @@ -68,7 +60,7 @@ public function __construct( public function getWorkspaceMetadata(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName): WorkspaceMetadata { $workspace = $this->requireWorkspace($contentRepositoryId, $workspaceName); - $metadata = $this->loadWorkspaceMetadata($contentRepositoryId, $workspaceName); + $metadata = $this->metadataAndRoleRepository->loadWorkspaceMetadata($contentRepositoryId, $workspaceName); return $metadata ?? new WorkspaceMetadata( WorkspaceTitle::fromString($workspaceName->value), WorkspaceDescription::fromString(''), @@ -84,9 +76,8 @@ public function getWorkspaceMetadata(ContentRepositoryId $contentRepositoryId, W */ public function setWorkspaceTitle(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, WorkspaceTitle $newWorkspaceTitle): void { - $this->updateWorkspaceMetadata($contentRepositoryId, $workspaceName, [ - 'title' => $newWorkspaceTitle->value, - ]); + $workspace = $this->requireWorkspace($contentRepositoryId, $workspaceName); + $this->metadataAndRoleRepository->updateWorkspaceMetadata($contentRepositoryId, $workspace, title: $newWorkspaceTitle->value, description: null); } /** @@ -96,9 +87,8 @@ public function setWorkspaceTitle(ContentRepositoryId $contentRepositoryId, Work */ public function setWorkspaceDescription(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, WorkspaceDescription $newWorkspaceDescription): void { - $this->updateWorkspaceMetadata($contentRepositoryId, $workspaceName, [ - 'description' => $newWorkspaceDescription->value, - ]); + $workspace = $this->requireWorkspace($contentRepositoryId, $workspaceName); + $this->metadataAndRoleRepository->updateWorkspaceMetadata($contentRepositoryId, $workspace, title: null, description: $newWorkspaceDescription->value); } /** @@ -108,7 +98,7 @@ public function setWorkspaceDescription(ContentRepositoryId $contentRepositoryId */ public function getPersonalWorkspaceForUser(ContentRepositoryId $contentRepositoryId, UserId $userId): Workspace { - $workspaceName = $this->findPrimaryWorkspaceNameForUser($contentRepositoryId, $userId); + $workspaceName = $this->metadataAndRoleRepository->findPrimaryWorkspaceNameForUser($contentRepositoryId, $userId); if ($workspaceName === null) { throw new \RuntimeException(sprintf('No workspace is assigned to the user with id "%s")', $userId->value), 1718293801); } @@ -129,7 +119,7 @@ public function createRootWorkspace(ContentRepositoryId $contentRepositoryId, Wo ContentStreamId::create() ) ); - $this->addWorkspaceMetadata($contentRepositoryId, $workspaceName, $title, $description, WorkspaceClassification::ROOT, null); + $this->metadataAndRoleRepository->addWorkspaceMetadata($contentRepositoryId, $workspaceName, $title, $description, WorkspaceClassification::ROOT, null); } /** @@ -145,7 +135,7 @@ public function createLiveWorkspaceIfMissing(ContentRepositoryId $contentReposit return; } $this->createRootWorkspace($contentRepositoryId, $workspaceName, WorkspaceTitle::fromString('Public live workspace'), WorkspaceDescription::empty()); - $this->assignWorkspaceRole($contentRepositoryId, $workspaceName, WorkspaceRoleAssignment::createForGroup('Neos.Neos:LivePublisher', WorkspaceRole::COLLABORATOR)); + $this->metadataAndRoleRepository->assignWorkspaceRole($contentRepositoryId, $workspaceName, WorkspaceRoleAssignment::createForGroup('Neos.Neos:LivePublisher', WorkspaceRole::COLLABORATOR)); } /** @@ -170,7 +160,7 @@ public function createSharedWorkspace(ContentRepositoryId $contentRepositoryId, */ public function createPersonalWorkspaceForUserIfMissing(ContentRepositoryId $contentRepositoryId, User $user): void { - $existingWorkspaceName = $this->findPrimaryWorkspaceNameForUser($contentRepositoryId, $user->getId()); + $existingWorkspaceName = $this->metadataAndRoleRepository->findPrimaryWorkspaceNameForUser($contentRepositoryId, $user->getId()); if ($existingWorkspaceName !== null) { $this->requireWorkspace($contentRepositoryId, $existingWorkspaceName); return; @@ -195,19 +185,7 @@ public function createPersonalWorkspaceForUserIfMissing(ContentRepositoryId $con public function assignWorkspaceRole(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, WorkspaceRoleAssignment $assignment): void { $this->requireWorkspace($contentRepositoryId, $workspaceName); - try { - $this->dbal->insert(self::TABLE_NAME_WORKSPACE_ROLE, [ - 'content_repository_id' => $contentRepositoryId->value, - 'workspace_name' => $workspaceName->value, - 'subject_type' => $assignment->subject->type->value, - 'subject' => $assignment->subject->value, - 'role' => $assignment->role->value, - ]); - } catch (UniqueConstraintViolationException $e) { - throw new \RuntimeException(sprintf('Failed to assign role for workspace "%s" to subject "%s" (Content Repository "%s"): There is already a role assigned for that user/group, please unassign that first', $workspaceName->value, $assignment->subject->value, $contentRepositoryId->value), 1728476154, $e); - } catch (DbalException $e) { - throw new \RuntimeException(sprintf('Failed to assign role for workspace "%s" to subject "%s" (Content Repository "%s"): %s', $workspaceName->value, $assignment->subject->value, $contentRepositoryId->value, $e->getMessage()), 1728396138, $e); - } + $this->metadataAndRoleRepository->assignWorkspaceRole($contentRepositoryId, $workspaceName, $assignment); } /** @@ -218,19 +196,7 @@ public function assignWorkspaceRole(ContentRepositoryId $contentRepositoryId, Wo public function unassignWorkspaceRole(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, WorkspaceRoleSubject $subject): void { $this->requireWorkspace($contentRepositoryId, $workspaceName); - try { - $affectedRows = $this->dbal->delete(self::TABLE_NAME_WORKSPACE_ROLE, [ - 'content_repository_id' => $contentRepositoryId->value, - 'workspace_name' => $workspaceName->value, - 'subject_type' => $subject->type->value, - 'subject' => $subject->value, - ]); - } catch (DbalException $e) { - throw new \RuntimeException(sprintf('Failed to unassign role for subject "%s" from workspace "%s" (Content Repository "%s"): %s', $subject->value, $workspaceName->value, $contentRepositoryId->value, $e->getMessage()), 1728396169, $e); - } - if ($affectedRows === 0) { - throw new \RuntimeException(sprintf('Failed to unassign role for subject "%s" from workspace "%s" (Content Repository "%s"): No role assignment exists for this user/group', $subject->value, $workspaceName->value, $contentRepositoryId->value), 1728477071); - } + $this->metadataAndRoleRepository->unassignWorkspaceRole($contentRepositoryId, $workspaceName, $subject); } /** @@ -240,93 +206,7 @@ public function unassignWorkspaceRole(ContentRepositoryId $contentRepositoryId, */ public function getWorkspaceRoleAssignments(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName): WorkspaceRoleAssignments { - $table = self::TABLE_NAME_WORKSPACE_ROLE; - $query = <<dbal->fetchAllAssociative($query, [ - 'contentRepositoryId' => $contentRepositoryId->value, - 'workspaceName' => $workspaceName->value, - ]); - } catch (DbalException $e) { - throw new \RuntimeException(sprintf('Failed to fetch workspace role assignments for workspace "%s" (Content Repository "%s"): %s', $workspaceName->value, $contentRepositoryId->value, $e->getMessage()), 1728474440, $e); - } - return WorkspaceRoleAssignments::fromArray( - array_map(static fn (array $row) => WorkspaceRoleAssignment::create( - WorkspaceRoleSubject::create( - WorkspaceRoleSubjectType::from($row['subject_type']), - $row['subject'], - ), - WorkspaceRole::from($row['role']), - ), $rows) - ); - } - - /** - * Get the role with the most privileges for the specified {@see WorkspaceRoleSubjects} on workspace $workspaceName - * - * NOTE: This should never be used to evaluate permissions, instead {@see ContentRepositoryAuthorizationService::getWorkspacePermissions()} should be used! - */ - public function getMostPrivilegedWorkspaceRoleForSubjects(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, WorkspaceRoleSubjects $subjects): ?WorkspaceRole - { - $tableRole = self::TABLE_NAME_WORKSPACE_ROLE; - $roleCasesBySpecificity = implode("\n", array_map(static fn (WorkspaceRole $role) => "WHEN role='{$role->value}' THEN {$role->specificity()}\n", WorkspaceRole::cases())); - $query = <<type === WorkspaceRoleSubjectType::GROUP) { - $groupSubjectValues[] = $subject->value; - } else { - $userSubjectValues[] = $subject->value; - } - } - try { - $role = $this->dbal->fetchOne($query, [ - 'contentRepositoryId' => $contentRepositoryId->value, - 'workspaceName' => $workspaceName->value, - 'userSubjectType' => WorkspaceRoleSubjectType::USER->value, - 'userSubjectValues' => $userSubjectValues, - 'groupSubjectType' => WorkspaceRoleSubjectType::GROUP->value, - 'groupSubjectValues' => $groupSubjectValues, - ], [ - 'userSubjectValues' => ArrayParameterType::STRING, - 'groupSubjectValues' => ArrayParameterType::STRING, - ]); - } catch (DbalException $e) { - throw new \RuntimeException(sprintf('Failed to load role for workspace "%s" (content repository "%s"): %e', $workspaceName->value, $contentRepositoryId->value, $e->getMessage()), 1729325871, $e); - } - if ($role === false) { - return null; - } - return WorkspaceRole::from($role); + return $this->metadataAndRoleRepository->getWorkspaceRoleAssignments($contentRepositoryId, $workspaceName); } /** @@ -362,13 +242,7 @@ public function getUniqueWorkspaceName(ContentRepositoryId $contentRepositoryId, */ public function pruneWorkspaceMetadata(ContentRepositoryId $contentRepositoryId): void { - try { - $this->dbal->delete(self::TABLE_NAME_WORKSPACE_METADATA, [ - 'content_repository_id' => $contentRepositoryId->value, - ]); - } catch (DbalException $e) { - throw new \RuntimeException(sprintf('Failed to prune workspace metadata Content Repository "%s": %s', $contentRepositoryId->value, $e->getMessage()), 1729512100, $e); - } + $this->metadataAndRoleRepository->pruneWorkspaceMetadata($contentRepositoryId); } /** @@ -376,79 +250,11 @@ public function pruneWorkspaceMetadata(ContentRepositoryId $contentRepositoryId) */ public function pruneRoleAssignments(ContentRepositoryId $contentRepositoryId): void { - try { - $this->dbal->delete(self::TABLE_NAME_WORKSPACE_ROLE, [ - 'content_repository_id' => $contentRepositoryId->value, - ]); - } catch (DbalException $e) { - throw new \RuntimeException(sprintf('Failed to prune workspace roles for Content Repository "%s": %s', $contentRepositoryId->value, $e->getMessage()), 1729512142, $e); - } + $this->metadataAndRoleRepository->pruneRoleAssignments($contentRepositoryId); } // ------------------ - private function loadWorkspaceMetadata(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName): ?WorkspaceMetadata - { - $table = self::TABLE_NAME_WORKSPACE_METADATA; - $query = <<dbal->fetchAssociative($query, [ - 'contentRepositoryId' => $contentRepositoryId->value, - 'workspaceName' => $workspaceName->value, - ]); - } catch (DbalException $e) { - throw new \RuntimeException(sprintf( - 'Failed to fetch metadata for workspace "%s" (Content Repository "%s), please ensure the database schema is up to date. %s', - $workspaceName->value, - $contentRepositoryId->value, - $e->getMessage() - ), 1727782164, $e); - } - if (!is_array($metadataRow)) { - return null; - } - return new WorkspaceMetadata( - WorkspaceTitle::fromString($metadataRow['title']), - WorkspaceDescription::fromString($metadataRow['description']), - WorkspaceClassification::from($metadataRow['classification']), - $metadataRow['owner_user_id'] !== null ? UserId::fromString($metadataRow['owner_user_id']) : null, - ); - } - - /** - * @param array $data - */ - private function updateWorkspaceMetadata(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, array $data): void - { - $workspace = $this->requireWorkspace($contentRepositoryId, $workspaceName); - try { - $affectedRows = $this->dbal->update(self::TABLE_NAME_WORKSPACE_METADATA, $data, [ - 'content_repository_id' => $contentRepositoryId->value, - 'workspace_name' => $workspaceName->value, - ]); - if ($affectedRows === 0) { - $this->dbal->insert(self::TABLE_NAME_WORKSPACE_METADATA, [ - 'content_repository_id' => $contentRepositoryId->value, - 'workspace_name' => $workspaceName->value, - 'description' => '', - 'title' => $workspaceName->value, - 'classification' => $workspace->isRootWorkspace() ? WorkspaceClassification::ROOT->value : WorkspaceClassification::UNKNOWN->value, - ...$data, - ]); - } - } catch (DbalException $e) { - throw new \RuntimeException(sprintf('Failed to update metadata for workspace "%s" (Content Repository "%s"): %s', $workspaceName->value, $contentRepositoryId->value, $e->getMessage()), 1726821159, $e); - } - } - private function createWorkspace(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, WorkspaceTitle $title, WorkspaceDescription $description, WorkspaceName $baseWorkspaceName, UserId|null $ownerId, WorkspaceClassification $classification): void { $contentRepository = $this->contentRepositoryRegistry->get($contentRepositoryId); @@ -459,44 +265,7 @@ private function createWorkspace(ContentRepositoryId $contentRepositoryId, Works ContentStreamId::create() ) ); - $this->addWorkspaceMetadata($contentRepositoryId, $workspaceName, $title, $description, $classification, $ownerId); - } - - private function addWorkspaceMetadata(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, WorkspaceTitle $title, WorkspaceDescription $description, WorkspaceClassification $classification, UserId|null $ownerUserId): void - { - try { - $this->dbal->insert(self::TABLE_NAME_WORKSPACE_METADATA, [ - 'content_repository_id' => $contentRepositoryId->value, - 'workspace_name' => $workspaceName->value, - 'title' => $title->value, - 'description' => $description->value, - 'classification' => $classification->value, - 'owner_user_id' => $ownerUserId?->value, - ]); - } catch (DbalException $e) { - throw new \RuntimeException(sprintf('Failed to add metadata for workspace "%s" (Content Repository "%s"): %s', $workspaceName->value, $contentRepositoryId->value, $e->getMessage()), 1727084068, $e); - } - } - - private function findPrimaryWorkspaceNameForUser(ContentRepositoryId $contentRepositoryId, UserId $userId): ?WorkspaceName - { - $tableMetadata = self::TABLE_NAME_WORKSPACE_METADATA; - $query = <<dbal->fetchOne($query, [ - 'contentRepositoryId' => $contentRepositoryId->value, - 'personalWorkspaceClassification' => WorkspaceClassification::PERSONAL->value, - 'userId' => $userId->value, - ]); - return $workspaceName === false ? null : WorkspaceName::fromString($workspaceName); + $this->metadataAndRoleRepository->addWorkspaceMetadata($contentRepositoryId, $workspaceName, $title, $description, $classification, $ownerId); } private function requireWorkspace(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName): Workspace diff --git a/Neos.Neos/Classes/Security/Authorization/ContentRepositoryAuthorizationService.php b/Neos.Neos/Classes/Security/Authorization/ContentRepositoryAuthorizationService.php index 0afdb315f02..ec3c91a93f3 100644 --- a/Neos.Neos/Classes/Security/Authorization/ContentRepositoryAuthorizationService.php +++ b/Neos.Neos/Classes/Security/Authorization/ContentRepositoryAuthorizationService.php @@ -20,7 +20,7 @@ use Neos\Neos\Domain\Model\WorkspaceRole; use Neos\Neos\Domain\Model\WorkspaceRoleSubject; use Neos\Neos\Domain\Model\WorkspaceRoleSubjects; -use Neos\Neos\Domain\Service\WorkspaceService; +use Neos\Neos\Domain\Repository\WorkspaceMetadataAndRoleRepository; use Neos\Neos\Security\Authorization\Privilege\EditNodePrivilege; use Neos\Neos\Security\Authorization\Privilege\ReadNodePrivilege; use Neos\Neos\Security\Authorization\Privilege\SubtreeTagPrivilegeSubject; @@ -36,7 +36,7 @@ private const ROLE_NEOS_ADMINISTRATOR = 'Neos.Neos:Administrator'; public function __construct( - private WorkspaceService $workspaceService, + private WorkspaceMetadataAndRoleRepository $metadataAndRoleRepository, private PolicyService $policyService, private PrivilegeManagerInterface $privilegeManager, ) { @@ -50,8 +50,8 @@ public function __construct( */ public function getWorkspacePermissions(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, array $roles, UserId|null $userId): WorkspacePermissions { - $workspaceMetadata = $this->workspaceService->getWorkspaceMetadata($contentRepositoryId, $workspaceName); - if ($userId !== null && $workspaceMetadata->ownerUserId !== null && $userId->equals($workspaceMetadata->ownerUserId)) { + $workspaceMetadata = $this->metadataAndRoleRepository->loadWorkspaceMetadata($contentRepositoryId, $workspaceName); + if ($userId !== null && $workspaceMetadata?->ownerUserId !== null && $userId->equals($workspaceMetadata->ownerUserId)) { return WorkspacePermissions::all(sprintf('User with id "%s" is the owner of workspace "%s"', $userId->value, $workspaceName->value)); } $roleIdentifiers = array_map(static fn (Role $role) => $role->getIdentifier(), array_values($roles)); @@ -60,7 +60,7 @@ public function getWorkspacePermissions(ContentRepositoryId $contentRepositoryId $subjects[] = WorkspaceRoleSubject::createForUser($userId); } $userIsAdministrator = in_array(self::ROLE_NEOS_ADMINISTRATOR, $roleIdentifiers, true); - $userWorkspaceRole = $this->workspaceService->getMostPrivilegedWorkspaceRoleForSubjects($contentRepositoryId, $workspaceName, WorkspaceRoleSubjects::fromArray($subjects)); + $userWorkspaceRole = $this->metadataAndRoleRepository->getMostPrivilegedWorkspaceRoleForSubjects($contentRepositoryId, $workspaceName, WorkspaceRoleSubjects::fromArray($subjects)); if ($userWorkspaceRole === null) { if ($userIsAdministrator) { return WorkspacePermissions::manage(sprintf('User is a Neos Administrator without explicit role for workspace "%s"', $workspaceName->value)); From 4f07965b295b93d105214bac67187ec893b34a24 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 11 Nov 2024 17:52:29 +0100 Subject: [PATCH 145/214] FEATURE: Check permissions for operations inside `WorkspaceService` --- .../Domain/Service/WorkspaceService.php | 27 ++++++++- .../Bootstrap/WorkspaceServiceTrait.php | 57 +++++++++++-------- 2 files changed, 58 insertions(+), 26 deletions(-) diff --git a/Neos.Neos/Classes/Domain/Service/WorkspaceService.php b/Neos.Neos/Classes/Domain/Service/WorkspaceService.php index 9aa2c14670c..969b88f5c79 100644 --- a/Neos.Neos/Classes/Domain/Service/WorkspaceService.php +++ b/Neos.Neos/Classes/Domain/Service/WorkspaceService.php @@ -23,6 +23,8 @@ use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Annotations as Flow; +use Neos\Flow\Security\Context as SecurityContext; +use Neos\Flow\Security\Exception\AccessDeniedException; use Neos\Neos\Domain\Model\User; use Neos\Neos\Domain\Model\UserId; use Neos\Neos\Domain\Model\WorkspaceClassification; @@ -46,7 +48,10 @@ { public function __construct( private ContentRepositoryRegistry $contentRepositoryRegistry, - private WorkspaceMetadataAndRoleRepository $metadataAndRoleRepository + private WorkspaceMetadataAndRoleRepository $metadataAndRoleRepository, + private UserService $userService, + private ContentRepositoryAuthorizationService $authorizationService, + private SecurityContext $securityContext, ) { } @@ -76,6 +81,7 @@ public function getWorkspaceMetadata(ContentRepositoryId $contentRepositoryId, W */ public function setWorkspaceTitle(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, WorkspaceTitle $newWorkspaceTitle): void { + $this->requireManagementWorkspacePermission($contentRepositoryId, $workspaceName); $workspace = $this->requireWorkspace($contentRepositoryId, $workspaceName); $this->metadataAndRoleRepository->updateWorkspaceMetadata($contentRepositoryId, $workspace, title: $newWorkspaceTitle->value, description: null); } @@ -87,6 +93,7 @@ public function setWorkspaceTitle(ContentRepositoryId $contentRepositoryId, Work */ public function setWorkspaceDescription(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, WorkspaceDescription $newWorkspaceDescription): void { + $this->requireManagementWorkspacePermission($contentRepositoryId, $workspaceName); $workspace = $this->requireWorkspace($contentRepositoryId, $workspaceName); $this->metadataAndRoleRepository->updateWorkspaceMetadata($contentRepositoryId, $workspace, title: null, description: $newWorkspaceDescription->value); } @@ -184,6 +191,7 @@ public function createPersonalWorkspaceForUserIfMissing(ContentRepositoryId $con */ public function assignWorkspaceRole(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, WorkspaceRoleAssignment $assignment): void { + $this->requireManagementWorkspacePermission($contentRepositoryId, $workspaceName); $this->requireWorkspace($contentRepositoryId, $workspaceName); $this->metadataAndRoleRepository->assignWorkspaceRole($contentRepositoryId, $workspaceName, $assignment); } @@ -195,6 +203,7 @@ public function assignWorkspaceRole(ContentRepositoryId $contentRepositoryId, Wo */ public function unassignWorkspaceRole(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, WorkspaceRoleSubject $subject): void { + $this->requireManagementWorkspacePermission($contentRepositoryId, $workspaceName); $this->requireWorkspace($contentRepositoryId, $workspaceName); $this->metadataAndRoleRepository->unassignWorkspaceRole($contentRepositoryId, $workspaceName, $subject); } @@ -278,4 +287,20 @@ private function requireWorkspace(ContentRepositoryId $contentRepositoryId, Work } return $workspace; } + + private function requireManagementWorkspacePermission(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName): void + { + if ($this->securityContext->areAuthorizationChecksDisabled()) { + return; + } + $workspacePermissions = $this->authorizationService->getWorkspacePermissions( + $contentRepositoryId, + $workspaceName, + $this->securityContext->getRoles(), + $this->userService->getCurrentUser()?->getId() + ); + if (!$workspacePermissions->manage) { + throw new AccessDeniedException(sprintf('The current user does not have manage permissions for workspace "%s" in content repository "%s"', $workspaceName->value, $contentRepositoryId->value), 1731343473); + } + } } diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/WorkspaceServiceTrait.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/WorkspaceServiceTrait.php index 74ba8b77b53..5377bfc6648 100644 --- a/Neos.Neos/Tests/Behavior/Features/Bootstrap/WorkspaceServiceTrait.php +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/WorkspaceServiceTrait.php @@ -18,13 +18,12 @@ use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Command\CreateWorkspace; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; -use Neos\Flow\Security\Context; +use Neos\Flow\Security\Context as SecurityContext; use Neos\Neos\Domain\Model\UserId; use Neos\Neos\Domain\Model\WorkspaceDescription; use Neos\Neos\Domain\Model\WorkspaceRole; use Neos\Neos\Domain\Model\WorkspaceRoleAssignment; use Neos\Neos\Domain\Model\WorkspaceRoleSubject; -use Neos\Neos\Domain\Model\WorkspaceRoleSubjectType; use Neos\Neos\Domain\Model\WorkspaceTitle; use Neos\Neos\Domain\Service\UserService; use Neos\Neos\Domain\Service\WorkspaceService; @@ -132,11 +131,13 @@ public function aWorkspaceWithBaseWorkspaceExistsWithoutMetadata(string $workspa */ public function theTitleOfWorkspaceIsSetTo(string $workspaceName, string $newTitle): void { - $this->tryCatchingExceptions(fn () => $this->getObject(WorkspaceService::class)->setWorkspaceTitle( - $this->currentContentRepository->id, - WorkspaceName::fromString($workspaceName), - WorkspaceTitle::fromString($newTitle), - )); + $this->getObject(SecurityContext::class)->withoutAuthorizationChecks(fn () => + $this->tryCatchingExceptions(fn () => $this->getObject(WorkspaceService::class)->setWorkspaceTitle( + $this->currentContentRepository->id, + WorkspaceName::fromString($workspaceName), + WorkspaceTitle::fromString($newTitle), + )) + ); } /** @@ -144,11 +145,13 @@ public function theTitleOfWorkspaceIsSetTo(string $workspaceName, string $newTit */ public function theDescriptionOfWorkspaceIsSetTo(string $workspaceName, string $newDescription): void { - $this->tryCatchingExceptions(fn () => $this->getObject(WorkspaceService::class)->setWorkspaceDescription( - $this->currentContentRepository->id, - WorkspaceName::fromString($workspaceName), - WorkspaceDescription::fromString($newDescription), - )); + $this->getObject(SecurityContext::class)->withoutAuthorizationChecks(fn () => + $this->tryCatchingExceptions(fn () => $this->getObject(WorkspaceService::class)->setWorkspaceDescription( + $this->currentContentRepository->id, + WorkspaceName::fromString($workspaceName), + WorkspaceDescription::fromString($newDescription), + )) + ); } /** @@ -171,14 +174,16 @@ public function theWorkspaceShouldHaveTheFollowingMetadata($workspaceName, Table */ public function theRoleIsAssignedToWorkspaceForGroupOrUser(string $role, string $workspaceName, string $groupName = null, string $userId = null): void { - $this->tryCatchingExceptions(fn () => $this->getObject(WorkspaceService::class)->assignWorkspaceRole( - $this->currentContentRepository->id, - WorkspaceName::fromString($workspaceName), - WorkspaceRoleAssignment::create( - $groupName !== null ? WorkspaceRoleSubject::createForGroup($groupName) : WorkspaceRoleSubject::createForUser(UserId::fromString($userId)), - WorkspaceRole::from($role) - ) - )); + $this->getObject(SecurityContext::class)->withoutAuthorizationChecks(fn () => + $this->tryCatchingExceptions(fn () => $this->getObject(WorkspaceService::class)->assignWorkspaceRole( + $this->currentContentRepository->id, + WorkspaceName::fromString($workspaceName), + WorkspaceRoleAssignment::create( + $groupName !== null ? WorkspaceRoleSubject::createForGroup($groupName) : WorkspaceRoleSubject::createForUser(UserId::fromString($userId)), + WorkspaceRole::from($role) + ) + )) + ); } /** @@ -187,11 +192,13 @@ public function theRoleIsAssignedToWorkspaceForGroupOrUser(string $role, string */ public function theRoleIsUnassignedFromWorkspace(string $workspaceName, string $groupName = null, string $userId = null): void { - $this->tryCatchingExceptions(fn () => $this->getObject(WorkspaceService::class)->unassignWorkspaceRole( - $this->currentContentRepository->id, - WorkspaceName::fromString($workspaceName), - $groupName !== null ? WorkspaceRoleSubject::createForGroup($groupName) : WorkspaceRoleSubject::createForUser(UserId::fromString($userId)), - )); + $this->getObject(SecurityContext::class)->withoutAuthorizationChecks(fn () => + $this->tryCatchingExceptions(fn () => $this->getObject(WorkspaceService::class)->unassignWorkspaceRole( + $this->currentContentRepository->id, + WorkspaceName::fromString($workspaceName), + $groupName !== null ? WorkspaceRoleSubject::createForGroup($groupName) : WorkspaceRoleSubject::createForUser(UserId::fromString($userId)), + )) + ); } /** From 99eb4764ac8d4cf7d9152ffea11c616bea948874 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 11 Nov 2024 18:58:08 +0100 Subject: [PATCH 146/214] TASK: Make `pruneWorkspaceMetadata` and `pruneRoleAssignments` internal by not exposing on the `WorkspaceService` They were introduce in #5306 for the pruning, but this is a purely internal task and there is no need for this to be api for the Neos User. Also, the reason is that it cannot be protected with security easily. --- .../Classes/Command/CrCommandController.php | 6 ++++-- .../Classes/Domain/Service/WorkspaceService.php | 16 ---------------- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/Neos.Neos/Classes/Command/CrCommandController.php b/Neos.Neos/Classes/Command/CrCommandController.php index ac92121499a..83c8ce2d539 100644 --- a/Neos.Neos/Classes/Command/CrCommandController.php +++ b/Neos.Neos/Classes/Command/CrCommandController.php @@ -27,6 +27,7 @@ use Neos\Neos\Domain\Model\WorkspaceRole; use Neos\Neos\Domain\Model\WorkspaceRoleAssignment; use Neos\Neos\Domain\Model\WorkspaceTitle; +use Neos\Neos\Domain\Repository\WorkspaceMetadataAndRoleRepository; use Neos\Neos\Domain\Service\WorkspaceService; use Neos\Utility\Files; @@ -41,6 +42,7 @@ public function __construct( private readonly ProjectionReplayServiceFactory $projectionReplayServiceFactory, private readonly AssetUsageService $assetUsageService, private readonly WorkspaceService $workspaceService, + private readonly WorkspaceMetadataAndRoleRepository $workspaceMetadataAndRoleRepository, private readonly ProjectionReplayServiceFactory $projectionServiceFactory, ) { parent::__construct(); @@ -162,8 +164,8 @@ public function pruneCommand(string $contentRepository = 'default', bool $force ); // remove the workspace metadata and role assignments for this cr - $this->workspaceService->pruneRoleAssignments($contentRepositoryId); - $this->workspaceService->pruneWorkspaceMetadata($contentRepositoryId); + $this->workspaceMetadataAndRoleRepository->pruneRoleAssignments($contentRepositoryId); + $this->workspaceMetadataAndRoleRepository->pruneWorkspaceMetadata($contentRepositoryId); // reset the events table $contentStreamPruner->pruneAllWorkspacesAndContentStreamsFromEventStream(); diff --git a/Neos.Neos/Classes/Domain/Service/WorkspaceService.php b/Neos.Neos/Classes/Domain/Service/WorkspaceService.php index 969b88f5c79..07cd7f8f064 100644 --- a/Neos.Neos/Classes/Domain/Service/WorkspaceService.php +++ b/Neos.Neos/Classes/Domain/Service/WorkspaceService.php @@ -246,22 +246,6 @@ public function getUniqueWorkspaceName(ContentRepositoryId $contentRepositoryId, throw new \RuntimeException(sprintf('Failed to find unique workspace name for "%s" after %d attempts.', $candidate, $attempt - 1), 1725975479); } - /** - * Removes all workspace metadata records for the specified content repository id - */ - public function pruneWorkspaceMetadata(ContentRepositoryId $contentRepositoryId): void - { - $this->metadataAndRoleRepository->pruneWorkspaceMetadata($contentRepositoryId); - } - - /** - * Removes all workspace role assignments for the specified content repository id - */ - public function pruneRoleAssignments(ContentRepositoryId $contentRepositoryId): void - { - $this->metadataAndRoleRepository->pruneRoleAssignments($contentRepositoryId); - } - // ------------------ private function createWorkspace(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, WorkspaceTitle $title, WorkspaceDescription $description, WorkspaceName $baseWorkspaceName, UserId|null $ownerId, WorkspaceClassification $classification): void From 139f3eb20200aa9385506f89352e19d59ce813d6 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 11 Nov 2024 19:34:41 +0100 Subject: [PATCH 147/214] TASK: Adjust docs for privileg and permission v/o --- .../Classes/Feature/Security/Dto/Privilege.php | 3 +++ Neos.Neos/Classes/Domain/Model/NodePermissions.php | 4 +++- Neos.Neos/Classes/Domain/Model/WorkspacePermissions.php | 4 +++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/Feature/Security/Dto/Privilege.php b/Neos.ContentRepository.Core/Classes/Feature/Security/Dto/Privilege.php index 6712ab9279f..2cda976620d 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/Security/Dto/Privilege.php +++ b/Neos.ContentRepository.Core/Classes/Feature/Security/Dto/Privilege.php @@ -36,6 +36,9 @@ public static function denied(string $reason): self return new self(false, $reason); } + /** + * Human-readable explanation for why this privilege was evaluated + */ public function getReason(): string { return $this->reason; diff --git a/Neos.Neos/Classes/Domain/Model/NodePermissions.php b/Neos.Neos/Classes/Domain/Model/NodePermissions.php index 28300b92af7..62201162bba 100644 --- a/Neos.Neos/Classes/Domain/Model/NodePermissions.php +++ b/Neos.Neos/Classes/Domain/Model/NodePermissions.php @@ -21,7 +21,6 @@ /** * @param bool $read Permission to read data from the corresponding node * @param bool $edit Permission to edit the corresponding node - * @param string $reason Human-readable explanation for why this permission was evaluated {@see getReason()} */ private function __construct( public bool $read, @@ -53,6 +52,9 @@ public static function none(string $reason): self return new self(false, false, $reason); } + /** + * Human-readable explanation for why this permission was evaluated + */ public function getReason(): string { return $this->reason; diff --git a/Neos.Neos/Classes/Domain/Model/WorkspacePermissions.php b/Neos.Neos/Classes/Domain/Model/WorkspacePermissions.php index 5761d61a74a..67f1f970110 100644 --- a/Neos.Neos/Classes/Domain/Model/WorkspacePermissions.php +++ b/Neos.Neos/Classes/Domain/Model/WorkspacePermissions.php @@ -23,7 +23,6 @@ * @param bool $read Permission to read data from the corresponding workspace (e.g. get hold of and traverse the content graph) * @param bool $write Permission to write to the corresponding workspace, including publishing a derived workspace to it * @param bool $manage Permission to change the metadata and roles of the corresponding workspace (e.g. change description/title or add/remove workspace roles) - * @param string $reason Human-readable explanation for why this permission was evaluated {@see getReason()} */ private function __construct( public bool $read, @@ -63,6 +62,9 @@ public static function none(string $reason): self return new self(false, false, false, $reason); } + /** + * Human-readable explanation for why this permission was evaluated + */ public function getReason(): string { return $this->reason; From bf6e4e29cda80b4fd9d8c9f44589abf8f719690e Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 11 Nov 2024 19:35:18 +0100 Subject: [PATCH 148/214] TASK: inline methods in ContentRepositoryAuthorizationService instead of having them private --- .../ContentRepositoryAuthorizationService.php | 37 +++++-------------- 1 file changed, 9 insertions(+), 28 deletions(-) diff --git a/Neos.Neos/Classes/Security/Authorization/ContentRepositoryAuthorizationService.php b/Neos.Neos/Classes/Security/Authorization/ContentRepositoryAuthorizationService.php index ec3c91a93f3..6de599cbd7c 100644 --- a/Neos.Neos/Classes/Security/Authorization/ContentRepositoryAuthorizationService.php +++ b/Neos.Neos/Classes/Security/Authorization/ContentRepositoryAuthorizationService.php @@ -82,7 +82,14 @@ public function getWorkspacePermissions(ContentRepositoryId $contentRepositoryId */ public function getNodePermissions(Node $node, array $roles): NodePermissions { - return $this->nodePermissionsForRoles($node, $roles); + $subtreeTagPrivilegeSubject = new SubtreeTagPrivilegeSubject($node->tags->all(), $node->contentRepositoryId); + $readGranted = $this->privilegeManager->isGrantedForRoles($roles, ReadNodePrivilege::class, $subtreeTagPrivilegeSubject, $readReason); + $writeGranted = $this->privilegeManager->isGrantedForRoles($roles, EditNodePrivilege::class, $subtreeTagPrivilegeSubject, $writeReason); + return NodePermissions::create( + read: $readGranted, + edit: $writeGranted, + reason: $readReason . "\n" . $writeReason, + ); } /** @@ -91,17 +98,6 @@ public function getNodePermissions(Node $node, array $roles): NodePermissions * @param array $roles */ public function getVisibilityConstraints(ContentRepositoryId $contentRepositoryId, array $roles): VisibilityConstraints - { - return VisibilityConstraints::fromTagConstraints($this->tagConstraintsForRoles($contentRepositoryId, $roles)); - } - - // ------------------------------ - - - /** - * @param array $roles - */ - private function tagConstraintsForRoles(ContentRepositoryId $contentRepositoryId, array $roles): SubtreeTags { $restrictedSubtreeTags = SubtreeTags::createEmpty(); /** @var ReadNodePrivilege $privilege */ @@ -110,21 +106,6 @@ private function tagConstraintsForRoles(ContentRepositoryId $contentRepositoryId $restrictedSubtreeTags = $restrictedSubtreeTags->merge($privilege->getSubtreeTags()); } } - return $restrictedSubtreeTags; - } - - /** - * @param array $roles - */ - private function nodePermissionsForRoles(Node $node, array $roles): NodePermissions - { - $subtreeTagPrivilegeSubject = new SubtreeTagPrivilegeSubject($node->tags->all(), $node->contentRepositoryId); - $readGranted = $this->privilegeManager->isGrantedForRoles($roles, ReadNodePrivilege::class, $subtreeTagPrivilegeSubject, $readReason); - $writeGranted = $this->privilegeManager->isGrantedForRoles($roles, EditNodePrivilege::class, $subtreeTagPrivilegeSubject, $writeReason); - return NodePermissions::create( - read: $readGranted, - edit: $writeGranted, - reason: $readReason . "\n" . $writeReason, - ); + return VisibilityConstraints::fromTagConstraints($restrictedSubtreeTags); } } From 80d4c31503a37a02529d28d797c05f4fac1892c8 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 11 Nov 2024 19:35:33 +0100 Subject: [PATCH 149/214] TASK: Remove obsolete warnings in docs --- Neos.Neos/Classes/Domain/Service/WorkspaceService.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Neos.Neos/Classes/Domain/Service/WorkspaceService.php b/Neos.Neos/Classes/Domain/Service/WorkspaceService.php index 07cd7f8f064..105a3126bff 100644 --- a/Neos.Neos/Classes/Domain/Service/WorkspaceService.php +++ b/Neos.Neos/Classes/Domain/Service/WorkspaceService.php @@ -76,8 +76,6 @@ public function getWorkspaceMetadata(ContentRepositoryId $contentRepositoryId, W /** * Update/set title metadata for the specified workspace - * - * NOTE: The workspace privileges are not evaluated for this interaction, this should be done in the calling side if needed */ public function setWorkspaceTitle(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, WorkspaceTitle $newWorkspaceTitle): void { @@ -88,8 +86,6 @@ public function setWorkspaceTitle(ContentRepositoryId $contentRepositoryId, Work /** * Update/set description metadata for the specified workspace - * - * NOTE: The workspace privileges are not evaluated for this interaction, this should be done in the calling side if needed */ public function setWorkspaceDescription(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, WorkspaceDescription $newWorkspaceDescription): void { From 843ceda60d2bae856437dcd0aed338692ad572ff Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 11 Nov 2024 20:24:45 +0100 Subject: [PATCH 150/214] TASK: Fix cli output of `workspace:assignrole` to not throw error --- Neos.Neos/Classes/Command/WorkspaceCommandController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Neos.Neos/Classes/Command/WorkspaceCommandController.php b/Neos.Neos/Classes/Command/WorkspaceCommandController.php index c4ff1597e03..876e5b8d199 100644 --- a/Neos.Neos/Classes/Command/WorkspaceCommandController.php +++ b/Neos.Neos/Classes/Command/WorkspaceCommandController.php @@ -299,7 +299,7 @@ public function assignRoleCommand(string $workspace, string $subject, string $ro $workspaceRole ) ); - $this->outputLine('Assigned role "%s" to subject "%s" for workspace "%s"', [$workspaceRole->value, $roleSubject, $workspaceName->value]); + $this->outputLine('Assigned role "%s" to subject "%s" for workspace "%s"', [$workspaceRole->value, $roleSubject->value, $workspaceName->value]); } /** From abd32d33ce8e3caf407b91799b60584d05582055 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 11 Nov 2024 20:25:16 +0100 Subject: [PATCH 151/214] TASK: Declare `AbstractSubtreeTagBasedPrivilege` internal as it doesnt make sense to be extended --- .../Privilege/AbstractSubtreeTagBasedPrivilege.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Neos.Neos/Classes/Security/Authorization/Privilege/AbstractSubtreeTagBasedPrivilege.php b/Neos.Neos/Classes/Security/Authorization/Privilege/AbstractSubtreeTagBasedPrivilege.php index e42051d4bc9..227cb75d6aa 100644 --- a/Neos.Neos/Classes/Security/Authorization/Privilege/AbstractSubtreeTagBasedPrivilege.php +++ b/Neos.Neos/Classes/Security/Authorization/Privilege/AbstractSubtreeTagBasedPrivilege.php @@ -22,7 +22,7 @@ /** * Common base class for privileges that evaluate {@see SubtreeTagPrivilegeSubject}s - * @see ReadNodePrivilege, EditNodePrivilege + * @internal the public API is {@see ReadNodePrivilege, EditNodePrivilege} */ abstract class AbstractSubtreeTagBasedPrivilege extends AbstractPrivilege { From 930266df5197653bcb8fde2d3a191ed9658738bc Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 11 Nov 2024 21:00:27 +0100 Subject: [PATCH 152/214] TASK: Remove hacky method again Chances that its okay to just be interested in the first dsp are low :D --- .../Classes/DimensionSpace/DimensionSpacePointSet.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/DimensionSpace/DimensionSpacePointSet.php b/Neos.ContentRepository.Core/Classes/DimensionSpace/DimensionSpacePointSet.php index 65d19b1e04c..191dcd2d7f6 100644 --- a/Neos.ContentRepository.Core/Classes/DimensionSpace/DimensionSpacePointSet.php +++ b/Neos.ContentRepository.Core/Classes/DimensionSpace/DimensionSpacePointSet.php @@ -148,14 +148,6 @@ public function equals(DimensionSpacePointSet $other): bool return $thisPointHashes === $otherPointHashes; } - public function getFirst(): ?DimensionSpacePoint - { - foreach ($this->points as $point) { - return $point; - } - return null; - } - public function getIterator(): \Traversable { yield from $this->points; From 5733ac5294799fbc13a592ee68dfeaf14cf3d355 Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Mon, 11 Nov 2024 21:57:41 +0100 Subject: [PATCH 153/214] TASK: Unify command and event dispatching in Behat tests --- ...ggregateWithNode_WithoutDimensions.feature | 4 +- .../CommandHandler/CommandInterface.php | 6 +- .../RebasableToOtherWorkspaceInterface.php | 6 - .../Command/MoveDimensionSpacePoint.php | 3 - .../Command/CreateNodeAggregateWithNode.php | 28 +++ ...gregateWithNodeAndSerializedProperties.php | 3 - .../Command/DisableNodeAggregate.php | 3 - .../Command/EnableNodeAggregate.php | 3 - .../Command/CopyNodesRecursively.php | 3 - .../Command/SetNodeProperties.php | 10 + .../Command/SetSerializedNodeProperties.php | 3 - .../NodeMove/Command/MoveNodeAggregate.php | 7 +- .../Dto/RelationDistributionStrategy.php | 6 +- .../Command/SetNodeReferences.php | 11 + .../Command/SetSerializedNodeReferences.php | 3 - .../Command/RemoveNodeAggregate.php | 3 - .../Command/ChangeNodeAggregateType.php | 3 - .../Command/CreateNodeVariant.php | 3 - .../CreateRootNodeAggregateWithNode.php | 3 - .../UpdateRootNodeAggregateDimensions.php | 3 - .../SubtreeTagging/Command/TagSubtree.php | 3 - .../SubtreeTagging/Command/UntagSubtree.php | 3 - .../Command/CreateRootWorkspace.php | 8 + .../Command/CreateWorkspace.php | 9 + .../Command/ChangeBaseWorkspace.php | 9 + .../Command/DeleteWorkspace.php | 8 + .../DiscardIndividualNodesFromWorkspace.php | 9 + .../Command/DiscardWorkspace.php | 8 + .../PublishIndividualNodesFromWorkspace.php | 9 + .../Command/PublishWorkspace.php | 8 + .../Command/RebaseWorkspace.php | 9 + .../Features/Bootstrap/CRTestSuiteTrait.php | 10 - .../Bootstrap/Features/NodeCreation.php | 210 +--------------- .../Bootstrap/Features/NodeDisabling.php | 113 --------- .../Bootstrap/Features/NodeModification.php | 44 ---- .../Features/Bootstrap/Features/NodeMove.php | 53 ----- .../Bootstrap/Features/NodeReferencing.php | 65 ----- .../Bootstrap/Features/NodeRemoval.php | 47 ---- .../Bootstrap/Features/NodeRenaming.php | 39 --- .../Bootstrap/Features/NodeTypeChange.php | 72 ------ .../Bootstrap/Features/NodeVariation.php | 70 ------ .../Bootstrap/Features/SubtreeTagging.php | 85 ------- .../Bootstrap/Features/WorkspaceCreation.php | 88 ------- .../Features/WorkspaceDiscarding.php | 85 ------- .../Features/WorkspacePublishing.php | 131 ---------- ...ricCommandExecutionAndEventPublication.php | 224 ++++++++++++++---- 46 files changed, 323 insertions(+), 1210 deletions(-) delete mode 100644 Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeDisabling.php delete mode 100644 Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeTypeChange.php delete mode 100644 Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeVariation.php delete mode 100644 Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/WorkspaceDiscarding.php delete mode 100644 Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/WorkspacePublishing.php diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/03-CreateNodeAggregateWithNode_WithoutDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/03-CreateNodeAggregateWithNode_WithoutDimensions.feature index 076f8d72e2e..02aa8fb61b8 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/03-CreateNodeAggregateWithNode_WithoutDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/03-CreateNodeAggregateWithNode_WithoutDimensions.feature @@ -202,14 +202,14 @@ Feature: Create node aggregate with node | nodeAggregateId | "lady-eleonode-rootford" | | nodeTypeName | "Neos.ContentRepository:Root" | - Given the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: + Given the command CreateNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "sir-david-nodenborough" | | nodeTypeName | "Neos.ContentRepository.Testing:NodeWithoutTetheredChildNodes" | | originDimensionSpacePoint | {} | | parentNodeAggregateId | "lady-eleonode-rootford" | | nodeName | "node" | - And the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: + And the command CreateNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "sir-nodeward-nodington-iii" | | nodeTypeName | "Neos.ContentRepository.Testing:NodeWithoutTetheredChildNodes" | diff --git a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandInterface.php b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandInterface.php index 8264248a6f9..fb36c3f6d11 100644 --- a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandInterface.php +++ b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandInterface.php @@ -5,10 +5,14 @@ namespace Neos\ContentRepository\Core\CommandHandler; /** - * Common (marker) interface for all commands of the Content Repository + * Common interface for all commands of the Content Repository * * @internal because extra commands are no extension point */ interface CommandInterface { + /** + * @param array $array + */ + public static function fromArray(array $array): self; } diff --git a/Neos.ContentRepository.Core/Classes/Feature/Common/RebasableToOtherWorkspaceInterface.php b/Neos.ContentRepository.Core/Classes/Feature/Common/RebasableToOtherWorkspaceInterface.php index c7b3d5cbc59..daf152f5781 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/Common/RebasableToOtherWorkspaceInterface.php +++ b/Neos.ContentRepository.Core/Classes/Feature/Common/RebasableToOtherWorkspaceInterface.php @@ -30,10 +30,4 @@ interface RebasableToOtherWorkspaceInterface extends CommandInterface public function createCopyForWorkspace( WorkspaceName $targetWorkspaceName, ): self; - - /** - * called during deserialization from metadata - * @param array $array - */ - public static function fromArray(array $array): self; } diff --git a/Neos.ContentRepository.Core/Classes/Feature/DimensionSpaceAdjustment/Command/MoveDimensionSpacePoint.php b/Neos.ContentRepository.Core/Classes/Feature/DimensionSpaceAdjustment/Command/MoveDimensionSpacePoint.php index 498ba261e49..2862312256b 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/DimensionSpaceAdjustment/Command/MoveDimensionSpacePoint.php +++ b/Neos.ContentRepository.Core/Classes/Feature/DimensionSpaceAdjustment/Command/MoveDimensionSpacePoint.php @@ -58,9 +58,6 @@ public static function create( return new self($workspaceName, $source, $target); } - /** - * @param array $array - */ public static function fromArray(array $array): self { return new self( diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/Command/CreateNodeAggregateWithNode.php b/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/Command/CreateNodeAggregateWithNode.php index 4aac2bb8761..ebca78299b4 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/Command/CreateNodeAggregateWithNode.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/Command/CreateNodeAggregateWithNode.php @@ -18,7 +18,9 @@ use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; use Neos\ContentRepository\Core\Feature\NodeCreation\Dto\NodeAggregateIdsByNodePaths; use Neos\ContentRepository\Core\Feature\NodeModification\Dto\PropertyValuesToWrite; +use Neos\ContentRepository\Core\Feature\NodeModification\Dto\SerializedPropertyValues; use Neos\ContentRepository\Core\Feature\NodeReferencing\Dto\NodeReferencesToWrite; +use Neos\ContentRepository\Core\Feature\NodeReferencing\Dto\SerializedNodeReferences; use Neos\ContentRepository\Core\NodeType\NodeTypeName; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentSubgraphInterface; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; @@ -76,6 +78,32 @@ public static function create(WorkspaceName $workspaceName, NodeAggregateId $nod return new self($workspaceName, $nodeAggregateId, $nodeTypeName, $originDimensionSpacePoint, $parentNodeAggregateId, $initialPropertyValues ?: PropertyValuesToWrite::createEmpty(), $succeedingSiblingNodeAggregateId, null, NodeAggregateIdsByNodePaths::createEmpty(), $references ?: NodeReferencesToWrite::createEmpty()); } + public static function fromArray(array $array): self + { + return new self( + WorkspaceName::fromString($array['workspaceName']), + NodeAggregateId::fromString($array['nodeAggregateId']), + NodeTypeName::fromString($array['nodeTypeName']), + isset($array['originDimensionSpacePoint']) + ? OriginDimensionSpacePoint::fromArray($array['originDimensionSpacePoint']) + : OriginDimensionSpacePoint::createWithoutDimensions(), + NodeAggregateId::fromString($array['parentNodeAggregateId']), + isset($array['initialPropertyValues']) + ? PropertyValuesToWrite::fromArray($array['initialPropertyValues']) + : PropertyValuesToWrite::createEmpty(), + isset($array['succeedingSiblingNodeAggregateId']) + ? NodeAggregateId::fromString($array['succeedingSiblingNodeAggregateId']) + : null, + isset($array['nodeName']) + ? NodeName::fromString($array['nodeName']) + : null, + isset($array['tetheredDescendantNodeAggregateIds']) + ? NodeAggregateIdsByNodePaths::fromArray($array['tetheredDescendantNodeAggregateIds']) + : NodeAggregateIdsByNodePaths::createEmpty(), + isset($array['references']) ? NodeReferencesToWrite::fromArray($array['references']) : NodeReferencesToWrite::createEmpty(), + ); + } + public function withInitialPropertyValues(PropertyValuesToWrite $newInitialPropertyValues): self { return new self( diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/Command/CreateNodeAggregateWithNodeAndSerializedProperties.php b/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/Command/CreateNodeAggregateWithNodeAndSerializedProperties.php index 958b1a30590..ddff2d673d6 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/Command/CreateNodeAggregateWithNodeAndSerializedProperties.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/Command/CreateNodeAggregateWithNodeAndSerializedProperties.php @@ -80,9 +80,6 @@ public static function create(WorkspaceName $workspaceName, NodeAggregateId $nod return new self($workspaceName, $nodeAggregateId, $nodeTypeName, $originDimensionSpacePoint, $parentNodeAggregateId, $initialPropertyValues ?? SerializedPropertyValues::createEmpty(), $succeedingSiblingNodeAggregateId, null, NodeAggregateIdsByNodePaths::createEmpty(), $references ?: SerializedNodeReferences::createEmpty()); } - /** - * @param array $array - */ public static function fromArray(array $array): self { return new self( diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/Command/DisableNodeAggregate.php b/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/Command/DisableNodeAggregate.php index f92f938df9c..90c725e6a69 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/Command/DisableNodeAggregate.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/Command/DisableNodeAggregate.php @@ -60,9 +60,6 @@ public static function create(WorkspaceName $workspaceName, NodeAggregateId $nod return new self($workspaceName, $nodeAggregateId, $coveredDimensionSpacePoint, $nodeVariantSelectionStrategy); } - /** - * @param array $array - */ public static function fromArray(array $array): self { return new self( diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/Command/EnableNodeAggregate.php b/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/Command/EnableNodeAggregate.php index de0ad11d57d..37c8407b0c7 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/Command/EnableNodeAggregate.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/Command/EnableNodeAggregate.php @@ -60,9 +60,6 @@ public static function create(WorkspaceName $workspaceName, NodeAggregateId $nod return new self($workspaceName, $nodeAggregateId, $coveredDimensionSpacePoint, $nodeVariantSelectionStrategy); } - /** - * @param array $array - */ public static function fromArray(array $array): self { return new self( diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/Command/CopyNodesRecursively.php b/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/Command/CopyNodesRecursively.php index ba31a07c601..e655014cd0f 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/Command/CopyNodesRecursively.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/Command/CopyNodesRecursively.php @@ -100,9 +100,6 @@ public static function createFromSubgraphAndStartNode( ); } - /** - * @param array $array - */ public static function fromArray(array $array): self { return new self( diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Command/SetNodeProperties.php b/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Command/SetNodeProperties.php index e2a3d63f587..bb978e7e948 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Command/SetNodeProperties.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Command/SetNodeProperties.php @@ -60,4 +60,14 @@ public static function create(WorkspaceName $workspaceName, NodeAggregateId $nod { return new self($workspaceName, $nodeAggregateId, $originDimensionSpacePoint, $propertyValues); } + + public static function fromArray(array $array): self + { + return new self( + WorkspaceName::fromString($array['workspaceName']), + NodeAggregateId::fromString($array['nodeAggregateId']), + OriginDimensionSpacePoint::fromArray($array['originDimensionSpacePoint']), + PropertyValuesToWrite::fromArray($array['propertyValues']), + ); + } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Command/SetSerializedNodeProperties.php b/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Command/SetSerializedNodeProperties.php index a2d684f6035..f9ca77329d7 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Command/SetSerializedNodeProperties.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Command/SetSerializedNodeProperties.php @@ -77,9 +77,6 @@ public static function create( ); } - /** - * @param array $array - */ public static function fromArray(array $array): self { return new self( diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeMove/Command/MoveNodeAggregate.php b/Neos.ContentRepository.Core/Classes/Feature/NodeMove/Command/MoveNodeAggregate.php index 3a756163488..4a657dc507f 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeMove/Command/MoveNodeAggregate.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeMove/Command/MoveNodeAggregate.php @@ -82,16 +82,15 @@ public static function create(WorkspaceName $workspaceName, DimensionSpacePoint return new self($workspaceName, $dimensionSpacePoint, $nodeAggregateId, $relationDistributionStrategy, $newParentNodeAggregateId, $newPrecedingSiblingNodeAggregateId, $newSucceedingSiblingNodeAggregateId); } - /** - * @param array $array - */ public static function fromArray(array $array): self { return new self( WorkspaceName::fromString($array['workspaceName']), DimensionSpacePoint::fromArray($array['dimensionSpacePoint']), NodeAggregateId::fromString($array['nodeAggregateId']), - RelationDistributionStrategy::fromString($array['relationDistributionStrategy']), + isset($array['relationDistributionStrategy']) + ? RelationDistributionStrategy::from($array['relationDistributionStrategy']) + : RelationDistributionStrategy::default(), isset($array['newParentNodeAggregateId']) ? NodeAggregateId::fromString($array['newParentNodeAggregateId']) : null, diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeMove/Dto/RelationDistributionStrategy.php b/Neos.ContentRepository.Core/Classes/Feature/NodeMove/Dto/RelationDistributionStrategy.php index 7b01ee90ce6..8ca1fd33f08 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeMove/Dto/RelationDistributionStrategy.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeMove/Dto/RelationDistributionStrategy.php @@ -34,11 +34,9 @@ enum RelationDistributionStrategy: string implements \JsonSerializable case STRATEGY_GATHER_ALL = 'gatherAll'; case STRATEGY_GATHER_SPECIALIZATIONS = 'gatherSpecializations'; - public static function fromString(?string $serialization): self + public static function default(): self { - return !is_null($serialization) - ? self::from($serialization) - : self::STRATEGY_GATHER_ALL; + return self::STRATEGY_GATHER_ALL; } public function jsonSerialize(): string diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Command/SetNodeReferences.php b/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Command/SetNodeReferences.php index 70da56b2c2d..30424548e8f 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Command/SetNodeReferences.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Command/SetNodeReferences.php @@ -6,6 +6,7 @@ use Neos\ContentRepository\Core\CommandHandler\CommandInterface; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; +use Neos\ContentRepository\Core\Feature\NodeModification\Dto\PropertyValuesToWrite; use Neos\ContentRepository\Core\Feature\NodeReferencing\Dto\NodeReferencesToWrite; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Node\ReferenceName; @@ -47,4 +48,14 @@ public static function create(WorkspaceName $workspaceName, NodeAggregateId $sou { return new self($workspaceName, $sourceNodeAggregateId, $sourceOriginDimensionSpacePoint, $references); } + + public static function fromArray(array $array): self + { + return new self( + WorkspaceName::fromString($array['workspaceName']), + NodeAggregateId::fromString($array['sourceNodeAggregateId']), + OriginDimensionSpacePoint::fromArray($array['sourceOriginDimensionSpacePoint']), + NodeReferencesToWrite::fromArray($array['references']), + ); + } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Command/SetSerializedNodeReferences.php b/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Command/SetSerializedNodeReferences.php index ae96cc5af13..ef2b5b7ed1e 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Command/SetSerializedNodeReferences.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Command/SetSerializedNodeReferences.php @@ -61,9 +61,6 @@ public static function create(WorkspaceName $workspaceName, NodeAggregateId $sou return new self($workspaceName, $sourceNodeAggregateId, $sourceOriginDimensionSpacePoint, $references); } - /** - * @param array $array - */ public static function fromArray(array $array): self { return new self( diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeRemoval/Command/RemoveNodeAggregate.php b/Neos.ContentRepository.Core/Classes/Feature/NodeRemoval/Command/RemoveNodeAggregate.php index 085af255b8c..92292bd34da 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeRemoval/Command/RemoveNodeAggregate.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeRemoval/Command/RemoveNodeAggregate.php @@ -59,9 +59,6 @@ public static function create(WorkspaceName $workspaceName, NodeAggregateId $nod return new self($workspaceName, $nodeAggregateId, $coveredDimensionSpacePoint, $nodeVariantSelectionStrategy, null); } - /** - * @param array $array - */ public static function fromArray(array $array): self { return new self( diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeTypeChange/Command/ChangeNodeAggregateType.php b/Neos.ContentRepository.Core/Classes/Feature/NodeTypeChange/Command/ChangeNodeAggregateType.php index e979e4d25c1..4a13da56621 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeTypeChange/Command/ChangeNodeAggregateType.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeTypeChange/Command/ChangeNodeAggregateType.php @@ -61,9 +61,6 @@ public static function create(WorkspaceName $workspaceName, NodeAggregateId $nod return new self($workspaceName, $nodeAggregateId, $newNodeTypeName, $strategy, NodeAggregateIdsByNodePaths::createEmpty()); } - /** - * @param array $array - */ public static function fromArray(array $array): self { return new self( diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeVariation/Command/CreateNodeVariant.php b/Neos.ContentRepository.Core/Classes/Feature/NodeVariation/Command/CreateNodeVariant.php index 54873f489dc..2064e99e0b2 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeVariation/Command/CreateNodeVariant.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeVariation/Command/CreateNodeVariant.php @@ -60,9 +60,6 @@ public static function create(WorkspaceName $workspaceName, NodeAggregateId $nod return new self($workspaceName, $nodeAggregateId, $sourceOrigin, $targetOrigin); } - /** - * @param array $array - */ public static function fromArray(array $array): self { return new self( diff --git a/Neos.ContentRepository.Core/Classes/Feature/RootNodeCreation/Command/CreateRootNodeAggregateWithNode.php b/Neos.ContentRepository.Core/Classes/Feature/RootNodeCreation/Command/CreateRootNodeAggregateWithNode.php index 9bbb1f320cc..27c946fb04c 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/RootNodeCreation/Command/CreateRootNodeAggregateWithNode.php +++ b/Neos.ContentRepository.Core/Classes/Feature/RootNodeCreation/Command/CreateRootNodeAggregateWithNode.php @@ -102,9 +102,6 @@ public function withTetheredDescendantNodeAggregateIds(NodeAggregateIdsByNodePat ); } - /** - * @param array $array - */ public static function fromArray(array $array): self { return new self( diff --git a/Neos.ContentRepository.Core/Classes/Feature/RootNodeCreation/Command/UpdateRootNodeAggregateDimensions.php b/Neos.ContentRepository.Core/Classes/Feature/RootNodeCreation/Command/UpdateRootNodeAggregateDimensions.php index 302c05ed895..fa4c9a42158 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/RootNodeCreation/Command/UpdateRootNodeAggregateDimensions.php +++ b/Neos.ContentRepository.Core/Classes/Feature/RootNodeCreation/Command/UpdateRootNodeAggregateDimensions.php @@ -51,9 +51,6 @@ public static function create(WorkspaceName $workspaceName, NodeAggregateId $nod return new self($workspaceName, $nodeAggregateId); } - /** - * @param array $array - */ public static function fromArray(array $array): self { return new self( diff --git a/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Command/TagSubtree.php b/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Command/TagSubtree.php index d4d37c8b8cb..8bff783394c 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Command/TagSubtree.php +++ b/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Command/TagSubtree.php @@ -64,9 +64,6 @@ public static function create(WorkspaceName $workspaceName, NodeAggregateId $nod return new self($workspaceName, $nodeAggregateId, $coveredDimensionSpacePoint, $nodeVariantSelectionStrategy, $tag); } - /** - * @param array $array - */ public static function fromArray(array $array): self { return new self( diff --git a/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Command/UntagSubtree.php b/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Command/UntagSubtree.php index 1ae9b4624a2..6ab11324feb 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Command/UntagSubtree.php +++ b/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Command/UntagSubtree.php @@ -65,9 +65,6 @@ public static function create(WorkspaceName $workspaceName, NodeAggregateId $nod return new self($workspaceName, $nodeAggregateId, $coveredDimensionSpacePoint, $nodeVariantSelectionStrategy, $tag); } - /** - * @param array $array - */ public static function fromArray(array $array): self { return new self( diff --git a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCreation/Command/CreateRootWorkspace.php b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCreation/Command/CreateRootWorkspace.php index 2a36c7655cb..c6b30e77711 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCreation/Command/CreateRootWorkspace.php +++ b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCreation/Command/CreateRootWorkspace.php @@ -45,4 +45,12 @@ public static function create(WorkspaceName $workspaceName, ContentStreamId $new { return new self($workspaceName, $newContentStreamId); } + + public static function fromArray(array $array): self + { + return new self( + WorkspaceName::fromString($array['workspaceName']), + ContentStreamId::fromString($array['newContentStreamId']), + ); + } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCreation/Command/CreateWorkspace.php b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCreation/Command/CreateWorkspace.php index 4add3ccf28f..7c329e86f37 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCreation/Command/CreateWorkspace.php +++ b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCreation/Command/CreateWorkspace.php @@ -46,4 +46,13 @@ public static function create(WorkspaceName $workspaceName, WorkspaceName $baseW { return new self($workspaceName, $baseWorkspaceName, $newContentStreamId); } + + public static function fromArray(array $array): self + { + return new self( + WorkspaceName::fromString($array['workspaceName']), + WorkspaceName::fromString($array['baseWorkspaceName']), + ContentStreamId::fromString($array['newContentStreamId']), + ); + } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceModification/Command/ChangeBaseWorkspace.php b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceModification/Command/ChangeBaseWorkspace.php index dd833312b5f..3bf1ace5b50 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceModification/Command/ChangeBaseWorkspace.php +++ b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceModification/Command/ChangeBaseWorkspace.php @@ -36,6 +36,15 @@ public static function create(WorkspaceName $workspaceName, WorkspaceName $baseW return new self($workspaceName, $baseWorkspaceName, ContentStreamId::create()); } + public static function fromArray(array $array): self + { + return new self( + WorkspaceName::fromString($array['workspaceName']), + WorkspaceName::fromString($array['baseWorkspaceName']), + isset($array['newContentStreamId']) ? ContentStreamId::fromString($array['newContentStreamId']) : ContentStreamId::create(), + ); + } + /** * During the publish process, we create a new content stream. * diff --git a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceModification/Command/DeleteWorkspace.php b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceModification/Command/DeleteWorkspace.php index ab182194ba0..3cd87fb25b6 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceModification/Command/DeleteWorkspace.php +++ b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceModification/Command/DeleteWorkspace.php @@ -5,6 +5,7 @@ namespace Neos\ContentRepository\Core\Feature\WorkspaceModification\Command; use Neos\ContentRepository\Core\CommandHandler\CommandInterface; +use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; /** @@ -29,4 +30,11 @@ public static function create(WorkspaceName $workspaceName): self { return new self($workspaceName); } + + public static function fromArray(array $array): self + { + return new self( + WorkspaceName::fromString($array['workspaceName']), + ); + } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/WorkspacePublication/Command/DiscardIndividualNodesFromWorkspace.php b/Neos.ContentRepository.Core/Classes/Feature/WorkspacePublication/Command/DiscardIndividualNodesFromWorkspace.php index fec436be434..7f6a2915c3d 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/WorkspacePublication/Command/DiscardIndividualNodesFromWorkspace.php +++ b/Neos.ContentRepository.Core/Classes/Feature/WorkspacePublication/Command/DiscardIndividualNodesFromWorkspace.php @@ -53,6 +53,15 @@ public static function create( ); } + public static function fromArray(array $array): self + { + return new self( + WorkspaceName::fromString($array['workspaceName']), + NodeIdsToPublishOrDiscard::fromArray($array['nodesToDiscard']), + isset($array['newContentStreamId']) ? ContentStreamId::fromString($array['newContentStreamId']) : ContentStreamId::create(), + ); + } + /** * Call this method if you want to run this command fully deterministically, f.e. during test cases */ diff --git a/Neos.ContentRepository.Core/Classes/Feature/WorkspacePublication/Command/DiscardWorkspace.php b/Neos.ContentRepository.Core/Classes/Feature/WorkspacePublication/Command/DiscardWorkspace.php index 5a5b0355945..e62465e8627 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/WorkspacePublication/Command/DiscardWorkspace.php +++ b/Neos.ContentRepository.Core/Classes/Feature/WorkspacePublication/Command/DiscardWorkspace.php @@ -43,6 +43,14 @@ public static function create(WorkspaceName $workspaceName): self return new self($workspaceName, ContentStreamId::create()); } + public static function fromArray(array $array): self + { + return new self( + WorkspaceName::fromString($array['workspaceName']), + isset($array['newContentStreamId']) ? ContentStreamId::fromString($array['newContentStreamId']) : ContentStreamId::create(), + ); + } + /** * Call this method if you want to run this command fully deterministically, f.e. during test cases */ diff --git a/Neos.ContentRepository.Core/Classes/Feature/WorkspacePublication/Command/PublishIndividualNodesFromWorkspace.php b/Neos.ContentRepository.Core/Classes/Feature/WorkspacePublication/Command/PublishIndividualNodesFromWorkspace.php index 2394c80ab99..7f9cf111dd8 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/WorkspacePublication/Command/PublishIndividualNodesFromWorkspace.php +++ b/Neos.ContentRepository.Core/Classes/Feature/WorkspacePublication/Command/PublishIndividualNodesFromWorkspace.php @@ -51,6 +51,15 @@ public static function create(WorkspaceName $workspaceName, NodeIdsToPublishOrDi ); } + public static function fromArray(array $array): self + { + return new self( + WorkspaceName::fromString($array['workspaceName']), + NodeIdsToPublishOrDiscard::fromArray($array['nodesToPublish']), + isset($array['contentStreamIdForRemainingPart']) ? ContentStreamId::fromString($array['contentStreamIdForRemainingPart']) : ContentStreamId::create(), + ); + } + /** * The id of the new content stream that will contain all remaining events * diff --git a/Neos.ContentRepository.Core/Classes/Feature/WorkspacePublication/Command/PublishWorkspace.php b/Neos.ContentRepository.Core/Classes/Feature/WorkspacePublication/Command/PublishWorkspace.php index 5364d13054d..fa76f2caa03 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/WorkspacePublication/Command/PublishWorkspace.php +++ b/Neos.ContentRepository.Core/Classes/Feature/WorkspacePublication/Command/PublishWorkspace.php @@ -34,6 +34,14 @@ private function __construct( ) { } + public static function fromArray(array $array): self + { + return new self( + WorkspaceName::fromString($array['workspaceName']), + isset($array['newContentStreamId']) ? ContentStreamId::fromString($array['newContentStreamId']) : ContentStreamId::create(), + ); + } + /** * During the publish process, we create a new content stream. * diff --git a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/Command/RebaseWorkspace.php b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/Command/RebaseWorkspace.php index 5e9aa2cbc91..d9440c74a9d 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/Command/RebaseWorkspace.php +++ b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/Command/RebaseWorkspace.php @@ -42,6 +42,15 @@ public static function create(WorkspaceName $workspaceName): self return new self($workspaceName, ContentStreamId::create(), RebaseErrorHandlingStrategy::STRATEGY_FAIL); } + public static function fromArray(array $array): self + { + return new self( + WorkspaceName::fromString($array['workspaceName']), + isset($array['rebasedContentStreamId']) ? ContentStreamId::fromString($array['rebasedContentStreamId']) : ContentStreamId::create(), + isset($array['rebaseErrorHandlingStrategy']) ? RebaseErrorHandlingStrategy::from($array['rebaseErrorHandlingStrategy']) : RebaseErrorHandlingStrategy::STRATEGY_FAIL, + ); + } + /** * Call this method if you want to run this command fully deterministically, f.e. during test cases */ diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteTrait.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteTrait.php index 18fddc56f68..c22577dc67d 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteTrait.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteTrait.php @@ -35,18 +35,13 @@ use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Features\ContentStreamClosing; use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Features\NodeCopying; use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Features\NodeCreation; -use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Features\NodeDisabling; use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Features\NodeModification; use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Features\NodeMove; use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Features\NodeReferencing; use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Features\NodeRemoval; use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Features\NodeRenaming; -use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Features\NodeTypeChange; -use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Features\NodeVariation; use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Features\SubtreeTagging; use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Features\WorkspaceCreation; -use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Features\WorkspaceDiscarding; -use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Features\WorkspacePublishing; use Neos\EventStore\EventStoreInterface; use PHPUnit\Framework\Assert; @@ -67,19 +62,14 @@ trait CRTestSuiteTrait use NodeCreation; use NodeCopying; - use NodeDisabling; use SubtreeTagging; use NodeModification; use NodeMove; use NodeReferencing; use NodeRemoval; use NodeRenaming; - use NodeTypeChange; - use NodeVariation; use WorkspaceCreation; - use WorkspaceDiscarding; - use WorkspacePublishing; /** * @BeforeScenario diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeCreation.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeCreation.php index fc6f5eb0fd2..850a9bca3d8 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeCreation.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeCreation.php @@ -22,7 +22,6 @@ use Neos\ContentRepository\Core\Feature\NodeCreation\Dto\NodeAggregateIdsByNodePaths; use Neos\ContentRepository\Core\Feature\NodeModification\Dto\PropertyValuesToWrite; use Neos\ContentRepository\Core\Feature\NodeModification\Dto\SerializedPropertyValues; -use Neos\ContentRepository\Core\Feature\RootNodeCreation\Command\CreateRootNodeAggregateWithNode; use Neos\ContentRepository\Core\Feature\RootNodeCreation\Command\UpdateRootNodeAggregateDimensions; use Neos\ContentRepository\Core\NodeType\NodeTypeName; use Neos\ContentRepository\Core\SharedModel\Exception\ContentStreamDoesNotExistYet; @@ -31,6 +30,7 @@ use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\CRTestSuiteRuntimeVariables; +use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\GenericCommandExecutionAndEventPublication; use Neos\EventStore\Model\Event\StreamName; /** @@ -46,46 +46,6 @@ abstract protected function readPayloadTable(TableNode $payloadTable): array; abstract protected function publishEvent(string $eventType, StreamName $streamName, array $eventPayload): void; - /** - * @When /^the command CreateRootNodeAggregateWithNode is executed with payload:$/ - * @param TableNode $payloadTable - * @throws ContentStreamDoesNotExistYet - * @throws \Exception - */ - public function theCommandCreateRootNodeAggregateWithNodeIsExecutedWithPayload(TableNode $payloadTable) - { - $commandArguments = $this->readPayloadTable($payloadTable); - $workspaceName = isset($commandArguments['workspaceName']) - ? WorkspaceName::fromString($commandArguments['workspaceName']) - : $this->currentWorkspaceName; - $nodeAggregateId = NodeAggregateId::fromString($commandArguments['nodeAggregateId']); - - $command = CreateRootNodeAggregateWithNode::create( - $workspaceName, - $nodeAggregateId, - NodeTypeName::fromString($commandArguments['nodeTypeName']), - ); - if (isset($commandArguments['tetheredDescendantNodeAggregateIds'])) { - $command = $command->withTetheredDescendantNodeAggregateIds(NodeAggregateIdsByNodePaths::fromArray($commandArguments['tetheredDescendantNodeAggregateIds'])); - } - - $this->currentContentRepository->handle($command); - $this->currentRootNodeAggregateId = $nodeAggregateId; - } - - /** - * @When /^the command CreateRootNodeAggregateWithNode is executed with payload and exceptions are caught:$/ - * @param TableNode $payloadTable - */ - public function theCommandCreateRootNodeAggregateWithNodeIsExecutedWithPayloadAndExceptionsAreCaught(TableNode $payloadTable) - { - try { - $this->theCommandCreateRootNodeAggregateWithNodeIsExecutedWithPayload($payloadTable); - } catch (\Exception $exception) { - $this->lastCommandException = $exception; - } - } - /** * @Given /^the event RootNodeAggregateWithNodeWasCreated was published with payload:$/ * @param TableNode $payloadTable @@ -102,174 +62,6 @@ public function theEventRootNodeAggregateWithNodeWasCreatedWasPublishedToStreamW $this->currentRootNodeAggregateId = $nodeAggregateId; } - /** - * @When /^the command UpdateRootNodeAggregateDimensions is executed with payload:$/ - * @param TableNode $payloadTable - * @throws ContentStreamDoesNotExistYet - * @throws \Exception - */ - public function theCommandUpdateRootNodeAggregateDimensionsIsExecutedWithPayload(TableNode $payloadTable) - { - $commandArguments = $this->readPayloadTable($payloadTable); - $workspaceName = isset($commandArguments['workspaceName']) - ? WorkspaceName::fromString($commandArguments['workspaceName']) - : $this->currentWorkspaceName; - $nodeAggregateId = NodeAggregateId::fromString($commandArguments['nodeAggregateId']); - - $command = UpdateRootNodeAggregateDimensions::create( - $workspaceName, - $nodeAggregateId, - ); - - $this->currentContentRepository->handle($command); - $this->currentRootNodeAggregateId = $nodeAggregateId; - } - - /** - * @When /^the command CreateNodeAggregateWithNode is executed with payload:$/ - * @param TableNode $payloadTable - */ - public function theCommandCreateNodeAggregateWithNodeIsExecutedWithPayload(TableNode $payloadTable) - { - $commandArguments = $this->readPayloadTable($payloadTable); - $workspaceName = isset($commandArguments['workspaceName']) - ? WorkspaceName::fromString($commandArguments['workspaceName']) - : $this->currentWorkspaceName; - $originDimensionSpacePoint = isset($commandArguments['originDimensionSpacePoint']) - ? OriginDimensionSpacePoint::fromArray($commandArguments['originDimensionSpacePoint']) - : OriginDimensionSpacePoint::fromDimensionSpacePoint($this->currentDimensionSpacePoint); - - $command = CreateNodeAggregateWithNode::create( - $workspaceName, - NodeAggregateId::fromString($commandArguments['nodeAggregateId']), - NodeTypeName::fromString($commandArguments['nodeTypeName']), - $originDimensionSpacePoint, - NodeAggregateId::fromString($commandArguments['parentNodeAggregateId']), - isset($commandArguments['succeedingSiblingNodeAggregateId']) - ? NodeAggregateId::fromString($commandArguments['succeedingSiblingNodeAggregateId']) - : null, - isset($commandArguments['initialPropertyValues']) - ? $this->deserializeProperties($commandArguments['initialPropertyValues']) - : null, - ); - if (isset($commandArguments['tetheredDescendantNodeAggregateIds'])) { - $command = $command->withTetheredDescendantNodeAggregateIds(NodeAggregateIdsByNodePaths::fromArray($commandArguments['tetheredDescendantNodeAggregateIds'])); - } - if (isset($commandArguments['nodeName'])) { - $command = $command->withNodeName(NodeName::fromString($commandArguments['nodeName'])); - } - $this->currentContentRepository->handle($command); - } - - /** - * @When /^the command CreateNodeAggregateWithNode is executed with payload and exceptions are caught:$/ - * @param TableNode $payloadTable - */ - public function theCommandCreateNodeAggregateWithNodeIsExecutedWithPayloadAndExceptionsAreCaught(TableNode $payloadTable) - { - try { - $this->theCommandCreateNodeAggregateWithNodeIsExecutedWithPayload($payloadTable); - } catch (\Exception $exception) { - $this->lastCommandException = $exception; - } - } - - /** - * @When the following CreateNodeAggregateWithNode commands are executed: - */ - public function theFollowingCreateNodeAggregateWithNodeCommandsAreExecuted(TableNode $table): void - { - foreach ($table->getHash() as $row) { - $workspaceName = isset($row['workspaceName']) - ? WorkspaceName::fromString($row['workspaceName']) - : $this->currentWorkspaceName; - $originDimensionSpacePoint = isset($row['originDimensionSpacePoint']) - ? OriginDimensionSpacePoint::fromJsonString($row['originDimensionSpacePoint']) - : OriginDimensionSpacePoint::fromDimensionSpacePoint($this->currentDimensionSpacePoint); - $rawParentNodeAggregateId = $row['parentNodeAggregateId']; - $command = CreateNodeAggregateWithNode::create( - $workspaceName, - NodeAggregateId::fromString($row['nodeAggregateId']), - NodeTypeName::fromString($row['nodeTypeName']), - $originDimensionSpacePoint, - \str_starts_with($rawParentNodeAggregateId, '$') - ? $this->rememberedNodeAggregateIds[\mb_substr($rawParentNodeAggregateId, 1)] - : NodeAggregateId::fromString($rawParentNodeAggregateId), - !empty($row['succeedingSiblingNodeAggregateId']) - ? NodeAggregateId::fromString($row['succeedingSiblingNodeAggregateId']) - : null, - isset($row['initialPropertyValues']) - ? $this->parsePropertyValuesJsonString($row['initialPropertyValues']) - : null, - isset($row['references']) ? json_decode($row['references']) : null, - ); - if (!empty($row['tetheredDescendantNodeAggregateIds'])) { - $command = $command->withTetheredDescendantNodeAggregateIds(NodeAggregateIdsByNodePaths::fromJsonString($row['tetheredDescendantNodeAggregateIds'])); - } - if (!empty($row['nodeName'])) { - $command = $command->withNodeName(NodeName::fromString($row['nodeName'])); - } - $this->currentContentRepository->handle($command); - } - } - - private function parsePropertyValuesJsonString(string $jsonString): PropertyValuesToWrite - { - $array = \json_decode($jsonString, true, 512, JSON_THROW_ON_ERROR); - - return $this->deserializeProperties($array); - } - - /** - * @When /^the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload:$/ - * @param TableNode $payloadTable - * @throws \Exception - */ - public function theCommandCreateNodeAggregateWithNodeAndSerializedPropertiesIsExecutedWithPayload(TableNode $payloadTable) - { - $commandArguments = $this->readPayloadTable($payloadTable); - $workspaceName = isset($commandArguments['workspaceName']) - ? WorkspaceName::fromString($commandArguments['workspaceName']) - : $this->currentWorkspaceName; - $originDimensionSpacePoint = isset($commandArguments['originDimensionSpacePoint']) - ? OriginDimensionSpacePoint::fromArray($commandArguments['originDimensionSpacePoint']) - : OriginDimensionSpacePoint::fromDimensionSpacePoint($this->currentDimensionSpacePoint); - - $command = CreateNodeAggregateWithNodeAndSerializedProperties::create( - $workspaceName, - NodeAggregateId::fromString($commandArguments['nodeAggregateId']), - NodeTypeName::fromString($commandArguments['nodeTypeName']), - $originDimensionSpacePoint, - NodeAggregateId::fromString($commandArguments['parentNodeAggregateId']), - isset($commandArguments['succeedingSiblingNodeAggregateId']) - ? NodeAggregateId::fromString($commandArguments['succeedingSiblingNodeAggregateId']) - : null, - isset($commandArguments['initialPropertyValues']) - ? SerializedPropertyValues::fromArray($commandArguments['initialPropertyValues']) - : null - ); - if (isset($commandArguments['tetheredDescendantNodeAggregateIds'])) { - $command = $command->withTetheredDescendantNodeAggregateIds(NodeAggregateIdsByNodePaths::fromArray($commandArguments['tetheredDescendantNodeAggregateIds'])); - } - if (isset($commandArguments['nodeName'])) { - $command = $command->withNodeName(NodeName::fromString($commandArguments['nodeName'])); - } - $this->currentContentRepository->handle($command); - } - - /** - * @When /^the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload and exceptions are caught:$/ - * @param TableNode $payloadTable - */ - public function theCommandCreateNodeAggregateWithNodeAndSerializedPropertiesIsExecutedWithPayloadAndExceptionsAreCaught(TableNode $payloadTable) - { - try { - $this->theCommandCreateNodeAggregateWithNodeAndSerializedPropertiesIsExecutedWithPayload($payloadTable); - } catch (\Exception $exception) { - $this->lastCommandException = $exception; - } - } - /** * @Given /^the event NodeAggregateWithNodeWasCreated was published with payload:$/ * @param TableNode $payloadTable diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeDisabling.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeDisabling.php deleted file mode 100644 index 62c6948d904..00000000000 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeDisabling.php +++ /dev/null @@ -1,113 +0,0 @@ -readPayloadTable($payloadTable); - $workspaceName = isset($commandArguments['workspaceName']) - ? WorkspaceName::fromString($commandArguments['workspaceName']) - : $this->currentWorkspaceName; - $coveredDimensionSpacePoint = isset($commandArguments['coveredDimensionSpacePoint']) - ? DimensionSpacePoint::fromArray($commandArguments['coveredDimensionSpacePoint']) - : $this->currentDimensionSpacePoint; - - $command = DisableNodeAggregate::create( - $workspaceName, - NodeAggregateId::fromString($commandArguments['nodeAggregateId']), - $coveredDimensionSpacePoint, - NodeVariantSelectionStrategy::from($commandArguments['nodeVariantSelectionStrategy']), - ); - - $this->currentContentRepository->handle($command); - } - - /** - * @Given /^the command DisableNodeAggregate is executed with payload and exceptions are caught:$/ - * @param TableNode $payloadTable - */ - public function theCommandDisableNodeAggregateIsExecutedWithPayloadAndExceptionsAreCaught(TableNode $payloadTable): void - { - try { - $this->theCommandDisableNodeAggregateIsExecutedWithPayload($payloadTable); - } catch (\Exception $exception) { - $this->lastCommandException = $exception; - } - } - - /** - * @Given /^the command EnableNodeAggregate is executed with payload:$/ - * @param TableNode $payloadTable - * @throws \Exception - */ - public function theCommandEnableNodeAggregateIsExecutedWithPayload(TableNode $payloadTable): void - { - $commandArguments = $this->readPayloadTable($payloadTable); - $workspaceName = isset($commandArguments['workspaceName']) - ? WorkspaceName::fromString($commandArguments['workspaceName']) - : $this->currentWorkspaceName; - $coveredDimensionSpacePoint = isset($commandArguments['coveredDimensionSpacePoint']) - ? DimensionSpacePoint::fromArray($commandArguments['coveredDimensionSpacePoint']) - : $this->currentDimensionSpacePoint; - - $command = EnableNodeAggregate::create( - $workspaceName, - NodeAggregateId::fromString($commandArguments['nodeAggregateId']), - $coveredDimensionSpacePoint, - NodeVariantSelectionStrategy::from($commandArguments['nodeVariantSelectionStrategy']), - ); - - $this->currentContentRepository->handle($command); - } - - /** - * @Given /^the command EnableNodeAggregate is executed with payload and exceptions are caught:$/ - * @param TableNode $payloadTable - */ - public function theCommandEnableNodeAggregateIsExecutedWithPayloadAndExceptionsAreCaught(TableNode $payloadTable): void - { - try { - $this->theCommandEnableNodeAggregateIsExecutedWithPayload($payloadTable); - } catch (\Exception $exception) { - $this->lastCommandException = $exception; - } - } -} diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeModification.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeModification.php index db983fd4e59..4671d932858 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeModification.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeModification.php @@ -15,13 +15,9 @@ namespace Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Features; use Behat\Gherkin\Node\TableNode; -use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; use Neos\ContentRepository\Core\Feature\ContentStreamEventStreamName; -use Neos\ContentRepository\Core\Feature\NodeModification\Command\SetNodeProperties; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; -use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; -use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\CRTestSuiteRuntimeVariables; use Neos\EventStore\Model\Event\StreamName; use PHPUnit\Framework\Assert; @@ -37,46 +33,6 @@ abstract protected function readPayloadTable(TableNode $payloadTable): array; abstract protected function publishEvent(string $eventType, StreamName $streamName, array $eventPayload): void; - /** - * @When /^the command SetNodeProperties is executed with payload:$/ - * @param TableNode $payloadTable - */ - public function theCommandSetPropertiesIsExecutedWithPayload(TableNode $payloadTable) - { - $commandArguments = $this->readPayloadTable($payloadTable); - if (!isset($commandArguments['workspaceName'])) { - $commandArguments['workspaceName'] = $this->currentWorkspaceName->value; - } - if (!isset($commandArguments['originDimensionSpacePoint'])) { - $commandArguments['originDimensionSpacePoint'] = $this->currentDimensionSpacePoint->jsonSerialize(); - } - - $rawNodeAggregateId = $commandArguments['nodeAggregateId']; - $command = SetNodeProperties::create( - WorkspaceName::fromString($commandArguments['workspaceName']), - \str_starts_with($rawNodeAggregateId, '$') - ? $this->rememberedNodeAggregateIds[\mb_substr($rawNodeAggregateId, 1)] - : NodeAggregateId::fromString($rawNodeAggregateId), - OriginDimensionSpacePoint::fromArray($commandArguments['originDimensionSpacePoint']), - $this->deserializeProperties($commandArguments['propertyValues']), - ); - - $this->currentContentRepository->handle($command); - } - - /** - * @When /^the command SetNodeProperties is executed with payload and exceptions are caught:$/ - * @param TableNode $payloadTable - */ - public function theCommandSetPropertiesIsExecutedWithPayloadAndExceptionsAreCaught(TableNode $payloadTable) - { - try { - $this->theCommandSetPropertiesIsExecutedWithPayload($payloadTable); - } catch (\Exception $exception) { - $this->lastCommandException = $exception; - } - } - /** * @Given /^the event NodePropertiesWereSet was published with payload:$/ * @param TableNode $payloadTable diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeMove.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeMove.php index 73edb77c65c..fa4a9bc28a5 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeMove.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeMove.php @@ -36,59 +36,6 @@ abstract protected function readPayloadTable(TableNode $payloadTable): array; abstract protected function publishEvent(string $eventType, StreamName $streamName, array $eventPayload): void; - /** - * @Given /^the command MoveNodeAggregate is executed with payload:$/ - * @param TableNode $payloadTable - * @throws \Exception - */ - public function theCommandMoveNodeIsExecutedWithPayload(TableNode $payloadTable): void - { - $commandArguments = $this->readPayloadTable($payloadTable); - $workspaceName = isset($commandArguments['workspaceName']) - ? WorkspaceName::fromString($commandArguments['workspaceName']) - : $this->currentWorkspaceName; - $dimensionSpacePoint = isset($commandArguments['dimensionSpacePoint']) - ? DimensionSpacePoint::fromArray($commandArguments['dimensionSpacePoint']) - : $this->currentDimensionSpacePoint; - $newParentNodeAggregateId = isset($commandArguments['newParentNodeAggregateId']) - ? NodeAggregateId::fromString($commandArguments['newParentNodeAggregateId']) - : null; - $newPrecedingSiblingNodeAggregateId = isset($commandArguments['newPrecedingSiblingNodeAggregateId']) - ? NodeAggregateId::fromString($commandArguments['newPrecedingSiblingNodeAggregateId']) - : null; - $newSucceedingSiblingNodeAggregateId = isset($commandArguments['newSucceedingSiblingNodeAggregateId']) - ? NodeAggregateId::fromString($commandArguments['newSucceedingSiblingNodeAggregateId']) - : null; - $relationDistributionStrategy = RelationDistributionStrategy::fromString( - $commandArguments['relationDistributionStrategy'] ?? null - ); - - $command = MoveNodeAggregate::create( - $workspaceName, - $dimensionSpacePoint, - NodeAggregateId::fromString($commandArguments['nodeAggregateId']), - $relationDistributionStrategy, - $newParentNodeAggregateId, - $newPrecedingSiblingNodeAggregateId, - $newSucceedingSiblingNodeAggregateId, - ); - - $this->currentContentRepository->handle($command); - } - - /** - * @Given /^the command MoveNodeAggregate is executed with payload and exceptions are caught:$/ - * @param TableNode $payloadTable - */ - public function theCommandMoveNodeIsExecutedWithPayloadAndExceptionsAreCaught(TableNode $payloadTable): void - { - try { - $this->theCommandMoveNodeIsExecutedWithPayload($payloadTable); - } catch (\Exception $exception) { - $this->lastCommandException = $exception; - } - } - /** * @Given /^the event NodeAggregateWasMoved was published with payload:$/ * @param TableNode $payloadTable diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeReferencing.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeReferencing.php index 43cdfc68c60..3d07a3cf3ec 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeReferencing.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeReferencing.php @@ -15,17 +15,8 @@ namespace Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Features; use Behat\Gherkin\Node\TableNode; -use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; use Neos\ContentRepository\Core\Feature\ContentStreamEventStreamName; -use Neos\ContentRepository\Core\Feature\NodeModification\Dto\PropertyValuesToWrite; -use Neos\ContentRepository\Core\Feature\NodeReferencing\Command\SetNodeReferences; -use Neos\ContentRepository\Core\Feature\NodeReferencing\Dto\NodeReferencesForName; -use Neos\ContentRepository\Core\Feature\NodeReferencing\Dto\NodeReferencesToWrite; -use Neos\ContentRepository\Core\Feature\NodeReferencing\Dto\NodeReferenceToWrite; -use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; -use Neos\ContentRepository\Core\SharedModel\Node\ReferenceName; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; -use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\CRTestSuiteRuntimeVariables; use Neos\EventStore\Model\Event\StreamName; @@ -38,50 +29,8 @@ trait NodeReferencing abstract protected function readPayloadTable(TableNode $payloadTable): array; - abstract protected function deserializeProperties(array $properties): PropertyValuesToWrite; - abstract protected function publishEvent(string $eventType, StreamName $streamName, array $eventPayload): void; - /** - * @Given /^the command SetNodeReferences is executed with payload:$/ - * @param TableNode $payloadTable - * @throws \Exception - */ - public function theCommandSetNodeReferencesIsExecutedWithPayload(TableNode $payloadTable) - { - $commandArguments = $this->readPayloadTable($payloadTable); - $workspaceName = isset($commandArguments['workspaceName']) - ? WorkspaceName::fromString($commandArguments['workspaceName']) - : $this->currentWorkspaceName; - $sourceOriginDimensionSpacePoint = isset($commandArguments['sourceOriginDimensionSpacePoint']) - ? OriginDimensionSpacePoint::fromArray($commandArguments['sourceOriginDimensionSpacePoint']) - : OriginDimensionSpacePoint::fromDimensionSpacePoint($this->currentDimensionSpacePoint); - - $references = $this->mapRawNodeReferencesToNodeReferencesToWrite($commandArguments['references']); - $command = SetNodeReferences::create( - $workspaceName, - NodeAggregateId::fromString($commandArguments['sourceNodeAggregateId']), - $sourceOriginDimensionSpacePoint, - $references, - ); - - $this->currentContentRepository->handle($command); - } - - /** - * @Given /^the command SetNodeReferences is executed with payload and exceptions are caught:$/ - * @param TableNode $payloadTable - * @throws \Exception - */ - public function theCommandSetNodeReferencesIsExecutedWithPayloadAndExceptionsAreCaught(TableNode $payloadTable) - { - try { - $this->theCommandSetNodeReferencesIsExecutedWithPayload($payloadTable); - } catch (\Exception $exception) { - $this->lastCommandException = $exception; - } - } - /** * @Given /^the event NodeReferencesWereSet was published with payload:$/ * @param TableNode $payloadTable @@ -97,18 +46,4 @@ public function theEventNodeReferencesWereSetWasPublishedWithPayload(TableNode $ $this->publishEvent('NodeReferencesWereSet', $streamName->getEventStreamName(), $eventPayload); } - - protected function mapRawNodeReferencesToNodeReferencesToWrite(array $deserializedTableContent): NodeReferencesToWrite - { - $referencesForProperty = []; - foreach ($deserializedTableContent as $nodeReferencesForProperty) { - $references = []; - foreach ($nodeReferencesForProperty['references'] as $referenceData) { - $properties = isset($referenceData['properties']) ? $this->deserializeProperties($referenceData['properties']) : PropertyValuesToWrite::createEmpty(); - $references[] = NodeReferenceToWrite::fromTargetAndProperties(NodeAggregateId::fromString($referenceData['target']), $properties); - } - $referencesForProperty[] = NodeReferencesForName::fromReferences(ReferenceName::fromString($nodeReferencesForProperty['referenceName']), $references); - } - return NodeReferencesToWrite::fromArray($referencesForProperty); - } } diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeRemoval.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeRemoval.php index 9bd92a7caba..a7d8a85c050 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeRemoval.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeRemoval.php @@ -15,13 +15,8 @@ namespace Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Features; use Behat\Gherkin\Node\TableNode; -use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\Feature\ContentStreamEventStreamName; -use Neos\ContentRepository\Core\Feature\NodeRemoval\Command\RemoveNodeAggregate; -use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; -use Neos\ContentRepository\Core\SharedModel\Node\NodeVariantSelectionStrategy; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; -use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\CRTestSuiteRuntimeVariables; use Neos\EventStore\Model\Event\StreamName; @@ -36,48 +31,6 @@ abstract protected function readPayloadTable(TableNode $payloadTable): array; abstract protected function publishEvent(string $eventType, StreamName $streamName, array $eventPayload): void; - /** - * @Given /^the command RemoveNodeAggregate is executed with payload:$/ - * @param TableNode $payloadTable - * @throws \Exception - */ - public function theCommandRemoveNodeAggregateIsExecutedWithPayload(TableNode $payloadTable) - { - $commandArguments = $this->readPayloadTable($payloadTable); - $workspaceName = isset($commandArguments['workspaceName']) - ? WorkspaceName::fromString($commandArguments['workspaceName']) - : $this->currentWorkspaceName; - $coveredDimensionSpacePoint = isset($commandArguments['coveredDimensionSpacePoint']) - ? DimensionSpacePoint::fromArray($commandArguments['coveredDimensionSpacePoint']) - : $this->currentDimensionSpacePoint; - - $command = RemoveNodeAggregate::create( - $workspaceName, - NodeAggregateId::fromString($commandArguments['nodeAggregateId']), - $coveredDimensionSpacePoint, - NodeVariantSelectionStrategy::from($commandArguments['nodeVariantSelectionStrategy']), - ); - if (isset($commandArguments['removalAttachmentPoint'])) { - $command = $command->withRemovalAttachmentPoint(NodeAggregateId::fromString($commandArguments['removalAttachmentPoint'])); - } - - $this->currentContentRepository->handle($command); - } - - /** - * @Given /^the command RemoveNodeAggregate is executed with payload and exceptions are caught:$/ - * @param TableNode $payloadTable - * @throws \Exception - */ - public function theCommandRemoveNodeAggregateIsExecutedWithPayloadAndExceptionsAreCaught(TableNode $payloadTable) - { - try { - $this->theCommandRemoveNodeAggregateIsExecutedWithPayload($payloadTable); - } catch (\Exception $exception) { - $this->lastCommandException = $exception; - } - } - /** * @Given /^the event NodeAggregateWasRemoved was published with payload:$/ * @param TableNode $payloadTable diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeRenaming.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeRenaming.php index ac383b6ba5b..456183e507a 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeRenaming.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeRenaming.php @@ -14,11 +14,7 @@ namespace Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Features; -use Behat\Gherkin\Node\TableNode; -use Neos\ContentRepository\Core\Feature\NodeRenaming\Command\ChangeNodeAggregateName; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; -use Neos\ContentRepository\Core\SharedModel\Node\NodeName; -use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\CRTestSuiteRuntimeVariables; use PHPUnit\Framework\Assert; @@ -29,41 +25,6 @@ trait NodeRenaming { use CRTestSuiteRuntimeVariables; - /** - * @Given /^the command ChangeNodeAggregateName is executed with payload:$/ - * @param TableNode $payloadTable - * @throws \Exception - */ - public function theCommandChangeNodeAggregateNameIsExecutedWithPayload(TableNode $payloadTable) - { - $commandArguments = $this->readPayloadTable($payloadTable); - $workspaceName = isset($commandArguments['workspaceName']) - ? WorkspaceName::fromString($commandArguments['workspaceName']) - : $this->currentWorkspaceName; - - $command = ChangeNodeAggregateName::create( - $workspaceName, - NodeAggregateId::fromString($commandArguments['nodeAggregateId']), - NodeName::fromString($commandArguments['newNodeName']), - ); - - $this->currentContentRepository->handle($command); - } - - /** - * @Given /^the command ChangeNodeAggregateName is executed with payload and exceptions are caught:$/ - * @param TableNode $payloadTable - * @throws \Exception - */ - public function theCommandChangeNodeAggregateNameIsExecutedWithPayloadAndExceptionsAreCaught(TableNode $payloadTable) - { - try { - $this->theCommandChangeNodeAggregateNameIsExecutedWithPayload($payloadTable); - } catch (\Exception $exception) { - $this->lastCommandException = $exception; - } - } - /** * @Then /^I expect the node "([^"]*)" to have the name "([^"]*)"$/ * @param string $nodeAggregateId diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeTypeChange.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeTypeChange.php deleted file mode 100644 index 1033e93f2f9..00000000000 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeTypeChange.php +++ /dev/null @@ -1,72 +0,0 @@ -readPayloadTable($payloadTable); - $workspaceName = isset($commandArguments['workspaceName']) - ? WorkspaceName::fromString($commandArguments['workspaceName']) - : $this->currentWorkspaceName; - $command = ChangeNodeAggregateType::create( - $workspaceName, - NodeAggregateId::fromString($commandArguments['nodeAggregateId']), - NodeTypeName::fromString($commandArguments['newNodeTypeName']), - NodeAggregateTypeChangeChildConstraintConflictResolutionStrategy::from($commandArguments['strategy']), - ); - if (isset($commandArguments['tetheredDescendantNodeAggregateIds'])) { - $command = $command->withTetheredDescendantNodeAggregateIds(NodeAggregateIdsByNodePaths::fromArray($commandArguments['tetheredDescendantNodeAggregateIds'])); - } - - $this->currentContentRepository->handle($command); - } - - /** - * @Given /^the command ChangeNodeAggregateType is executed with payload and exceptions are caught:$/ - * @param TableNode $payloadTable - * @throws \Exception - */ - public function theCommandChangeNodeAggregateTypeIsExecutedWithPayloadAndExceptionsAreCaught(TableNode $payloadTable) - { - try { - $this->theCommandChangeNodeAggregateTypeIsExecutedWithPayload($payloadTable); - } catch (\Exception $exception) { - $this->lastCommandException = $exception; - } - } -} diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeVariation.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeVariation.php deleted file mode 100644 index 3ead5a2de26..00000000000 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeVariation.php +++ /dev/null @@ -1,70 +0,0 @@ -readPayloadTable($payloadTable); - $workspaceName = isset($commandArguments['workspaceName']) - ? WorkspaceName::fromString($commandArguments['workspaceName']) - : $this->currentWorkspaceName; - - $command = CreateNodeVariant::create( - $workspaceName, - NodeAggregateId::fromString($commandArguments['nodeAggregateId']), - OriginDimensionSpacePoint::fromArray($commandArguments['sourceOrigin']), - OriginDimensionSpacePoint::fromArray($commandArguments['targetOrigin']), - ); - $this->currentContentRepository->handle($command); - } - - /** - * @Given /^the command CreateNodeVariant is executed with payload and exceptions are caught:$/ - * @param TableNode $payloadTable - * @throws \Exception - */ - public function theCommandCreateNodeVariantIsExecutedWithPayloadAndExceptionsAreCaught(TableNode $payloadTable) - { - try { - $this->theCommandCreateNodeVariantIsExecutedWithPayload($payloadTable); - } catch (\Exception $exception) { - $this->lastCommandException = $exception; - } - } -} diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/SubtreeTagging.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/SubtreeTagging.php index f249060dca5..f73e05d78c8 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/SubtreeTagging.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/SubtreeTagging.php @@ -15,15 +15,8 @@ namespace Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Features; use Behat\Gherkin\Node\TableNode; -use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\Feature\ContentStreamEventStreamName; -use Neos\ContentRepository\Core\Feature\SubtreeTagging\Command\TagSubtree; -use Neos\ContentRepository\Core\Feature\SubtreeTagging\Command\UntagSubtree; -use Neos\ContentRepository\Core\Feature\SubtreeTagging\Dto\SubtreeTag; -use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; -use Neos\ContentRepository\Core\SharedModel\Node\NodeVariantSelectionStrategy; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; -use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\CRTestSuiteRuntimeVariables; use Neos\EventStore\Model\Event\StreamName; @@ -38,44 +31,6 @@ abstract protected function readPayloadTable(TableNode $payloadTable): array; abstract protected function publishEvent(string $eventType, StreamName $streamName, array $eventPayload): void; - /** - * @Given /^the command TagSubtree is executed with payload:$/ - * @param TableNode $payloadTable - * @throws \Exception - */ - public function theCommandTagSubtreeIsExecutedWithPayload(TableNode $payloadTable): void - { - $commandArguments = $this->readPayloadTable($payloadTable); - $workspaceName = isset($commandArguments['workspaceName']) - ? WorkspaceName::fromString($commandArguments['workspaceName']) - : $this->currentWorkspaceName; - $coveredDimensionSpacePoint = isset($commandArguments['coveredDimensionSpacePoint']) - ? DimensionSpacePoint::fromArray($commandArguments['coveredDimensionSpacePoint']) - : $this->currentDimensionSpacePoint; - - $command = TagSubtree::create( - $workspaceName, - NodeAggregateId::fromString($commandArguments['nodeAggregateId']), - $coveredDimensionSpacePoint, - NodeVariantSelectionStrategy::from($commandArguments['nodeVariantSelectionStrategy']), - SubtreeTag::fromString($commandArguments['tag']), - ); - - $this->currentContentRepository->handle($command); - } - - /** - * @Given /^the command TagSubtree is executed with payload and exceptions are caught:$/ - * @param TableNode $payloadTable - */ - public function theCommandTagSubtreeIsExecutedWithPayloadAndExceptionsAreCaught(TableNode $payloadTable): void - { - try { - $this->theCommandTagSubtreeIsExecutedWithPayload($payloadTable); - } catch (\Exception $exception) { - $this->lastCommandException = $exception; - } - } /** * @Given /^the event SubtreeWasTagged was published with payload:$/ @@ -107,44 +62,4 @@ public function theEventSubtreeWasUntaggedWasPublishedWithPayload(TableNode $pay $this->publishEvent('SubtreeWasUntagged', $streamName->getEventStreamName(), $eventPayload); } - - - /** - * @Given /^the command UntagSubtree is executed with payload:$/ - * @param TableNode $payloadTable - * @throws \Exception - */ - public function theCommandUntagSubtreeIsExecutedWithPayload(TableNode $payloadTable): void - { - $commandArguments = $this->readPayloadTable($payloadTable); - $workspaceName = isset($commandArguments['workspaceName']) - ? WorkspaceName::fromString($commandArguments['workspaceName']) - : $this->currentWorkspaceName; - $coveredDimensionSpacePoint = isset($commandArguments['coveredDimensionSpacePoint']) - ? DimensionSpacePoint::fromArray($commandArguments['coveredDimensionSpacePoint']) - : $this->currentDimensionSpacePoint; - - $command = UntagSubtree::create( - $workspaceName, - NodeAggregateId::fromString($commandArguments['nodeAggregateId']), - $coveredDimensionSpacePoint, - NodeVariantSelectionStrategy::from($commandArguments['nodeVariantSelectionStrategy']), - SubtreeTag::fromString($commandArguments['tag']), - ); - - $this->currentContentRepository->handle($command); - } - - /** - * @Given /^the command UntagSubtree is executed with payload and exceptions are caught:$/ - * @param TableNode $payloadTable - */ - public function theCommandUntagSubtreeIsExecutedWithPayloadAndExceptionsAreCaught(TableNode $payloadTable): void - { - try { - $this->theCommandUntagSubtreeIsExecutedWithPayload($payloadTable); - } catch (\Exception $exception) { - $this->lastCommandException = $exception; - } - } } diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/WorkspaceCreation.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/WorkspaceCreation.php index 91122fe5dba..54b239604c1 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/WorkspaceCreation.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/WorkspaceCreation.php @@ -15,15 +15,8 @@ namespace Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Features; use Behat\Gherkin\Node\TableNode; -use Neos\ContentRepository\Core\Feature\ContentStreamCreation\Command\CreateContentStream; use Neos\ContentRepository\Core\Feature\ContentStreamEventStreamName; -use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Command\CreateRootWorkspace; -use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Command\CreateWorkspace; -use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Command\RebaseWorkspace; -use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Dto\RebaseErrorHandlingStrategy; -use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Exception\WorkspaceRebaseFailed; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; -use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\CRTestSuiteRuntimeVariables; use Neos\EventStore\Model\Event\StreamName; @@ -38,22 +31,6 @@ abstract protected function readPayloadTable(TableNode $payloadTable): array; abstract protected function publishEvent(string $eventType, StreamName $streamName, array $eventPayload): void; - /** - * @When /^the command CreateRootWorkspace is executed with payload:$/ - * @param TableNode $payloadTable - * @throws \Exception - */ - public function theCommandCreateRootWorkspaceIsExecutedWithPayload(TableNode $payloadTable) - { - $commandArguments = $this->readPayloadTable($payloadTable); - - $command = CreateRootWorkspace::create( - WorkspaceName::fromString($commandArguments['workspaceName']), - ContentStreamId::fromString($commandArguments['newContentStreamId']) - ); - - $this->currentContentRepository->handle($command); - } /** * @Given /^the event RootWorkspaceWasCreated was published with payload:$/ * @param TableNode $payloadTable @@ -66,69 +43,4 @@ public function theEventRootWorkspaceWasCreatedWasPublishedToStreamWithPayload(T $streamName = ContentStreamEventStreamName::fromContentStreamId($newContentStreamId); $this->publishEvent('RootWorkspaceWasCreated', $streamName->getEventStreamName(), $eventPayload); } - - /** - * @When /^the command CreateWorkspace is executed with payload:$/ - * @param TableNode $payloadTable - * @throws \Exception - */ - public function theCommandCreateWorkspaceIsExecutedWithPayload(TableNode $payloadTable) - { - $commandArguments = $this->readPayloadTable($payloadTable); - - $command = CreateWorkspace::create( - WorkspaceName::fromString($commandArguments['workspaceName']), - WorkspaceName::fromString($commandArguments['baseWorkspaceName']), - ContentStreamId::fromString($commandArguments['newContentStreamId']), - ); - - $this->currentContentRepository->handle($command); - } - - /** - * @When /^the command CreateWorkspace is executed with payload and exceptions are caught:$/ - * @param TableNode $payloadTable - * @throws \Exception - */ - public function theCommandCreateWorkspaceIsExecutedWithPayloadAndExceptionsAreCaught(TableNode $payloadTable) - { - try { - $this->theCommandCreateWorkspaceIsExecutedWithPayload($payloadTable); - } catch (\Exception $e) { - $this->lastCommandException = $e; - } - } - - /** - * @When /^the command RebaseWorkspace is executed with payload:$/ - * @param TableNode $payloadTable - * @throws \Exception - */ - public function theCommandRebaseWorkspaceIsExecutedWithPayload(TableNode $payloadTable) - { - $commandArguments = $this->readPayloadTable($payloadTable); - $command = RebaseWorkspace::create( - WorkspaceName::fromString($commandArguments['workspaceName']), - ); - if (isset($commandArguments['rebasedContentStreamId'])) { - $command = $command->withRebasedContentStreamId(ContentStreamId::fromString($commandArguments['rebasedContentStreamId'])); - } - if (isset($commandArguments['rebaseErrorHandlingStrategy'])) { - $command = $command->withErrorHandlingStrategy(RebaseErrorHandlingStrategy::from($commandArguments['rebaseErrorHandlingStrategy'])); - } - - $this->currentContentRepository->handle($command); - } - - /** - * @When /^the command RebaseWorkspace is executed with payload and exceptions are caught:$/ - */ - public function theCommandRebaseWorkspaceIsExecutedWithPayloadAndExceptionsAreCaught(TableNode $payloadTable) - { - try { - $this->theCommandRebaseWorkspaceIsExecutedWithPayload($payloadTable); - } catch (\Exception $e) { - $this->lastCommandException = $e; - } - } } diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/WorkspaceDiscarding.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/WorkspaceDiscarding.php deleted file mode 100644 index 28aaf305b73..00000000000 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/WorkspaceDiscarding.php +++ /dev/null @@ -1,85 +0,0 @@ -readPayloadTable($payloadTable); - $command = DiscardWorkspace::create( - WorkspaceName::fromString($commandArguments['workspaceName']), - ); - if (isset($commandArguments['newContentStreamId'])) { - $command = $command->withNewContentStreamId(ContentStreamId::fromString($commandArguments['newContentStreamId'])); - } - - $this->currentContentRepository->handle($command); - } - - - /** - * @Given /^the command DiscardIndividualNodesFromWorkspace is executed with payload:$/ - * @param TableNode $payloadTable - * @throws \Exception - */ - public function theCommandDiscardIndividualNodesFromWorkspaceIsExecuted(TableNode $payloadTable): void - { - $commandArguments = $this->readPayloadTable($payloadTable); - $nodesToDiscard = NodeIdsToPublishOrDiscard::fromArray($commandArguments['nodesToDiscard']); - $command = DiscardIndividualNodesFromWorkspace::create( - WorkspaceName::fromString($commandArguments['workspaceName']), - $nodesToDiscard, - ); - if (isset($commandArguments['newContentStreamId'])) { - $command = $command->withNewContentStreamId(ContentStreamId::fromString($commandArguments['newContentStreamId'])); - } - - $this->currentContentRepository->handle($command); - } - - - /** - * @Given /^the command DiscardIndividualNodesFromWorkspace is executed with payload and exceptions are caught:$/ - */ - public function theCommandDiscardIndividualNodesFromWorkspaceIsExecutedAndExceptionsAreCaught(TableNode $payloadTable): void - { - try { - $this->theCommandDiscardIndividualNodesFromWorkspaceIsExecuted($payloadTable); - } catch (\Exception $exception) { - $this->lastCommandException = $exception; - } - } -} diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/WorkspacePublishing.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/WorkspacePublishing.php deleted file mode 100644 index 6b656113bee..00000000000 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/WorkspacePublishing.php +++ /dev/null @@ -1,131 +0,0 @@ -readPayloadTable($payloadTable); - $nodesToPublish = NodeIdsToPublishOrDiscard::fromArray($commandArguments['nodesToPublish']); - - $command = PublishIndividualNodesFromWorkspace::create( - array_key_exists('workspaceName', $commandArguments) - ? WorkspaceName::fromString($commandArguments['workspaceName']) - : $this->currentWorkspaceName, - $nodesToPublish, - ); - if (isset($commandArguments['contentStreamIdForRemainingPart'])) { - $command = $command->withContentStreamIdForRemainingPart(ContentStreamId::fromString($commandArguments['contentStreamIdForRemainingPart'])); - } - - $this->currentContentRepository->handle($command); - } - - /** - * @Given /^the command PublishIndividualNodesFromWorkspace is executed with payload and exceptions are caught:$/ - */ - public function theCommandPublishIndividualNodesFromWorkspaceIsExecutedAndExceptionsAreCaught(TableNode $payloadTable): void - { - try { - $this->theCommandPublishIndividualNodesFromWorkspaceIsExecuted($payloadTable); - } catch (\Exception $exception) { - $this->lastCommandException = $exception; - } - } - - /** - * @Given /^the command PublishWorkspace is executed with payload:$/ - * @throws \Exception - */ - public function theCommandPublishWorkspaceIsExecuted(TableNode $payloadTable): void - { - $commandArguments = $this->readPayloadTable($payloadTable); - - $command = PublishWorkspace::create( - WorkspaceName::fromString($commandArguments['workspaceName']), - ); - if (array_key_exists('newContentStreamId', $commandArguments)) { - $command = $command->withNewContentStreamId( - ContentStreamId::fromString($commandArguments['newContentStreamId']) - ); - } - - $this->currentContentRepository->handle($command); - } - - /** - * @Given /^the command PublishWorkspace is executed with payload and exceptions are caught:$/ - */ - public function theCommandPublishWorkspaceIsExecutedAndExceptionsAreCaught(TableNode $payloadTable): void - { - try { - $this->theCommandPublishWorkspaceIsExecuted($payloadTable); - } catch (\Exception $exception) { - $this->lastCommandException = $exception; - } - } - - /** - * @Given /^the command ChangeBaseWorkspace is executed with payload:$/ - * @throws \Exception - */ - public function theCommandChangeBaseWorkspaceIsExecuted(TableNode $payloadTable): void - { - $commandArguments = $this->readPayloadTable($payloadTable); - $command = ChangeBaseWorkspace::create( - array_key_exists('workspaceName', $commandArguments) - ? WorkspaceName::fromString($commandArguments['workspaceName']) - : $this->currentWorkspaceName, - WorkspaceName::fromString($commandArguments['baseWorkspaceName']), - ); - if (array_key_exists('newContentStreamId', $commandArguments)) { - $command = $command->withNewContentStreamId(ContentStreamId::fromString($commandArguments['newContentStreamId'])); - } - $this->currentContentRepository->handle($command); - } - - /** - * @Given /^the command ChangeBaseWorkspace is executed with payload and exceptions are caught:$/ - */ - public function theCommandChangeBaseWorkspaceIsExecutedAndExceptionsAreCaught(TableNode $payloadTable): void - { - try { - $this->theCommandChangeBaseWorkspaceIsExecuted($payloadTable); - } catch (\Exception $exception) { - $this->lastCommandException = $exception; - } - } -} diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php index dd899ee4e24..008bc184b84 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php @@ -15,26 +15,54 @@ namespace Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap; use Behat\Gherkin\Node\TableNode; +use Neos\ContentRepository\Core\CommandHandler\CommandInterface; +use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; +use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; use Neos\ContentRepository\Core\EventStore\EventNormalizer; use Neos\ContentRepository\Core\EventStore\EventPersister; use Neos\ContentRepository\Core\EventStore\Events; use Neos\ContentRepository\Core\EventStore\EventsToPublish; +use Neos\ContentRepository\Core\Feature\Common\RebasableToOtherWorkspaceInterface; +use Neos\ContentRepository\Core\Feature\DimensionSpaceAdjustment\Command\AddDimensionShineThrough; +use Neos\ContentRepository\Core\Feature\DimensionSpaceAdjustment\Command\MoveDimensionSpacePoint; +use Neos\ContentRepository\Core\Feature\NodeCreation\Command\CreateNodeAggregateWithNode; use Neos\ContentRepository\Core\Feature\NodeCreation\Command\CreateNodeAggregateWithNodeAndSerializedProperties; +use Neos\ContentRepository\Core\Feature\NodeCreation\Dto\NodeAggregateIdsByNodePaths; use Neos\ContentRepository\Core\Feature\NodeDisabling\Command\DisableNodeAggregate; use Neos\ContentRepository\Core\Feature\NodeDisabling\Command\EnableNodeAggregate; +use Neos\ContentRepository\Core\Feature\NodeDuplication\Command\CopyNodesRecursively; +use Neos\ContentRepository\Core\Feature\NodeModification\Command\SetNodeProperties; use Neos\ContentRepository\Core\Feature\NodeModification\Command\SetSerializedNodeProperties; +use Neos\ContentRepository\Core\Feature\NodeModification\Dto\PropertyValuesToWrite; use Neos\ContentRepository\Core\Feature\NodeMove\Command\MoveNodeAggregate; use Neos\ContentRepository\Core\Feature\NodeReferencing\Command\SetNodeReferences; +use Neos\ContentRepository\Core\Feature\NodeReferencing\Command\SetSerializedNodeReferences; +use Neos\ContentRepository\Core\Feature\NodeReferencing\Dto\NodeReferencesForName; +use Neos\ContentRepository\Core\Feature\NodeReferencing\Dto\NodeReferencesToWrite; +use Neos\ContentRepository\Core\Feature\NodeReferencing\Dto\NodeReferenceToWrite; +use Neos\ContentRepository\Core\Feature\NodeRemoval\Command\RemoveNodeAggregate; use Neos\ContentRepository\Core\Feature\NodeRenaming\Command\ChangeNodeAggregateName; +use Neos\ContentRepository\Core\Feature\NodeTypeChange\Command\ChangeNodeAggregateType; +use Neos\ContentRepository\Core\Feature\NodeVariation\Command\CreateNodeVariant; +use Neos\ContentRepository\Core\Feature\RootNodeCreation\Command\CreateRootNodeAggregateWithNode; +use Neos\ContentRepository\Core\Feature\RootNodeCreation\Command\UpdateRootNodeAggregateDimensions; use Neos\ContentRepository\Core\Feature\SubtreeTagging\Command\TagSubtree; use Neos\ContentRepository\Core\Feature\SubtreeTagging\Command\UntagSubtree; use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Command\CreateRootWorkspace; use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Command\CreateWorkspace; +use Neos\ContentRepository\Core\Feature\WorkspaceModification\Command\ChangeBaseWorkspace; +use Neos\ContentRepository\Core\Feature\WorkspaceModification\Command\DeleteWorkspace; +use Neos\ContentRepository\Core\Feature\WorkspacePublication\Command\DiscardIndividualNodesFromWorkspace; +use Neos\ContentRepository\Core\Feature\WorkspacePublication\Command\DiscardWorkspace; use Neos\ContentRepository\Core\Feature\WorkspacePublication\Command\PublishIndividualNodesFromWorkspace; use Neos\ContentRepository\Core\Feature\WorkspacePublication\Command\PublishWorkspace; use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Command\RebaseWorkspace; use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Exception\WorkspaceRebaseFailed; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; +use Neos\ContentRepository\Core\SharedModel\Node\NodeName; +use Neos\ContentRepository\Core\SharedModel\Node\ReferenceName; +use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\EventStore\EventStoreInterface; use Neos\EventStore\Model\Event; use Neos\EventStore\Model\Event\StreamName; @@ -57,66 +85,182 @@ abstract protected function readPayloadTable(TableNode $payloadTable): array; abstract protected function getEventStore(): EventStoreInterface; + abstract protected function deserializeProperties(array $properties): PropertyValuesToWrite; + /** - * @When /^the command "([^"]*)" is executed with payload:$/ - * @Given /^the command "([^"]*)" was executed with payload:$/ - * @param array|null $commandArguments + * @When the command :shortCommandName is executed with payload: * @throws \Exception */ - public function theCommandIsExecutedWithPayload(string $shortCommandName, TableNode $payloadTable = null, array $commandArguments = null): void + public function theCommandIsExecutedWithPayload(string $shortCommandName, TableNode $payloadTable): void { - $commandClassName = self::resolveShortCommandName($shortCommandName); - if ($commandArguments === null && $payloadTable !== null) { - $commandArguments = $this->readPayloadTable($payloadTable); - } - - if (isset($commandArguments['propertyValues.dateProperty'])) { - // special case to test Date type conversion - $commandArguments['propertyValues']['dateProperty'] = \DateTime::createFromFormat('Y-m-d\TH:i:sP', $commandArguments['propertyValues.dateProperty']); - } + $commandArguments = $this->readPayloadTable($payloadTable); + $this->handleCommand($shortCommandName, $commandArguments); + } - if (!method_exists($commandClassName, 'fromArray')) { - throw new \InvalidArgumentException(sprintf('Command "%s" does not implement a static "fromArray" constructor', $commandClassName), 1545564621); + /** + * @When the command :shortCommandName is executed with payload and exceptions are caught: + */ + public function theCommandIsExecutedWithPayloadAndExceptionsAreCaught(string $shortCommandName, TableNode $payloadTable): void + { + $commandArguments = $this->readPayloadTable($payloadTable); + try { + $this->handleCommand($shortCommandName, $commandArguments); + } catch (\Exception $exception) { + $this->lastCommandException = $exception; } + } - $command = $commandClassName::fromArray($commandArguments); - - $this->currentContentRepository->handle($command); + /** + * @When the command :shortCommandName is executed with payload :payload + */ + public function theCommandIsExecutedWithJsonPayload(string $shortCommandName, string $payload): void + { + $commandArguments = json_decode($payload, true, 512, JSON_THROW_ON_ERROR); + $this->handleCommand($shortCommandName, $commandArguments); } /** - * @When /^the command "([^"]*)" is executed with payload and exceptions are caught:$/ + * @When the command :shortCommandName is executed with payload :payload and exceptions are caught */ - public function theCommandIsExecutedWithPayloadAndExceptionsAreCaught($shortCommandName, TableNode $payloadTable): void + public function theCommandIsExecutedWithJsonPayloadAndExceptionsAreCaught(string $shortCommandName, string $payload): void { + $commandArguments = json_decode($payload, true, 512, JSON_THROW_ON_ERROR); try { - $this->theCommandIsExecutedWithPayload($shortCommandName, $payloadTable); + $this->handleCommand($shortCommandName, $commandArguments); } catch (\Exception $exception) { $this->lastCommandException = $exception; } } + /** + * @When the following :shortCommandName commands are executed: + */ + public function theFollowingCreateNodeAggregateWithNodeCommandsAreExecuted(string $shortCommandName, TableNode $table): void + { + foreach ($table->getHash() as $row) { + $this->handleCommand($shortCommandName, $row); + } + } + + private function handleCommand(string $shortCommandName, array $commandArguments): void + { + $commandClassName = self::resolveShortCommandName($shortCommandName); + $commandArguments['workspaceName'] = $commandArguments['workspaceName'] ?? $this->currentWorkspaceName?->value; + $commandArguments['coveredDimensionSpacePoint'] = $commandArguments['coveredDimensionSpacePoint'] ?? $this->currentDimensionSpacePoint?->coordinates; + $commandArguments['dimensionSpacePoint'] = $commandArguments['dimensionSpacePoint'] ?? $this->currentDimensionSpacePoint?->coordinates; + if (is_string($commandArguments['initialPropertyValues'] ?? null)) { + $commandArguments['initialPropertyValues'] = $this->deserializeProperties(json_decode($commandArguments['initialPropertyValues'], true, 512, JSON_THROW_ON_ERROR))->values; + } elseif (is_array($commandArguments['initialPropertyValues'] ?? null)) { + $commandArguments['initialPropertyValues'] = $this->deserializeProperties($commandArguments['initialPropertyValues'])->values; + } + if (is_string($commandArguments['propertyValues'] ?? null)) { + $commandArguments['propertyValues'] = $this->deserializeProperties(json_decode($commandArguments['propertyValues'], true, 512, JSON_THROW_ON_ERROR))->values; + } elseif (is_array($commandArguments['propertyValues'] ?? null)) { + $commandArguments['propertyValues'] = $this->deserializeProperties($commandArguments['propertyValues'])->values; + } + if (is_string($commandArguments['originDimensionSpacePoint'] ?? null) && !empty($commandArguments['originDimensionSpacePoint'])) { + $commandArguments['originDimensionSpacePoint'] = OriginDimensionSpacePoint::fromJsonString($commandArguments['originDimensionSpacePoint'])->coordinates; + } elseif (!isset($commandArguments['originDimensionSpacePoint'])) { + $commandArguments['originDimensionSpacePoint'] = $this->currentDimensionSpacePoint?->coordinates; + } + if (is_string($commandArguments['sourceOriginDimensionSpacePoint'] ?? null) && !empty($commandArguments['sourceOriginDimensionSpacePoint'])) { + $commandArguments['sourceOriginDimensionSpacePoint'] = OriginDimensionSpacePoint::fromJsonString($commandArguments['sourceOriginDimensionSpacePoint'])->coordinates; + } elseif (!isset($commandArguments['sourceOriginDimensionSpacePoint'])) { + $commandArguments['sourceOriginDimensionSpacePoint'] = $this->currentDimensionSpacePoint?->coordinates; + } + if (isset($commandArguments['succeedingSiblingNodeAggregateId']) && $commandArguments['succeedingSiblingNodeAggregateId'] === '') { + unset($commandArguments['succeedingSiblingNodeAggregateId']); + } + if (is_string($commandArguments['nodeAggregateId'] ?? null) && str_starts_with($commandArguments['nodeAggregateId'], '$')) { + $commandArguments['nodeAggregateId'] = $this->rememberedNodeAggregateIds[substr($commandArguments['nodeAggregateId'], 1)]?->value; + } elseif (!isset($commandArguments['nodeAggregateId'])) { + $commandArguments['nodeAggregateId'] = $this->currentNodeAggregate?->nodeAggregateId->value; + } + if (is_string($commandArguments['sourceNodeAggregateId'] ?? null) && str_starts_with($commandArguments['sourceNodeAggregateId'], '$')) { + $commandArguments['sourceNodeAggregateId'] = $this->rememberedNodeAggregateIds[substr($commandArguments['sourceNodeAggregateId'], 1)]?->value; + } elseif (!isset($commandArguments['sourceNodeAggregateId'])) { + $commandArguments['sourceNodeAggregateId'] = $this->currentNodeAggregate?->nodeAggregateId->value; + } + if (is_string($commandArguments['parentNodeAggregateId'] ?? null) && str_starts_with($commandArguments['parentNodeAggregateId'], '$')) { + $commandArguments['parentNodeAggregateId'] = $this->rememberedNodeAggregateIds[substr($commandArguments['parentNodeAggregateId'], 1)]?->value; + } elseif (!isset($commandArguments['parentNodeAggregateId'])) { + $commandArguments['parentNodeAggregateId'] = $this->currentNodeAggregate?->nodeAggregateId->value; + } + if (is_string($commandArguments['tetheredDescendantNodeAggregateIds'] ?? null)) { + if ($commandArguments['tetheredDescendantNodeAggregateIds'] === '') { + unset($commandArguments['tetheredDescendantNodeAggregateIds']); + } else { + $commandArguments['tetheredDescendantNodeAggregateIds'] = json_decode($commandArguments['tetheredDescendantNodeAggregateIds'], true, 512, JSON_THROW_ON_ERROR); + } + } + if (is_array($commandArguments['references'] ?? null)) { + $commandArguments['references'] = iterator_to_array($this->mapRawNodeReferencesToNodeReferencesToWrite($commandArguments['references'])); + } + + $command = $commandClassName::fromArray($commandArguments); + if ($command instanceof CreateRootNodeAggregateWithNode) { + $this->currentRootNodeAggregateId = $command->nodeAggregateId; + } + $this->currentContentRepository->handle($command); + } + + protected function mapRawNodeReferencesToNodeReferencesToWrite(array $deserializedTableContent): NodeReferencesToWrite + { + $referencesForProperty = []; + foreach ($deserializedTableContent as $nodeReferencesForProperty) { + $references = []; + foreach ($nodeReferencesForProperty['references'] as $referenceData) { + $properties = isset($referenceData['properties']) ? $this->deserializeProperties($referenceData['properties']) : PropertyValuesToWrite::createEmpty(); + $references[] = NodeReferenceToWrite::fromTargetAndProperties(NodeAggregateId::fromString($referenceData['target']), $properties); + } + $referencesForProperty[] = NodeReferencesForName::fromReferences(ReferenceName::fromString($nodeReferencesForProperty['referenceName']), $references); + } + return NodeReferencesToWrite::fromArray($referencesForProperty); + } + + /** + * @return class-string + */ protected static function resolveShortCommandName(string $shortCommandName): string { - return match ($shortCommandName) { - 'CreateRootWorkspace' => CreateRootWorkspace::class, - 'CreateWorkspace' => CreateWorkspace::class, - 'PublishWorkspace' => PublishWorkspace::class, - 'PublishIndividualNodesFromWorkspace' => PublishIndividualNodesFromWorkspace::class, - 'RebaseWorkspace' => RebaseWorkspace::class, - 'CreateNodeAggregateWithNodeAndSerializedProperties' => CreateNodeAggregateWithNodeAndSerializedProperties::class, - 'ChangeNodeAggregateName' => ChangeNodeAggregateName::class, - 'SetSerializedNodeProperties' => SetSerializedNodeProperties::class, - 'DisableNodeAggregate' => DisableNodeAggregate::class, - 'EnableNodeAggregate' => EnableNodeAggregate::class, - 'TagSubtree' => TagSubtree::class, - 'UntagSubtree' => UntagSubtree::class, - 'MoveNodeAggregate' => MoveNodeAggregate::class, - 'SetNodeReferences' => SetNodeReferences::class, - default => throw new \Exception( - 'The short command name "' . $shortCommandName . '" is currently not supported by the tests.' - ), - }; + $commandClassNames = [ + AddDimensionShineThrough::class, + ChangeBaseWorkspace::class, + ChangeNodeAggregateName::class, + ChangeNodeAggregateType::class, + CopyNodesRecursively::class, + CreateNodeAggregateWithNode::class, + CreateNodeAggregateWithNodeAndSerializedProperties::class, + CreateNodeVariant::class, + CreateRootNodeAggregateWithNode::class, + CreateRootWorkspace::class, + CreateWorkspace::class, + DeleteWorkspace::class, + DisableNodeAggregate::class, + DiscardIndividualNodesFromWorkspace::class, + DiscardWorkspace::class, + EnableNodeAggregate::class, + MoveDimensionSpacePoint::class, + MoveNodeAggregate::class, + PublishIndividualNodesFromWorkspace::class, + PublishWorkspace::class, + RebasableToOtherWorkspaceInterface::class, + RebaseWorkspace::class, + RemoveNodeAggregate::class, + SetNodeProperties::class, + SetNodeReferences::class, + SetSerializedNodeProperties::class, + SetSerializedNodeReferences::class, + TagSubtree::class, + UntagSubtree::class, + UpdateRootNodeAggregateDimensions::class, + ]; + foreach ($commandClassNames as $commandClassName) { + if (substr(strrchr($commandClassName, '\\'), 1) === $shortCommandName) { + return $commandClassName; + } + } + throw new \RuntimeException('The short command name "' . $shortCommandName . '" is currently not supported by the tests.'); } /** From cd680e5dd95d9c2bd0c978f0be99b0c3515bd014 Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Tue, 12 Nov 2024 09:26:15 +0100 Subject: [PATCH 154/214] Adjust tests that refered to internal, to be removed, commands --- ...NodeAggregateWithNode_WithoutDimensions.feature | 10 +++++----- .../RemoveNodeAggregateAfterDisabling.feature | 2 +- .../RemoveNodeAggregateWithDimensions.feature | 4 ++-- .../StructureAdjustment/DimensionMismatch.feature | 2 +- .../StructureAdjustment/Properties.feature | 2 +- .../TetheredNodesReordering.feature | 2 +- .../02-RebasingWithAutoCreatedNodes.feature | 14 +++++++------- .../04-AllFeaturePublication.feature | 4 ++-- .../GenericCommandExecutionAndEventPublication.php | 10 ---------- 9 files changed, 20 insertions(+), 30 deletions(-) diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/03-CreateNodeAggregateWithNode_WithoutDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/03-CreateNodeAggregateWithNode_WithoutDimensions.feature index 02aa8fb61b8..f8376aa7708 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/03-CreateNodeAggregateWithNode_WithoutDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/03-CreateNodeAggregateWithNode_WithoutDimensions.feature @@ -191,9 +191,9 @@ Feature: Create node aggregate with node And using identifier "default", I define a content repository And I am in content repository "default" And the command CreateRootWorkspace is executed with payload: - | Key | Value | - | workspaceName | "live" | - | newContentStreamId | "cs-identifier" | + | Key | Value | + | workspaceName | "live" | + | newContentStreamId | "cs-identifier" | And I am in workspace "live" And I am in dimension space point {} And I am user identified by "initiating-user-identifier" @@ -280,7 +280,7 @@ Feature: Create node aggregate with node | nodeAggregateId | "lady-eleonode-rootford" | | nodeTypeName | "Neos.ContentRepository:Root" | - When the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: + When the command CreateNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "sir-david-nodenborough" | | nodeTypeName | "Neos.ContentRepository.Testing:NodeWithTetheredChildNodes" | @@ -459,7 +459,7 @@ Feature: Create node aggregate with node | nodeAggregateId | "lady-eleonode-rootford" | | nodeTypeName | "Neos.ContentRepository:Root" | - When the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: + When the command CreateNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "sir-david-nodenborough" | | nodeTypeName | "Neos.ContentRepository.Testing:NodeWithTetheredChildNodes" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRemoval/RemoveNodeAggregateAfterDisabling.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRemoval/RemoveNodeAggregateAfterDisabling.feature index 8652a6a8252..156576e5069 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRemoval/RemoveNodeAggregateAfterDisabling.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRemoval/RemoveNodeAggregateAfterDisabling.feature @@ -50,7 +50,7 @@ Feature: Disable a node aggregate | affectedOccupiedDimensionSpacePoints | [{}] | | affectedCoveredDimensionSpacePoints | [{}] | - When the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: + When the command CreateNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "nody-mc-nodeface" | | nodeTypeName | "Neos.ContentRepository.Testing:Document" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRemoval/RemoveNodeAggregateWithDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRemoval/RemoveNodeAggregateWithDimensions.feature index 96e0ecaafb7..742b38d69b6 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRemoval/RemoveNodeAggregateWithDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRemoval/RemoveNodeAggregateWithDimensions.feature @@ -24,7 +24,7 @@ Feature: Remove NodeAggregate | nodeTypeName | "Neos.ContentRepository:Root" | # We have to add another node since root nodes are in all dimension space points and thus cannot be varied # Node /document - And the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: + And the command CreateNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "nody-mc-nodeface" | | nodeTypeName | "Neos.ContentRepository.Testing:Document" | @@ -32,7 +32,7 @@ Feature: Remove NodeAggregate | nodeName | "document" | # We also want to add a child node to make sure it is correctly removed when the parent is removed # Node /document/child-document - And the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: + And the command CreateNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "nodimus-prime" | | nodeTypeName | "Neos.ContentRepository.Testing:Document" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/DimensionMismatch.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/DimensionMismatch.feature index 6f10381a3cb..23347f92229 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/DimensionMismatch.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/DimensionMismatch.feature @@ -30,7 +30,7 @@ Feature: Dimension mismatch Scenario: Generalization detection # Node /document - When the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: + When the command CreateNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "sir-david-nodenborough" | | nodeTypeName | "Neos.ContentRepository.Testing:Document" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/Properties.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/Properties.feature index bd29ba2e76e..aaf81876ec4 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/Properties.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/Properties.feature @@ -28,7 +28,7 @@ Feature: Properties | nodeAggregateId | "lady-eleonode-rootford" | | nodeTypeName | "Neos.ContentRepository:Root" | # Node /document - When the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: + When the command CreateNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "sir-david-nodenborough" | | nodeTypeName | "Neos.ContentRepository.Testing:Document" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/TetheredNodesReordering.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/TetheredNodesReordering.feature index bb4974d7175..197a15e4989 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/TetheredNodesReordering.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/TetheredNodesReordering.feature @@ -28,7 +28,7 @@ Feature: Tethered Nodes Reordering Structure changes | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | | nodeTypeName | "Neos.ContentRepository:Root" | - And the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: + And the command CreateNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "sir-david-nodenborough" | | nodeTypeName | "Neos.ContentRepository.Testing:Document" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W6-WorkspaceRebasing/02-RebasingWithAutoCreatedNodes.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W6-WorkspaceRebasing/02-RebasingWithAutoCreatedNodes.feature index 39eca525c82..ae970ab3201 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W6-WorkspaceRebasing/02-RebasingWithAutoCreatedNodes.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W6-WorkspaceRebasing/02-RebasingWithAutoCreatedNodes.feature @@ -61,13 +61,13 @@ Feature: Rebasing auto-created nodes works And I expect this node to be a child of node user-cs-identifier;nody-mc-nodeface;{} # - then, for the auto-created child node, set a property. - When the command "SetSerializedNodeProperties" is executed with payload: - | Key | Value | - | workspaceName | "user-test" | - | nodeAggregateId | $this->currentNodeAggregateId | - | originDimensionSpacePoint | {} | - | propertyValues | {"text": {"value":"Modified","type":"string"}} | - | propertiesToUnset | {} | + When the command SetNodeProperties is executed with payload: + | Key | Value | + | workspaceName | "user-test" | + | nodeAggregateId | $this->currentNodeAggregateId | + | originDimensionSpacePoint | {} | + | propertyValues | {"text": "Modified"} | + | propertiesToUnset | {} | # ensure that live is outdated so the rebase is required: When the command CreateNodeAggregateWithNode is executed with payload: diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/04-AllFeaturePublication.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/04-AllFeaturePublication.feature index a5e32169667..37de2e73450 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/04-AllFeaturePublication.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/04-AllFeaturePublication.feature @@ -346,7 +346,7 @@ Feature: Publishing hide/show scenario of nodes | newContentStreamId | "user-cs-identifier" | # SETUP: set two new nodes in USER workspace - When the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: + When the command CreateNodeAggregateWithNode is executed with payload: | Key | Value | | workspaceName | "user-test" | | nodeAggregateId | "new1-agg" | @@ -354,7 +354,7 @@ Feature: Publishing hide/show scenario of nodes | originDimensionSpacePoint | {} | | parentNodeAggregateId | "lady-eleonode-rootford" | | nodeName | "foo" | - When the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: + When the command CreateNodeAggregateWithNode is executed with payload: | Key | Value | | workspaceName | "user-test" | | nodeAggregateId | "new2-agg" | diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php index 008bc184b84..71f9a37489e 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php @@ -16,7 +16,6 @@ use Behat\Gherkin\Node\TableNode; use Neos\ContentRepository\Core\CommandHandler\CommandInterface; -use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; use Neos\ContentRepository\Core\EventStore\EventNormalizer; @@ -27,17 +26,13 @@ use Neos\ContentRepository\Core\Feature\DimensionSpaceAdjustment\Command\AddDimensionShineThrough; use Neos\ContentRepository\Core\Feature\DimensionSpaceAdjustment\Command\MoveDimensionSpacePoint; use Neos\ContentRepository\Core\Feature\NodeCreation\Command\CreateNodeAggregateWithNode; -use Neos\ContentRepository\Core\Feature\NodeCreation\Command\CreateNodeAggregateWithNodeAndSerializedProperties; -use Neos\ContentRepository\Core\Feature\NodeCreation\Dto\NodeAggregateIdsByNodePaths; use Neos\ContentRepository\Core\Feature\NodeDisabling\Command\DisableNodeAggregate; use Neos\ContentRepository\Core\Feature\NodeDisabling\Command\EnableNodeAggregate; use Neos\ContentRepository\Core\Feature\NodeDuplication\Command\CopyNodesRecursively; use Neos\ContentRepository\Core\Feature\NodeModification\Command\SetNodeProperties; -use Neos\ContentRepository\Core\Feature\NodeModification\Command\SetSerializedNodeProperties; use Neos\ContentRepository\Core\Feature\NodeModification\Dto\PropertyValuesToWrite; use Neos\ContentRepository\Core\Feature\NodeMove\Command\MoveNodeAggregate; use Neos\ContentRepository\Core\Feature\NodeReferencing\Command\SetNodeReferences; -use Neos\ContentRepository\Core\Feature\NodeReferencing\Command\SetSerializedNodeReferences; use Neos\ContentRepository\Core\Feature\NodeReferencing\Dto\NodeReferencesForName; use Neos\ContentRepository\Core\Feature\NodeReferencing\Dto\NodeReferencesToWrite; use Neos\ContentRepository\Core\Feature\NodeReferencing\Dto\NodeReferenceToWrite; @@ -60,9 +55,7 @@ use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Command\RebaseWorkspace; use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Exception\WorkspaceRebaseFailed; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; -use Neos\ContentRepository\Core\SharedModel\Node\NodeName; use Neos\ContentRepository\Core\SharedModel\Node\ReferenceName; -use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\EventStore\EventStoreInterface; use Neos\EventStore\Model\Event; use Neos\EventStore\Model\Event\StreamName; @@ -230,7 +223,6 @@ protected static function resolveShortCommandName(string $shortCommandName): str ChangeNodeAggregateType::class, CopyNodesRecursively::class, CreateNodeAggregateWithNode::class, - CreateNodeAggregateWithNodeAndSerializedProperties::class, CreateNodeVariant::class, CreateRootNodeAggregateWithNode::class, CreateRootWorkspace::class, @@ -249,8 +241,6 @@ protected static function resolveShortCommandName(string $shortCommandName): str RemoveNodeAggregate::class, SetNodeProperties::class, SetNodeReferences::class, - SetSerializedNodeProperties::class, - SetSerializedNodeReferences::class, TagSubtree::class, UntagSubtree::class, UpdateRootNodeAggregateDimensions::class, From e915b940a384c38d263c91cd8d0d488bcee14df5 Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Tue, 12 Nov 2024 09:26:15 +0100 Subject: [PATCH 155/214] Adjust one more test that referred to internal command --- ...NodeAggregateWithNode_WithoutDimensions.feature | 10 +++++----- .../RemoveNodeAggregateAfterDisabling.feature | 2 +- .../RemoveNodeAggregateWithDimensions.feature | 4 ++-- .../StructureAdjustment/DimensionMismatch.feature | 2 +- .../StructureAdjustment/Properties.feature | 2 +- .../TetheredNodesReordering.feature | 2 +- .../02-RebasingWithAutoCreatedNodes.feature | 14 +++++++------- .../04-AllFeaturePublication.feature | 4 ++-- .../GenericCommandExecutionAndEventPublication.php | 10 ---------- .../Features/Fusion/ContentCollection.feature | 3 +-- 10 files changed, 21 insertions(+), 32 deletions(-) diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/03-CreateNodeAggregateWithNode_WithoutDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/03-CreateNodeAggregateWithNode_WithoutDimensions.feature index 02aa8fb61b8..f8376aa7708 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/03-CreateNodeAggregateWithNode_WithoutDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/03-CreateNodeAggregateWithNode_WithoutDimensions.feature @@ -191,9 +191,9 @@ Feature: Create node aggregate with node And using identifier "default", I define a content repository And I am in content repository "default" And the command CreateRootWorkspace is executed with payload: - | Key | Value | - | workspaceName | "live" | - | newContentStreamId | "cs-identifier" | + | Key | Value | + | workspaceName | "live" | + | newContentStreamId | "cs-identifier" | And I am in workspace "live" And I am in dimension space point {} And I am user identified by "initiating-user-identifier" @@ -280,7 +280,7 @@ Feature: Create node aggregate with node | nodeAggregateId | "lady-eleonode-rootford" | | nodeTypeName | "Neos.ContentRepository:Root" | - When the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: + When the command CreateNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "sir-david-nodenborough" | | nodeTypeName | "Neos.ContentRepository.Testing:NodeWithTetheredChildNodes" | @@ -459,7 +459,7 @@ Feature: Create node aggregate with node | nodeAggregateId | "lady-eleonode-rootford" | | nodeTypeName | "Neos.ContentRepository:Root" | - When the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: + When the command CreateNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "sir-david-nodenborough" | | nodeTypeName | "Neos.ContentRepository.Testing:NodeWithTetheredChildNodes" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRemoval/RemoveNodeAggregateAfterDisabling.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRemoval/RemoveNodeAggregateAfterDisabling.feature index 8652a6a8252..156576e5069 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRemoval/RemoveNodeAggregateAfterDisabling.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRemoval/RemoveNodeAggregateAfterDisabling.feature @@ -50,7 +50,7 @@ Feature: Disable a node aggregate | affectedOccupiedDimensionSpacePoints | [{}] | | affectedCoveredDimensionSpacePoints | [{}] | - When the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: + When the command CreateNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "nody-mc-nodeface" | | nodeTypeName | "Neos.ContentRepository.Testing:Document" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRemoval/RemoveNodeAggregateWithDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRemoval/RemoveNodeAggregateWithDimensions.feature index 96e0ecaafb7..742b38d69b6 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRemoval/RemoveNodeAggregateWithDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRemoval/RemoveNodeAggregateWithDimensions.feature @@ -24,7 +24,7 @@ Feature: Remove NodeAggregate | nodeTypeName | "Neos.ContentRepository:Root" | # We have to add another node since root nodes are in all dimension space points and thus cannot be varied # Node /document - And the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: + And the command CreateNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "nody-mc-nodeface" | | nodeTypeName | "Neos.ContentRepository.Testing:Document" | @@ -32,7 +32,7 @@ Feature: Remove NodeAggregate | nodeName | "document" | # We also want to add a child node to make sure it is correctly removed when the parent is removed # Node /document/child-document - And the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: + And the command CreateNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "nodimus-prime" | | nodeTypeName | "Neos.ContentRepository.Testing:Document" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/DimensionMismatch.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/DimensionMismatch.feature index 6f10381a3cb..23347f92229 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/DimensionMismatch.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/DimensionMismatch.feature @@ -30,7 +30,7 @@ Feature: Dimension mismatch Scenario: Generalization detection # Node /document - When the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: + When the command CreateNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "sir-david-nodenborough" | | nodeTypeName | "Neos.ContentRepository.Testing:Document" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/Properties.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/Properties.feature index bd29ba2e76e..aaf81876ec4 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/Properties.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/Properties.feature @@ -28,7 +28,7 @@ Feature: Properties | nodeAggregateId | "lady-eleonode-rootford" | | nodeTypeName | "Neos.ContentRepository:Root" | # Node /document - When the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: + When the command CreateNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "sir-david-nodenborough" | | nodeTypeName | "Neos.ContentRepository.Testing:Document" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/TetheredNodesReordering.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/TetheredNodesReordering.feature index bb4974d7175..197a15e4989 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/TetheredNodesReordering.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/TetheredNodesReordering.feature @@ -28,7 +28,7 @@ Feature: Tethered Nodes Reordering Structure changes | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | | nodeTypeName | "Neos.ContentRepository:Root" | - And the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: + And the command CreateNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "sir-david-nodenborough" | | nodeTypeName | "Neos.ContentRepository.Testing:Document" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W6-WorkspaceRebasing/02-RebasingWithAutoCreatedNodes.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W6-WorkspaceRebasing/02-RebasingWithAutoCreatedNodes.feature index 39eca525c82..ae970ab3201 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W6-WorkspaceRebasing/02-RebasingWithAutoCreatedNodes.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W6-WorkspaceRebasing/02-RebasingWithAutoCreatedNodes.feature @@ -61,13 +61,13 @@ Feature: Rebasing auto-created nodes works And I expect this node to be a child of node user-cs-identifier;nody-mc-nodeface;{} # - then, for the auto-created child node, set a property. - When the command "SetSerializedNodeProperties" is executed with payload: - | Key | Value | - | workspaceName | "user-test" | - | nodeAggregateId | $this->currentNodeAggregateId | - | originDimensionSpacePoint | {} | - | propertyValues | {"text": {"value":"Modified","type":"string"}} | - | propertiesToUnset | {} | + When the command SetNodeProperties is executed with payload: + | Key | Value | + | workspaceName | "user-test" | + | nodeAggregateId | $this->currentNodeAggregateId | + | originDimensionSpacePoint | {} | + | propertyValues | {"text": "Modified"} | + | propertiesToUnset | {} | # ensure that live is outdated so the rebase is required: When the command CreateNodeAggregateWithNode is executed with payload: diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/04-AllFeaturePublication.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/04-AllFeaturePublication.feature index a5e32169667..37de2e73450 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/04-AllFeaturePublication.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/04-AllFeaturePublication.feature @@ -346,7 +346,7 @@ Feature: Publishing hide/show scenario of nodes | newContentStreamId | "user-cs-identifier" | # SETUP: set two new nodes in USER workspace - When the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: + When the command CreateNodeAggregateWithNode is executed with payload: | Key | Value | | workspaceName | "user-test" | | nodeAggregateId | "new1-agg" | @@ -354,7 +354,7 @@ Feature: Publishing hide/show scenario of nodes | originDimensionSpacePoint | {} | | parentNodeAggregateId | "lady-eleonode-rootford" | | nodeName | "foo" | - When the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: + When the command CreateNodeAggregateWithNode is executed with payload: | Key | Value | | workspaceName | "user-test" | | nodeAggregateId | "new2-agg" | diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php index 008bc184b84..71f9a37489e 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php @@ -16,7 +16,6 @@ use Behat\Gherkin\Node\TableNode; use Neos\ContentRepository\Core\CommandHandler\CommandInterface; -use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; use Neos\ContentRepository\Core\EventStore\EventNormalizer; @@ -27,17 +26,13 @@ use Neos\ContentRepository\Core\Feature\DimensionSpaceAdjustment\Command\AddDimensionShineThrough; use Neos\ContentRepository\Core\Feature\DimensionSpaceAdjustment\Command\MoveDimensionSpacePoint; use Neos\ContentRepository\Core\Feature\NodeCreation\Command\CreateNodeAggregateWithNode; -use Neos\ContentRepository\Core\Feature\NodeCreation\Command\CreateNodeAggregateWithNodeAndSerializedProperties; -use Neos\ContentRepository\Core\Feature\NodeCreation\Dto\NodeAggregateIdsByNodePaths; use Neos\ContentRepository\Core\Feature\NodeDisabling\Command\DisableNodeAggregate; use Neos\ContentRepository\Core\Feature\NodeDisabling\Command\EnableNodeAggregate; use Neos\ContentRepository\Core\Feature\NodeDuplication\Command\CopyNodesRecursively; use Neos\ContentRepository\Core\Feature\NodeModification\Command\SetNodeProperties; -use Neos\ContentRepository\Core\Feature\NodeModification\Command\SetSerializedNodeProperties; use Neos\ContentRepository\Core\Feature\NodeModification\Dto\PropertyValuesToWrite; use Neos\ContentRepository\Core\Feature\NodeMove\Command\MoveNodeAggregate; use Neos\ContentRepository\Core\Feature\NodeReferencing\Command\SetNodeReferences; -use Neos\ContentRepository\Core\Feature\NodeReferencing\Command\SetSerializedNodeReferences; use Neos\ContentRepository\Core\Feature\NodeReferencing\Dto\NodeReferencesForName; use Neos\ContentRepository\Core\Feature\NodeReferencing\Dto\NodeReferencesToWrite; use Neos\ContentRepository\Core\Feature\NodeReferencing\Dto\NodeReferenceToWrite; @@ -60,9 +55,7 @@ use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Command\RebaseWorkspace; use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Exception\WorkspaceRebaseFailed; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; -use Neos\ContentRepository\Core\SharedModel\Node\NodeName; use Neos\ContentRepository\Core\SharedModel\Node\ReferenceName; -use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\EventStore\EventStoreInterface; use Neos\EventStore\Model\Event; use Neos\EventStore\Model\Event\StreamName; @@ -230,7 +223,6 @@ protected static function resolveShortCommandName(string $shortCommandName): str ChangeNodeAggregateType::class, CopyNodesRecursively::class, CreateNodeAggregateWithNode::class, - CreateNodeAggregateWithNodeAndSerializedProperties::class, CreateNodeVariant::class, CreateRootNodeAggregateWithNode::class, CreateRootWorkspace::class, @@ -249,8 +241,6 @@ protected static function resolveShortCommandName(string $shortCommandName): str RemoveNodeAggregate::class, SetNodeProperties::class, SetNodeReferences::class, - SetSerializedNodeProperties::class, - SetSerializedNodeReferences::class, TagSubtree::class, UntagSubtree::class, UpdateRootNodeAggregateDimensions::class, diff --git a/Neos.Neos/Tests/Behavior/Features/Fusion/ContentCollection.feature b/Neos.Neos/Tests/Behavior/Features/Fusion/ContentCollection.feature index b1edf071af4..f5c77c72d9a 100644 --- a/Neos.Neos/Tests/Behavior/Features/Fusion/ContentCollection.feature +++ b/Neos.Neos/Tests/Behavior/Features/Fusion/ContentCollection.feature @@ -109,12 +109,11 @@ Feature: Tests for the "Neos.Neos:ContentCollection" Fusion prototype """ Scenario: - When the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: + When the command CreateNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "a1" | | nodeTypeName | "Neos.Neos:Test.DocumentType" | | parentNodeAggregateId | "a" | - | initialPropertyValues | {} | | tetheredDescendantNodeAggregateIds | { "main": "a1-main"} | When the following CreateNodeAggregateWithNode commands are executed: | nodeAggregateId | parentNodeAggregateId | nodeTypeName | From 8f123a0e8596f8b99ba7a6a66a8fc003f4225573 Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Tue, 12 Nov 2024 10:11:27 +0100 Subject: [PATCH 156/214] Remove magic "$this->" construct from behat tests --- .../02-RebasingWithAutoCreatedNodes.feature | 1 - .../Bootstrap/CRTestSuiteRuntimeVariables.php | 5 ++--- .../Features/Bootstrap/CRTestSuiteTrait.php | 16 +--------------- ...enericCommandExecutionAndEventPublication.php | 2 +- 4 files changed, 4 insertions(+), 20 deletions(-) diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W6-WorkspaceRebasing/02-RebasingWithAutoCreatedNodes.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W6-WorkspaceRebasing/02-RebasingWithAutoCreatedNodes.feature index ae970ab3201..9a8e3dd3c4e 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W6-WorkspaceRebasing/02-RebasingWithAutoCreatedNodes.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W6-WorkspaceRebasing/02-RebasingWithAutoCreatedNodes.feature @@ -64,7 +64,6 @@ Feature: Rebasing auto-created nodes works When the command SetNodeProperties is executed with payload: | Key | Value | | workspaceName | "user-test" | - | nodeAggregateId | $this->currentNodeAggregateId | | originDimensionSpacePoint | {} | | propertyValues | {"text": "Modified"} | | propertiesToUnset | {} | diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteRuntimeVariables.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteRuntimeVariables.php index e4505319463..70c5c0277b2 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteRuntimeVariables.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteRuntimeVariables.php @@ -141,9 +141,8 @@ public function iRememberNodeAggregateIdOfNodesChildAs(string $parentNodeAggrega )->aggregateId; } - protected function getCurrentNodeAggregateId(): NodeAggregateId + protected function getCurrentNodeAggregateId(): ?NodeAggregateId { - assert($this->currentNode instanceof Node); - return $this->currentNode->aggregateId; + return $this->currentNode?->aggregateId; } } diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteTrait.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteTrait.php index c22577dc67d..e15be252379 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteTrait.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteTrait.php @@ -97,21 +97,7 @@ protected function readPayloadTable(TableNode $payloadTable): array { $eventPayload = []; foreach ($payloadTable->getHash() as $line) { - if (\str_starts_with($line['Value'], '$this->')) { - // Special case: Referencing stuff from the context here - $propertyOrMethodName = \mb_substr($line['Value'], \mb_strlen('$this->')); - $value = match ($propertyOrMethodName) { - 'currentNodeAggregateId' => $this->getCurrentNodeAggregateId()->value, - default => method_exists($this, $propertyOrMethodName) ? (string)$this->$propertyOrMethodName() : (string)$this->$propertyOrMethodName, - }; - } else { - // default case - $value = json_decode($line['Value'], true); - if ($value === null && json_last_error() !== JSON_ERROR_NONE) { - throw new \Exception(sprintf('The value "%s" is no valid JSON string', $line['Value']), 1546522626); - } - } - $eventPayload[$line['Key']] = $value; + $eventPayload[$line['Key']] = json_decode($line['Value'], true, 512, JSON_THROW_ON_ERROR); } return $eventPayload; diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php index 71f9a37489e..2e0b35785cb 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php @@ -167,7 +167,7 @@ private function handleCommand(string $shortCommandName, array $commandArguments if (is_string($commandArguments['nodeAggregateId'] ?? null) && str_starts_with($commandArguments['nodeAggregateId'], '$')) { $commandArguments['nodeAggregateId'] = $this->rememberedNodeAggregateIds[substr($commandArguments['nodeAggregateId'], 1)]?->value; } elseif (!isset($commandArguments['nodeAggregateId'])) { - $commandArguments['nodeAggregateId'] = $this->currentNodeAggregate?->nodeAggregateId->value; + $commandArguments['nodeAggregateId'] = $this->getCurrentNodeAggregateId()?->value; } if (is_string($commandArguments['sourceNodeAggregateId'] ?? null) && str_starts_with($commandArguments['sourceNodeAggregateId'], '$')) { $commandArguments['sourceNodeAggregateId'] = $this->rememberedNodeAggregateIds[substr($commandArguments['sourceNodeAggregateId'], 1)]?->value; From a6278c0932585b562c3b0ba328dfc378088c08b8 Mon Sep 17 00:00:00 2001 From: Jenkins Date: Tue, 12 Nov 2024 09:26:41 +0000 Subject: [PATCH 157/214] TASK: Update references [skip ci] --- .../References/CommandReference.rst | 172 +++++++++--------- .../References/ViewHelpers/FluidAdaptor.rst | 2 +- .../References/ViewHelpers/Form.rst | 2 +- .../References/ViewHelpers/Media.rst | 2 +- .../References/ViewHelpers/Neos.rst | 2 +- .../References/ViewHelpers/TYPO3Fluid.rst | 2 +- 6 files changed, 86 insertions(+), 96 deletions(-) diff --git a/Neos.Neos/Documentation/References/CommandReference.rst b/Neos.Neos/Documentation/References/CommandReference.rst index ee2cc993df6..cb220a6bc92 100644 --- a/Neos.Neos/Documentation/References/CommandReference.rst +++ b/Neos.Neos/Documentation/References/CommandReference.rst @@ -19,7 +19,7 @@ commands that may be available, use:: ./flow help -The following reference was automatically generated from code on 2024-11-08 +The following reference was automatically generated from code on 2024-11-12 .. _`Neos Command Reference: NEOS.FLOW`: @@ -2154,87 +2154,6 @@ Package *NEOS.NEOS* ------------------- -.. _`Neos Command Reference: NEOS.NEOS neos.neos:cr:export`: - -``neos.neos:cr:export`` -*********************** - -**Export the events from the specified content repository** - - - -Arguments -^^^^^^^^^ - -``--path`` - The path for storing the result - - - -Options -^^^^^^^ - -``--content-repository`` - The content repository identifier -``--verbose`` - If set, all notices will be rendered - - - - - -.. _`Neos Command Reference: NEOS.NEOS neos.neos:cr:import`: - -``neos.neos:cr:import`` -*********************** - -**Import the events from the path into the specified content repository** - - - -Arguments -^^^^^^^^^ - -``--path`` - The path of the stored events like resource://Neos.Demo/Private/Content - - - -Options -^^^^^^^ - -``--content-repository`` - The content repository identifier -``--verbose`` - If set, all notices will be rendered - - - - - -.. _`Neos Command Reference: NEOS.NEOS neos.neos:cr:prune`: - -``neos.neos:cr:prune`` -********************** - -**This will completely prune the data of the specified content repository.** - - - - - -Options -^^^^^^^ - -``--content-repository`` - Name of the content repository where the data should be pruned from. -``--force`` - Prune the cr without confirmation. This cannot be reverted! - - - - - .. _`Neos Command Reference: NEOS.NEOS neos.neos:domain:activate`: ``neos.neos:domain:activate`` @@ -2434,6 +2353,73 @@ Arguments +.. _`Neos Command Reference: NEOS.NEOS neos.neos:site:exportall`: + +``neos.neos:site:exportall`` +**************************** + +**Export sites** + +This command exports all sites of the content repository. + +If a path is specified, this command creates the directory if needed and exports into that. + +If a package key is specified, this command exports to the private resources +directory of the given package (Resources/Private/Content). + + + +Options +^^^^^^^ + +``--package-key`` + Package key specifying the package containing the sites content +``--path`` + relative or absolute path and filename to the export files +``--content-repository`` + contentRepository +``--verbose`` + verbose + + + + + +.. _`Neos Command Reference: NEOS.NEOS neos.neos:site:importall`: + +``neos.neos:site:importall`` +**************************** + +**Import sites** + +This command allows importing sites from the given path/package. The format must +be identical to that produced by the exportAll command. + +If a path is specified, this command expects the corresponding directory to contain the exported files + +If a package key is specified, this command expects the export files to be located in the private resources +directory of the given package (Resources/Private/Content). + +**Note that the live workspace has to be empty prior to importing.** + + + +Options +^^^^^^^ + +``--package-key`` + Package key specifying the package containing the sites content +``--path`` + relative or absolute path and filename to the export files +``--content-repository`` + contentRepository +``--verbose`` + verbose + + + + + .. _`Neos Command Reference: NEOS.NEOS neos.neos:site:list`: ``neos.neos:site:list`` @@ -2449,22 +2435,26 @@ Arguments -.. _`Neos Command Reference: NEOS.NEOS neos.neos:site:prune`: +.. _`Neos Command Reference: NEOS.NEOS neos.neos:site:pruneall`: -``neos.neos:site:prune`` -************************ +``neos.neos:site:pruneall`` +*************************** -**Remove site with content and related data (with globbing)** +**This will completely prune the data of the specified content repository and remove all site-records.** -In the future we need some more sophisticated cleanup. -Arguments -^^^^^^^^^ -``--site-node`` - Name for site root nodes to clear only content of this sites (globbing is supported) +Options +^^^^^^^ + +``--content-repository`` + Prune the cr without confirmation. This cannot be reverted! +``--force`` + force +``--verbose`` + verbose diff --git a/Neos.Neos/Documentation/References/ViewHelpers/FluidAdaptor.rst b/Neos.Neos/Documentation/References/ViewHelpers/FluidAdaptor.rst index d6642d59d89..dd993f659eb 100644 --- a/Neos.Neos/Documentation/References/ViewHelpers/FluidAdaptor.rst +++ b/Neos.Neos/Documentation/References/ViewHelpers/FluidAdaptor.rst @@ -3,7 +3,7 @@ FluidAdaptor ViewHelper Reference ################################# -This reference was automatically generated from code on 2024-11-08 +This reference was automatically generated from code on 2024-11-12 .. _`FluidAdaptor ViewHelper Reference: f:debug`: diff --git a/Neos.Neos/Documentation/References/ViewHelpers/Form.rst b/Neos.Neos/Documentation/References/ViewHelpers/Form.rst index da276092fcc..d513ff0fe85 100644 --- a/Neos.Neos/Documentation/References/ViewHelpers/Form.rst +++ b/Neos.Neos/Documentation/References/ViewHelpers/Form.rst @@ -3,7 +3,7 @@ Form ViewHelper Reference ######################### -This reference was automatically generated from code on 2024-11-08 +This reference was automatically generated from code on 2024-11-12 .. _`Form ViewHelper Reference: neos.form:form`: diff --git a/Neos.Neos/Documentation/References/ViewHelpers/Media.rst b/Neos.Neos/Documentation/References/ViewHelpers/Media.rst index 7bd4022063b..add07aae4fa 100644 --- a/Neos.Neos/Documentation/References/ViewHelpers/Media.rst +++ b/Neos.Neos/Documentation/References/ViewHelpers/Media.rst @@ -3,7 +3,7 @@ Media ViewHelper Reference ########################## -This reference was automatically generated from code on 2024-11-08 +This reference was automatically generated from code on 2024-11-12 .. _`Media ViewHelper Reference: neos.media:fileTypeIcon`: diff --git a/Neos.Neos/Documentation/References/ViewHelpers/Neos.rst b/Neos.Neos/Documentation/References/ViewHelpers/Neos.rst index b982875a8d2..7addbdf3cb8 100644 --- a/Neos.Neos/Documentation/References/ViewHelpers/Neos.rst +++ b/Neos.Neos/Documentation/References/ViewHelpers/Neos.rst @@ -3,7 +3,7 @@ Neos ViewHelper Reference ######################### -This reference was automatically generated from code on 2024-11-08 +This reference was automatically generated from code on 2024-11-12 .. _`Neos ViewHelper Reference: neos:backend.authenticationProviderLabel`: diff --git a/Neos.Neos/Documentation/References/ViewHelpers/TYPO3Fluid.rst b/Neos.Neos/Documentation/References/ViewHelpers/TYPO3Fluid.rst index 98a91c21902..fac6148952b 100644 --- a/Neos.Neos/Documentation/References/ViewHelpers/TYPO3Fluid.rst +++ b/Neos.Neos/Documentation/References/ViewHelpers/TYPO3Fluid.rst @@ -3,7 +3,7 @@ TYPO3 Fluid ViewHelper Reference ################################ -This reference was automatically generated from code on 2024-11-08 +This reference was automatically generated from code on 2024-11-12 .. _`TYPO3 Fluid ViewHelper Reference: f:alias`: From af193a57ebc9e9c3d01748de02675825476421a1 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Tue, 12 Nov 2024 10:51:57 +0100 Subject: [PATCH 158/214] TASK: Unify `CatchUpHookFactoryDependencies` and `CommandHooksFactoryDependencies` --- .../Classes/ContentRepository.php | 2 +- .../CommandHooksFactoryDependencies.php | 12 ++++++++++ .../Factory/ContentRepositoryFactory.php | 3 +++ .../CatchUpHookFactoryDependencies.php | 24 ++++++++++++++++++- 4 files changed, 39 insertions(+), 2 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/ContentRepository.php b/Neos.ContentRepository.Core/Classes/ContentRepository.php index b2ef11475b5..c885d168514 100644 --- a/Neos.ContentRepository.Core/Classes/ContentRepository.php +++ b/Neos.ContentRepository.Core/Classes/ContentRepository.php @@ -150,7 +150,7 @@ public function catchUpProjection(string $projectionClassName, CatchUpOptions $o $projection = $this->projectionsAndCatchUpHooks->projections->get($projectionClassName); $catchUpHookFactory = $this->projectionsAndCatchUpHooks->getCatchUpHookFactoryForProjection($projection); - $catchUpHook = $catchUpHookFactory?->build(new CatchUpHookFactoryDependencies( + $catchUpHook = $catchUpHookFactory?->build(CatchUpHookFactoryDependencies::create( $this->id, $projection->getState(), $this->nodeTypeManager, diff --git a/Neos.ContentRepository.Core/Classes/Factory/CommandHooksFactoryDependencies.php b/Neos.ContentRepository.Core/Classes/Factory/CommandHooksFactoryDependencies.php index 5149682ff4b..3082edcd7f6 100644 --- a/Neos.ContentRepository.Core/Classes/Factory/CommandHooksFactoryDependencies.php +++ b/Neos.ContentRepository.Core/Classes/Factory/CommandHooksFactoryDependencies.php @@ -15,6 +15,9 @@ namespace Neos\ContentRepository\Core\Factory; use Neos\ContentRepository\Core\CommandHandler\CommandHookInterface; +use Neos\ContentRepository\Core\Dimension\ContentDimensionSourceInterface; +use Neos\ContentRepository\Core\DimensionSpace\InterDimensionalVariationGraph; +use Neos\ContentRepository\Core\NodeType\NodeTypeManager; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphReadModelInterface; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; @@ -26,6 +29,9 @@ private function __construct( public ContentRepositoryId $contentRepositoryId, public ContentGraphReadModelInterface $contentGraphReadModel, + public NodeTypeManager $nodeTypeManager, + public ContentDimensionSourceInterface $contentDimensionSource, + public InterDimensionalVariationGraph $variationGraph ) { } @@ -35,10 +41,16 @@ private function __construct( public static function create( ContentRepositoryId $contentRepositoryId, ContentGraphReadModelInterface $contentGraphReadModel, + NodeTypeManager $nodeTypeManager, + ContentDimensionSourceInterface $contentDimensionSource, + InterDimensionalVariationGraph $variationGraph ): self { return new self( $contentRepositoryId, $contentGraphReadModel, + $nodeTypeManager, + $contentDimensionSource, + $variationGraph ); } } diff --git a/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php b/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php index 2469a37f30d..1268c604ca0 100644 --- a/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php +++ b/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php @@ -138,6 +138,9 @@ public function getOrBuild(): ContentRepository $commandHooks = $this->commandHooksFactory->build(CommandHooksFactoryDependencies::create( $this->contentRepositoryId, $this->projectionsAndCatchUpHooks->contentGraphProjection->getState(), + $this->projectionFactoryDependencies->nodeTypeManager, + $this->projectionFactoryDependencies->contentDimensionSource, + $this->projectionFactoryDependencies->interDimensionalVariationGraph, )); $this->contentRepository = new ContentRepository( $this->contentRepositoryId, diff --git a/Neos.ContentRepository.Core/Classes/Projection/CatchUpHookFactoryDependencies.php b/Neos.ContentRepository.Core/Classes/Projection/CatchUpHookFactoryDependencies.php index 037e4164150..0a98f7e11c3 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/CatchUpHookFactoryDependencies.php +++ b/Neos.ContentRepository.Core/Classes/Projection/CatchUpHookFactoryDependencies.php @@ -30,7 +30,7 @@ * @param ContentRepositoryId $contentRepositoryId the content repository the catchup was registered in * @param ProjectionStateInterface&T $projectionState the state of the projection the catchup was registered to (Its only safe to access this projections state) */ - public function __construct( + private function __construct( public ContentRepositoryId $contentRepositoryId, public ProjectionStateInterface $projectionState, public NodeTypeManager $nodeTypeManager, @@ -38,4 +38,26 @@ public function __construct( public InterDimensionalVariationGraph $variationGraph ) { } + + /** + * @template U of ProjectionStateInterface + * @param ProjectionStateInterface&U $projectionState + * @return CatchUpHookFactoryDependencies + * @internal + */ + public static function create( + ContentRepositoryId $contentRepositoryId, + ProjectionStateInterface $projectionState, + NodeTypeManager $nodeTypeManager, + ContentDimensionSourceInterface $contentDimensionSource, + InterDimensionalVariationGraph $variationGraph + ): self { + return new self( + $contentRepositoryId, + $projectionState, + $nodeTypeManager, + $contentDimensionSource, + $variationGraph + ); + } } From 50742546a20ace297dbe7f9425e182d9266291e6 Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Tue, 12 Nov 2024 11:30:45 +0100 Subject: [PATCH 159/214] Cleanup default command arguments resolution --- ...ricCommandExecutionAndEventPublication.php | 104 ++++++++++-------- 1 file changed, 61 insertions(+), 43 deletions(-) diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php index ef50156e47f..8976f80caea 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php @@ -138,63 +138,81 @@ public function theFollowingCreateNodeAggregateWithNodeCommandsAreExecuted(strin private function handleCommand(string $shortCommandName, array $commandArguments): void { $commandClassName = self::resolveShortCommandName($shortCommandName); + $commandArguments = $this->addDefaultCommandArgumentValues($commandClassName, $commandArguments); + $command = $commandClassName::fromArray($commandArguments); + if ($command instanceof CreateRootNodeAggregateWithNode) { + $this->currentRootNodeAggregateId = $command->nodeAggregateId; + } + $this->currentContentRepository->handle($command); + } + + /** + * @param class-string $commandClassName + */ + protected function addDefaultCommandArgumentValues(string $commandClassName, array $commandArguments): array + { $commandArguments['workspaceName'] = $commandArguments['workspaceName'] ?? $this->currentWorkspaceName?->value; $commandArguments['coveredDimensionSpacePoint'] = $commandArguments['coveredDimensionSpacePoint'] ?? $this->currentDimensionSpacePoint?->coordinates; $commandArguments['dimensionSpacePoint'] = $commandArguments['dimensionSpacePoint'] ?? $this->currentDimensionSpacePoint?->coordinates; - if (is_string($commandArguments['initialPropertyValues'] ?? null)) { - $commandArguments['initialPropertyValues'] = $this->deserializeProperties(json_decode($commandArguments['initialPropertyValues'], true, 512, JSON_THROW_ON_ERROR))->values; - } elseif (is_array($commandArguments['initialPropertyValues'] ?? null)) { - $commandArguments['initialPropertyValues'] = $this->deserializeProperties($commandArguments['initialPropertyValues'])->values; - } - if (is_string($commandArguments['propertyValues'] ?? null)) { - $commandArguments['propertyValues'] = $this->deserializeProperties(json_decode($commandArguments['propertyValues'], true, 512, JSON_THROW_ON_ERROR))->values; - } elseif (is_array($commandArguments['propertyValues'] ?? null)) { - $commandArguments['propertyValues'] = $this->deserializeProperties($commandArguments['propertyValues'])->values; - } - if (is_string($commandArguments['originDimensionSpacePoint'] ?? null) && !empty($commandArguments['originDimensionSpacePoint'])) { - $commandArguments['originDimensionSpacePoint'] = OriginDimensionSpacePoint::fromJsonString($commandArguments['originDimensionSpacePoint'])->coordinates; - } elseif (!isset($commandArguments['originDimensionSpacePoint'])) { - $commandArguments['originDimensionSpacePoint'] = $this->currentDimensionSpacePoint?->coordinates; - } - if (is_string($commandArguments['sourceOriginDimensionSpacePoint'] ?? null) && !empty($commandArguments['sourceOriginDimensionSpacePoint'])) { - $commandArguments['sourceOriginDimensionSpacePoint'] = OriginDimensionSpacePoint::fromJsonString($commandArguments['sourceOriginDimensionSpacePoint'])->coordinates; - } elseif (!isset($commandArguments['sourceOriginDimensionSpacePoint'])) { - $commandArguments['sourceOriginDimensionSpacePoint'] = $this->currentDimensionSpacePoint?->coordinates; - } - if (isset($commandArguments['succeedingSiblingNodeAggregateId']) && $commandArguments['succeedingSiblingNodeAggregateId'] === '') { - unset($commandArguments['succeedingSiblingNodeAggregateId']); - } if (is_string($commandArguments['nodeAggregateId'] ?? null) && str_starts_with($commandArguments['nodeAggregateId'], '$')) { $commandArguments['nodeAggregateId'] = $this->rememberedNodeAggregateIds[substr($commandArguments['nodeAggregateId'], 1)]?->value; } elseif (!isset($commandArguments['nodeAggregateId'])) { $commandArguments['nodeAggregateId'] = $this->getCurrentNodeAggregateId()?->value; } - if (is_string($commandArguments['sourceNodeAggregateId'] ?? null) && str_starts_with($commandArguments['sourceNodeAggregateId'], '$')) { - $commandArguments['sourceNodeAggregateId'] = $this->rememberedNodeAggregateIds[substr($commandArguments['sourceNodeAggregateId'], 1)]?->value; - } elseif (!isset($commandArguments['sourceNodeAggregateId'])) { - $commandArguments['sourceNodeAggregateId'] = $this->currentNodeAggregate?->nodeAggregateId->value; + if ($commandClassName === CreateNodeAggregateWithNode::class) { + if (is_string($commandArguments['initialPropertyValues'] ?? null)) { + $commandArguments['initialPropertyValues'] = $this->deserializeProperties(json_decode($commandArguments['initialPropertyValues'], true, 512, JSON_THROW_ON_ERROR))->values; + } elseif (is_array($commandArguments['initialPropertyValues'] ?? null)) { + $commandArguments['initialPropertyValues'] = $this->deserializeProperties($commandArguments['initialPropertyValues'])->values; + } + if (isset($commandArguments['succeedingSiblingNodeAggregateId']) && $commandArguments['succeedingSiblingNodeAggregateId'] === '') { + unset($commandArguments['succeedingSiblingNodeAggregateId']); + } + if (is_string($commandArguments['parentNodeAggregateId'] ?? null) && str_starts_with($commandArguments['parentNodeAggregateId'], '$')) { + $commandArguments['parentNodeAggregateId'] = $this->rememberedNodeAggregateIds[substr($commandArguments['parentNodeAggregateId'], 1)]?->value; + } } - if (is_string($commandArguments['parentNodeAggregateId'] ?? null) && str_starts_with($commandArguments['parentNodeAggregateId'], '$')) { - $commandArguments['parentNodeAggregateId'] = $this->rememberedNodeAggregateIds[substr($commandArguments['parentNodeAggregateId'], 1)]?->value; - } elseif (!isset($commandArguments['parentNodeAggregateId'])) { - $commandArguments['parentNodeAggregateId'] = $this->currentNodeAggregate?->nodeAggregateId->value; + if ($commandClassName === SetNodeProperties::class) { + if (is_string($commandArguments['propertyValues'] ?? null)) { + $commandArguments['propertyValues'] = $this->deserializeProperties(json_decode($commandArguments['propertyValues'], true, 512, JSON_THROW_ON_ERROR))->values; + } elseif (is_array($commandArguments['propertyValues'] ?? null)) { + $commandArguments['propertyValues'] = $this->deserializeProperties($commandArguments['propertyValues'])->values; + } } - if (is_string($commandArguments['tetheredDescendantNodeAggregateIds'] ?? null)) { - if ($commandArguments['tetheredDescendantNodeAggregateIds'] === '') { - unset($commandArguments['tetheredDescendantNodeAggregateIds']); - } else { - $commandArguments['tetheredDescendantNodeAggregateIds'] = json_decode($commandArguments['tetheredDescendantNodeAggregateIds'], true, 512, JSON_THROW_ON_ERROR); + if ($commandClassName === CreateNodeAggregateWithNode::class || $commandClassName === SetNodeProperties::class) { + if (is_string($commandArguments['originDimensionSpacePoint'] ?? null) && !empty($commandArguments['originDimensionSpacePoint'])) { + $commandArguments['originDimensionSpacePoint'] = OriginDimensionSpacePoint::fromJsonString($commandArguments['originDimensionSpacePoint'])->coordinates; + } elseif (!isset($commandArguments['originDimensionSpacePoint'])) { + $commandArguments['originDimensionSpacePoint'] = $this->currentDimensionSpacePoint?->coordinates; } } - if (is_array($commandArguments['references'] ?? null)) { - $commandArguments['references'] = iterator_to_array($this->mapRawNodeReferencesToNodeReferencesToWrite($commandArguments['references'])); + if ($commandClassName === CreateNodeAggregateWithNode::class || $commandClassName === SetNodeReferences::class) { + if (is_array($commandArguments['references'] ?? null)) { + $commandArguments['references'] = iterator_to_array($this->mapRawNodeReferencesToNodeReferencesToWrite($commandArguments['references'])); + } } - - $command = $commandClassName::fromArray($commandArguments); - if ($command instanceof CreateRootNodeAggregateWithNode) { - $this->currentRootNodeAggregateId = $command->nodeAggregateId; + if ($commandClassName === SetNodeReferences::class) { + if (is_string($commandArguments['sourceOriginDimensionSpacePoint'] ?? null) && !empty($commandArguments['sourceOriginDimensionSpacePoint'])) { + $commandArguments['sourceOriginDimensionSpacePoint'] = OriginDimensionSpacePoint::fromJsonString($commandArguments['sourceOriginDimensionSpacePoint'])->coordinates; + } elseif (!isset($commandArguments['sourceOriginDimensionSpacePoint'])) { + $commandArguments['sourceOriginDimensionSpacePoint'] = $this->currentDimensionSpacePoint?->coordinates; + } + if (is_string($commandArguments['sourceNodeAggregateId'] ?? null) && str_starts_with($commandArguments['sourceNodeAggregateId'], '$')) { + $commandArguments['sourceNodeAggregateId'] = $this->rememberedNodeAggregateIds[substr($commandArguments['sourceNodeAggregateId'], 1)]?->value; + } elseif (!isset($commandArguments['sourceNodeAggregateId'])) { + $commandArguments['sourceNodeAggregateId'] = $this->currentNodeAggregate?->nodeAggregateId->value; + } } - $this->currentContentRepository->handle($command); + if ($commandClassName === CreateNodeAggregateWithNode::class || $commandClassName === ChangeNodeAggregateType::class || $commandClassName === CreateRootNodeAggregateWithNode::class) { + if (is_string($commandArguments['tetheredDescendantNodeAggregateIds'] ?? null)) { + if ($commandArguments['tetheredDescendantNodeAggregateIds'] === '') { + unset($commandArguments['tetheredDescendantNodeAggregateIds']); + } else { + $commandArguments['tetheredDescendantNodeAggregateIds'] = json_decode($commandArguments['tetheredDescendantNodeAggregateIds'], true, 512, JSON_THROW_ON_ERROR); + } + } + } + return $commandArguments; } protected function mapRawNodeReferencesToNodeReferencesToWrite(array $deserializedTableContent): NodeReferencesToWrite From 255b38e994eb8c9bbec00db3a4bd612f78ee9287 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Tue, 12 Nov 2024 12:35:50 +0100 Subject: [PATCH 160/214] TASK: Adjust changed `WorkspaceRebaseFailed` --- .../W8-IndividualNodePublication/03-MoreBasicFeatures.feature | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/03-MoreBasicFeatures.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/03-MoreBasicFeatures.feature index b65bee35200..68a71a14266 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/03-MoreBasicFeatures.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/03-MoreBasicFeatures.feature @@ -201,8 +201,8 @@ Feature: Publishing individual nodes (basics) | nodesToPublish | [{"dimensionSpacePoint": {}, "nodeAggregateId": "sir-unchanged"}] | | contentStreamIdForRemainingPart | "user-cs-identifier-remaining" | Then the last command should have thrown the WorkspaceRebaseFailed exception with: - | SequenceNumber | Command | Exception | - | 14 | TagSubtree | SubtreeIsAlreadyTagged | + | SequenceNumber | Event | Exception | + | 14 | SubtreeWasTagged | SubtreeIsAlreadyTagged | Scenario: It is possible to publish all nodes When the command PublishIndividualNodesFromWorkspace is executed with payload: From dbbb7f19c23987437c11c672caf3ae53fe062e8a Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Tue, 12 Nov 2024 12:36:14 +0100 Subject: [PATCH 161/214] TASK: Remove obsolete array declaration due to interface --- .../Command/AddDimensionShineThrough.php | 3 --- .../CreateNodeAggregateWithNodeAndSerializedProperties.php | 3 --- .../NodeModification/Command/SetSerializedNodeProperties.php | 3 --- .../NodeReferencing/Command/SetSerializedNodeReferences.php | 3 --- .../Feature/NodeRenaming/Command/ChangeNodeAggregateName.php | 3 --- 5 files changed, 15 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/Feature/DimensionSpaceAdjustment/Command/AddDimensionShineThrough.php b/Neos.ContentRepository.Core/Classes/Feature/DimensionSpaceAdjustment/Command/AddDimensionShineThrough.php index 936505d278d..f3e3f55bdad 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/DimensionSpaceAdjustment/Command/AddDimensionShineThrough.php +++ b/Neos.ContentRepository.Core/Classes/Feature/DimensionSpaceAdjustment/Command/AddDimensionShineThrough.php @@ -63,9 +63,6 @@ public static function create( return new self($workspaceName, $source, $target); } - /** - * @param array $array - */ public static function fromArray(array $array): self { return new self( diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/Command/CreateNodeAggregateWithNodeAndSerializedProperties.php b/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/Command/CreateNodeAggregateWithNodeAndSerializedProperties.php index 3aa8d471d0e..2eb1324ae2e 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/Command/CreateNodeAggregateWithNodeAndSerializedProperties.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/Command/CreateNodeAggregateWithNodeAndSerializedProperties.php @@ -78,9 +78,6 @@ public static function create(WorkspaceName $workspaceName, NodeAggregateId $nod return new self($workspaceName, $nodeAggregateId, $nodeTypeName, $originDimensionSpacePoint, $parentNodeAggregateId, $initialPropertyValues ?? SerializedPropertyValues::createEmpty(), $succeedingSiblingNodeAggregateId, null, NodeAggregateIdsByNodePaths::createEmpty(), $references ?: SerializedNodeReferences::createEmpty()); } - /** - * @param array $array - */ public static function fromArray(array $array): self { return new self( diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Command/SetSerializedNodeProperties.php b/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Command/SetSerializedNodeProperties.php index b3d38bf88dc..c3906bae03f 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Command/SetSerializedNodeProperties.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Command/SetSerializedNodeProperties.php @@ -74,9 +74,6 @@ public static function create( ); } - /** - * @param array $array - */ public static function fromArray(array $array): self { return new self( diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Command/SetSerializedNodeReferences.php b/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Command/SetSerializedNodeReferences.php index 5e95099f806..132f1299ed7 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Command/SetSerializedNodeReferences.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Command/SetSerializedNodeReferences.php @@ -59,9 +59,6 @@ public static function create(WorkspaceName $workspaceName, NodeAggregateId $sou return new self($workspaceName, $sourceNodeAggregateId, $sourceOriginDimensionSpacePoint, $references); } - /** - * @param array $array - */ public static function fromArray(array $array): self { return new self( diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeRenaming/Command/ChangeNodeAggregateName.php b/Neos.ContentRepository.Core/Classes/Feature/NodeRenaming/Command/ChangeNodeAggregateName.php index 38d1f195ed6..d19d6210a48 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeRenaming/Command/ChangeNodeAggregateName.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeRenaming/Command/ChangeNodeAggregateName.php @@ -59,9 +59,6 @@ public static function create(WorkspaceName $workspaceName, NodeAggregateId $nod return new self($workspaceName, $nodeAggregateId, $newNodeName); } - /** - * @param array $array - */ public static function fromArray(array $array): self { return new self( From 9f9f946e3ede5ee37d6e4645779a310ab7ff6fd9 Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Tue, 12 Nov 2024 12:39:59 +0100 Subject: [PATCH 162/214] Fix test --- .../W8-IndividualNodePublication/03-MoreBasicFeatures.feature | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/03-MoreBasicFeatures.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/03-MoreBasicFeatures.feature index b65bee35200..68a71a14266 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/03-MoreBasicFeatures.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/03-MoreBasicFeatures.feature @@ -201,8 +201,8 @@ Feature: Publishing individual nodes (basics) | nodesToPublish | [{"dimensionSpacePoint": {}, "nodeAggregateId": "sir-unchanged"}] | | contentStreamIdForRemainingPart | "user-cs-identifier-remaining" | Then the last command should have thrown the WorkspaceRebaseFailed exception with: - | SequenceNumber | Command | Exception | - | 14 | TagSubtree | SubtreeIsAlreadyTagged | + | SequenceNumber | Event | Exception | + | 14 | SubtreeWasTagged | SubtreeIsAlreadyTagged | Scenario: It is possible to publish all nodes When the command PublishIndividualNodesFromWorkspace is executed with payload: From f883e65f8431bafcf2dc0000d5588a381ab22581 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Tue, 12 Nov 2024 13:29:21 +0100 Subject: [PATCH 163/214] TASK: Fix tests after bastis command test overhaul --- .../Bootstrap/GenericCommandExecutionAndEventPublication.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php index 7e150e296e1..67c91d51875 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php @@ -171,6 +171,9 @@ protected function addDefaultCommandArgumentValues(string $commandClassName, arr if (is_string($commandArguments['parentNodeAggregateId'] ?? null) && str_starts_with($commandArguments['parentNodeAggregateId'], '$')) { $commandArguments['parentNodeAggregateId'] = $this->rememberedNodeAggregateIds[substr($commandArguments['parentNodeAggregateId'], 1)]?->value; } + if (empty($commandArguments['nodeName'])) { + unset($commandArguments['nodeName']); + } } if ($commandClassName === SetNodeProperties::class) { if (is_string($commandArguments['propertyValues'] ?? null)) { From dfd457360a13aee60ae9c22d9a19c5c4b5683dcb Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Tue, 12 Nov 2024 19:13:18 +0100 Subject: [PATCH 164/214] TASK: Recorrect naming of method again (my code editor seems to misbehave :)) --- Neos.ContentRepository.Core/Classes/ContentRepository.php | 6 +++--- .../Classes/EventStore/EventPersister.php | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/ContentRepository.php b/Neos.ContentRepository.Core/Classes/ContentRepository.php index 443a4b3f514..efc240075ee 100644 --- a/Neos.ContentRepository.Core/Classes/ContentRepository.php +++ b/Neos.ContentRepository.Core/Classes/ContentRepository.php @@ -104,7 +104,7 @@ public function handle(CommandInterface $command): void // simple case if ($toPublish instanceof EventsToPublish) { $eventsToPublish = $this->enrichEventsToPublishWithMetadata($toPublish); - $this->eventPersister->publishWithoutKetchup($eventsToPublish); + $this->eventPersister->publishWithoutCatchup($eventsToPublish); $this->catchupProjections(); return; } @@ -114,7 +114,7 @@ public function handle(CommandInterface $command): void foreach ($toPublish as $yieldedEventsToPublish) { $eventsToPublish = $this->enrichEventsToPublishWithMetadata($yieldedEventsToPublish); try { - $this->eventPersister->publishWithoutKetchup($eventsToPublish); + $this->eventPersister->publishWithoutCatchup($eventsToPublish); } catch (ConcurrencyException $concurrencyException) { // we pass the exception into the generator (->throw), so it could be try-caught and reacted upon: // @@ -126,7 +126,7 @@ public function handle(CommandInterface $command): void // } $yieldedErrorStrategy = $toPublish->throw($concurrencyException); if ($yieldedErrorStrategy instanceof EventsToPublish) { - $this->eventPersister->publishWithoutKetchup($yieldedErrorStrategy); + $this->eventPersister->publishWithoutCatchup($yieldedErrorStrategy); } throw $concurrencyException; } diff --git a/Neos.ContentRepository.Core/Classes/EventStore/EventPersister.php b/Neos.ContentRepository.Core/Classes/EventStore/EventPersister.php index 3d3a9b018a0..1af59ff3ce9 100644 --- a/Neos.ContentRepository.Core/Classes/EventStore/EventPersister.php +++ b/Neos.ContentRepository.Core/Classes/EventStore/EventPersister.php @@ -30,7 +30,7 @@ public function __construct( */ public function publishEvents(ContentRepository $contentRepository, EventsToPublish $eventsToPublish): void { - $this->publishWithoutKetchup($eventsToPublish); + $this->publishWithoutCatchup($eventsToPublish); $contentRepository->catchUpProjections(); } @@ -38,7 +38,7 @@ public function publishEvents(ContentRepository $contentRepository, EventsToPubl * TODO Will be refactored via https://github.com/neos/neos-development-collection/pull/5321 * @throws ConcurrencyException in case the expectedVersion does not match */ - public function publishWithoutKetchup(EventsToPublish $eventsToPublish): CommitResult + public function publishWithoutCatchup(EventsToPublish $eventsToPublish): CommitResult { $normalizedEvents = Events::fromArray( $eventsToPublish->events->map($this->eventNormalizer->normalize(...)) From eb99d2fb907a2f08f91ad0940d4a5458dbb78379 Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Wed, 13 Nov 2024 09:53:09 +0100 Subject: [PATCH 165/214] BUGFIX: Behat: Reset `lastCommandException` once asserted Without this fix it was not possible to expect multiple exceptions in a single step or, worse, it led to false positives: ```gherkin Scenario: When doing something Then the last command should have thrown an exception of type "Foo" When doing something else Then the last command should have thrown an exception of type "Bar" ``` The 2nd time would succeed even if the previous interaction did not lead to an exception --- .../Bootstrap/GenericCommandExecutionAndEventPublication.php | 1 + 1 file changed, 1 insertion(+) diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php index 67c91d51875..4c97d14bffe 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php @@ -321,6 +321,7 @@ public function theLastCommandShouldHaveThrown(string $shortExceptionName, ?int $this->lastCommandException->getMessage() )); } + $this->lastCommandException = null; } /** From eed50fbfbfe598d22bac4aa9a099f66ce36843a5 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Wed, 13 Nov 2024 10:01:39 +0100 Subject: [PATCH 166/214] WIP: #5352 automated migration to remove invalid events while replaying --- .../NeosBetaMigrationCommandController.php | 152 ++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 Neos.ContentRepositoryRegistry/Classes/Command/NeosBetaMigrationCommandController.php diff --git a/Neos.ContentRepositoryRegistry/Classes/Command/NeosBetaMigrationCommandController.php b/Neos.ContentRepositoryRegistry/Classes/Command/NeosBetaMigrationCommandController.php new file mode 100644 index 00000000000..aa2ff5c65f1 --- /dev/null +++ b/Neos.ContentRepositoryRegistry/Classes/Command/NeosBetaMigrationCommandController.php @@ -0,0 +1,152 @@ +contentRepositoryRegistry->get($contentRepositoryId); + + $this->backup($contentRepositoryId); + + + $progressBar = new ProgressBar($this->output->getOutput()); + + $progressBar->start($this->highestSequenceNumber($contentRepositoryId)->value, 1); + $options = CatchUpOptions::create(progressCallback: fn () => $progressBar->advance()); + + if ($resetProjection) { + $contentRepository->resetProjectionState(DoctrineDbalContentGraphProjection::class); + } + + $automaticRemovedSequenceNumbers = []; + $manualRemovedSequenceNumbers = []; + + do { + try { + $contentRepository->catchUpProjection(DoctrineDbalContentGraphProjection::class, $options); + } catch (\Throwable $e) { + $this->outputLine(); + + if (preg_match('/^Exception while catching up to sequence number (\d+)/', $e->getMessage(), $matches) !== 1) { + $this->outputLine('Could not replay because of unexpected error'); + $this->outputLine('Removed %d other events: %s', [count($automaticRemovedSequenceNumbers), join(', ', $automaticRemovedSequenceNumbers)]); + throw $e; + } + $failedSequenceNumber = SequenceNumber::fromInteger((int)$matches[1]); + + $eventRow = $this->getEventEnvelopeData($failedSequenceNumber, $contentRepositoryId); + + if ($eventRow['metadata'] !== null) { + $this->outputLine('Did not delete event %s because it doesnt seem to be auto generated', [$failedSequenceNumber->value]); + $this->outputLine('The exception: %s', [$e->getMessage()]); + $this->outputLine(json_encode($eventRow, JSON_PRETTY_PRINT)); + $this->outputLine(); + if ($this->output->askConfirmation(sprintf('> still delete it %d? (y/n) ', $failedSequenceNumber->value), false)) { + $manualRemovedSequenceNumbers[] = $failedSequenceNumber->value; + $this->deleteEvent($failedSequenceNumber, $contentRepositoryId); + continue; + } + $this->outputLine('Removed %d other events: %s', [count($automaticRemovedSequenceNumbers), join(', ', $automaticRemovedSequenceNumbers)]); + throw $e; + } + + $this->outputLine('Deleted event %s because it seems to be invalid and auto generated', [$failedSequenceNumber->value]); + $this->outputLine(json_encode($eventRow)); + + $automaticRemovedSequenceNumbers[] = $failedSequenceNumber->value; + $this->deleteEvent($failedSequenceNumber, $contentRepositoryId); + + $this->outputLine(); + continue; + } + + $progressBar->finish(); + + $this->outputLine(); + $this->outputLine('Replay was successfully.'); + $this->outputLine('Removed %d automatic events: %s', [count($automaticRemovedSequenceNumbers), join(', ', $automaticRemovedSequenceNumbers)]); + if ($manualRemovedSequenceNumbers) { + $this->outputLine('Also removed %d events manually: %s', [count($manualRemovedSequenceNumbers), join(', ', $manualRemovedSequenceNumbers)]); + } + + return; + + } while (true); + } + + public function highestSequenceNumber(ContentRepositoryId $contentRepositoryId): SequenceNumber + { + $eventTableName = DoctrineEventStoreFactory::databaseTableName($contentRepositoryId); + return SequenceNumber::fromInteger((int)$this->connection->fetchOne( + 'SELECT sequencenumber FROM ' . $eventTableName . ' ORDER BY sequencenumber ASC' + )); + } + + + private function backup(ContentRepositoryId $contentRepositoryId): void + { + $backupEventTableName = DoctrineEventStoreFactory::databaseTableName($contentRepositoryId) + . '_bkp_' . date('Y_m_d_H_i_s'); + $this->copyEventTable($backupEventTableName, $contentRepositoryId); + $this->outputLine(sprintf('Copied events table to %s', $backupEventTableName)); + } + + /** + * @return array + */ + private function getEventEnvelopeData(SequenceNumber $sequenceNumber, ContentRepositoryId $contentRepositoryId): array + { + $eventTableName = DoctrineEventStoreFactory::databaseTableName($contentRepositoryId); + return $this->connection->fetchAssociative( + 'SELECT * FROM ' . $eventTableName . ' WHERE sequencenumber=:sequenceNumber', + [ + 'sequenceNumber' => $sequenceNumber->value, + ] + ); + } + + private function deleteEvent(SequenceNumber $sequenceNumber, ContentRepositoryId $contentRepositoryId): void + { + $eventTableName = DoctrineEventStoreFactory::databaseTableName($contentRepositoryId); + $this->connection->beginTransaction(); + $this->connection->executeStatement( + 'DELETE FROM ' . $eventTableName . ' WHERE sequencenumber=:sequenceNumber', + [ + 'sequenceNumber' => $sequenceNumber->value + ] + ); + $this->connection->commit(); + } + + private function copyEventTable(string $backupEventTableName, ContentRepositoryId $contentRepositoryId): void + { + $eventTableName = DoctrineEventStoreFactory::databaseTableName($contentRepositoryId); + $this->connection->executeStatement( + 'CREATE TABLE ' . $backupEventTableName . ' AS + SELECT * + FROM ' . $eventTableName + ); + } +} From 72a02e03c828aa01b88e0c1e1e6166db9c62d2f1 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Wed, 13 Nov 2024 10:09:25 +0100 Subject: [PATCH 167/214] TASK: Enforce command exceptions are asserted --- ...GenericCommandExecutionAndEventPublication.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php index 4c97d14bffe..af0ba476dee 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php @@ -344,6 +344,21 @@ public function theLastCommandShouldHaveThrownTheWorkspaceRebaseFailedWith(Table } Assert::assertSame($payloadTable->getHash(), $actualComparableHash); + $this->lastCommandException = null; + } + + /** + * @AfterScenario + */ + public function ensureNoUnhandledCommandExceptions(): void + { + if ($this->lastCommandException !== null) { + Assert::fail(sprintf( + 'Last command did throw with exception which was not asserted: %s: %s', + $this->lastCommandException::class, + $this->lastCommandException->getMessage() + )); + } } /** From 7b73bf796f9366f3b813b17f1d24b2a3c87dee79 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Wed, 13 Nov 2024 10:24:17 +0100 Subject: [PATCH 168/214] TASK: Improve lastCommandException assertions --- .../01-DisableNodeAggregate_ConstraintChecks.feature | 1 + .../GenericCommandExecutionAndEventPublication.php | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/01-DisableNodeAggregate_ConstraintChecks.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/01-DisableNodeAggregate_ConstraintChecks.feature index 5a69b19b85a..db7221eb28d 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/01-DisableNodeAggregate_ConstraintChecks.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/01-DisableNodeAggregate_ConstraintChecks.feature @@ -53,6 +53,7 @@ Feature: Constraint checks on node aggregate disabling | nodeAggregateId | "i-do-not-exist" | | nodeVariantSelectionStrategy | "allVariants" | | tag | "disabled" | + Then the last command should have thrown an exception of type "NodeAggregateCurrentlyDoesNotExist" Scenario: Try to disable an already disabled node aggregate Given the command DisableNodeAggregate is executed with payload: diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php index af0ba476dee..8ca963361d9 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php @@ -350,13 +350,15 @@ public function theLastCommandShouldHaveThrownTheWorkspaceRebaseFailedWith(Table /** * @AfterScenario */ - public function ensureNoUnhandledCommandExceptions(): void + public function ensureNoUnhandledCommandExceptions(\Behat\Behat\Hook\Scope\AfterScenarioScope $event): void { if ($this->lastCommandException !== null) { Assert::fail(sprintf( - 'Last command did throw with exception which was not asserted: %s: %s', + 'Last command did throw with exception which was not asserted: %s: "%s" in %s:%s', $this->lastCommandException::class, - $this->lastCommandException->getMessage() + $this->lastCommandException->getMessage(), + $event->getFeature()->getFile(), + $event->getScenario()->getLine(), )); } } From 93c1b8bd5b70193391423fa64344f091278100db Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Wed, 13 Nov 2024 13:07:40 +0100 Subject: [PATCH 169/214] Add more tests --- .../Security/EditNodePrivilege.feature | 84 ++++++---- .../Security/ReadNodePrivilege.feature | 53 +++++-- .../Security/WorkspaceAccess.feature | 108 ------------- .../Security/WorkspacePermissions.feature | 147 ++++++++++++++++++ 4 files changed, 241 insertions(+), 151 deletions(-) delete mode 100644 Neos.Neos/Tests/Behavior/Features/ContentRepository/Security/WorkspaceAccess.feature create mode 100644 Neos.Neos/Tests/Behavior/Features/ContentRepository/Security/WorkspacePermissions.feature diff --git a/Neos.Neos/Tests/Behavior/Features/ContentRepository/Security/EditNodePrivilege.feature b/Neos.Neos/Tests/Behavior/Features/ContentRepository/Security/EditNodePrivilege.feature index d90d4448763..d279d035287 100644 --- a/Neos.Neos/Tests/Behavior/Features/ContentRepository/Security/EditNodePrivilege.feature +++ b/Neos.Neos/Tests/Behavior/Features/ContentRepository/Security/EditNodePrivilege.feature @@ -6,15 +6,26 @@ Feature: EditNodePrivilege related features """ privilegeTargets: 'Neos\Neos\Security\Authorization\Privilege\EditNodePrivilege': - 'Neos.Neos:EditBlog': - matcher: 'blog' + 'Neos.Neos:EditSubtreeA': + matcher: 'subtree_a' + roles: + 'Neos.Neos:RoleWithPrivilegeToEditSubtree': + privileges: + - + privilegeTarget: 'Neos.Neos:EditSubtreeA' + permission: GRANT """ And using the following content dimensions: | Identifier | Values | Generalizations | | language | mul, de, en, gsw, ltz | ltz->de->mul, gsw->de->mul, en->mul | And using the following node types: """yaml - 'Neos.Neos:Document': {} + 'Neos.Neos:Document': + properties: + foo: + type: string + references: + ref: [] """ And using identifier "default", I define a content repository And I am in content repository "default" @@ -41,34 +52,55 @@ Feature: EditNodePrivilege related features | b | Neos.Neos:Document | root | b | {"language":"de"} | | b1 | Neos.Neos:Document | b | b1 | {"language":"de"} | And the following Neos users exist: - | Username | First name | Last name | Roles | - | jane.doe | Jane | Doe | Neos.Neos:Administrator | - | john.doe | John | Doe | Neos.Neos:RestrictedEditor,Neos.Neos:UserManager | - | editor | Edward | Editor | Neos.Neos:Editor | - - Scenario: TODO - Given I am in workspace "live" + | Username | First name | Last name | Roles | + | admin | Armin | Admin | Neos.Neos:Administrator | + | restricted_editor | Rich | Restricted | Neos.Neos:RestrictedEditor | + | editor | Edward | Editor | Neos.Neos:Editor | + | editor_with_privilege | Pete | Privileged | Neos.Neos:Editor,Neos.Neos:RoleWithPrivilegeToEditSubtree | + And I am in workspace "live" And I am in dimension space point {"language":"de"} And the command TagSubtree is executed with payload: | Key | Value | | nodeAggregateId | "a" | | nodeVariantSelectionStrategy | "allSpecializations" | - | tag | "blog" | - And the role MANAGER is assigned to workspace "live" for user "jane.doe" - When content repository security is enabled - And I am authenticated as "jane.doe" - When the command DisableNodeAggregate is executed with payload and exceptions are caught: + | tag | "subtree_a" | + And the command DisableNodeAggregate is executed with payload: | Key | Value | - | nodeAggregateId | "a1a" | + | nodeAggregateId | "a1a1a" | | nodeVariantSelectionStrategy | "allVariants" | + And the role COLLABORATOR is assigned to workspace "live" for group "Neos.Neos:Editor" + When a personal workspace for user "editor" is created + And content repository security is enabled + + Scenario Outline: Handling all relevant EditNodePrivilege related commands with different users + Given I am authenticated as "editor" + When the command is executed with payload '' and exceptions are caught + Then the last command should have thrown an exception of type "AccessDenied" with code 1729086686 + + When I am authenticated as "restricted_editor" + When the command is executed with payload '' and exceptions are caught + Then the last command should have thrown an exception of type "AccessDenied" with code 1729086686 + + When I am authenticated as "admin" + When the command is executed with payload '' and exceptions are caught Then the last command should have thrown an exception of type "AccessDenied" with code 1729086686 -# Then the last command should have thrown an exception of type "AccessDenied" with message: -# """ -# Command "Neos\ContentRepository\Core\Feature\NodeDisabling\Command\DisableNodeAggregate" was denied: No edit permissions for node "a1a" in workspace "live": Evaluated following 2 privilege target(s): -# "Neos.Neos:ReadBlog": ABSTAIN -# "Neos.Neos:ReadBlog": GRANT -# (1 granted, 0 denied, 1 abstained) -# Evaluated following 1 privilege target(s): -# "Neos.Neos:EditBlog": ABSTAIN -# (0 granted, 0 denied, 1 abstained) -# """ + + When I am authenticated as "editor_with_privilege" + And the command is executed with payload '' + + When I am in workspace "edward-editor" + And the command is executed with payload '' and exceptions are caught + Then the last command should have thrown an exception of type "AccessDenied" with code 1729086686 + + Examples: + | command | command payload | + | CreateNodeAggregateWithNode | {"nodeAggregateId":"a1b1","parentNodeAggregateId":"a1b","nodeTypeName":"Neos.Neos:Document"} | + | CreateNodeVariant | {"nodeAggregateId":"a1","sourceOrigin":{"language":"de"},"targetOrigin":{"language":"en"}} | + | DisableNodeAggregate | {"nodeAggregateId":"a1","nodeVariantSelectionStrategy":"allVariants"} | + | EnableNodeAggregate | {"nodeAggregateId":"a1a1a","nodeVariantSelectionStrategy":"allVariants"} | + | RemoveNodeAggregate | {"nodeAggregateId":"a1","nodeVariantSelectionStrategy":"allVariants"} | + | TagSubtree | {"nodeAggregateId":"a1","tag":"some_tag","nodeVariantSelectionStrategy":"allVariants"} | + | UntagSubtree | {"nodeAggregateId":"a","tag":"subtree_a","nodeVariantSelectionStrategy":"allVariants"} | + | MoveNodeAggregate | {"nodeAggregateId":"a1","newParentNodeAggregateId":"b"} | + | SetNodeProperties | {"nodeAggregateId":"a1","propertyValues":{"foo":"bar"}} | + | SetNodeReferences | {"sourceNodeAggregateId":"a1","references":[{"referenceName": "ref", "references": [{"target":"b"}]}]} | diff --git a/Neos.Neos/Tests/Behavior/Features/ContentRepository/Security/ReadNodePrivilege.feature b/Neos.Neos/Tests/Behavior/Features/ContentRepository/Security/ReadNodePrivilege.feature index f30fc37739f..2545f9c7bac 100644 --- a/Neos.Neos/Tests/Behavior/Features/ContentRepository/Security/ReadNodePrivilege.feature +++ b/Neos.Neos/Tests/Behavior/Features/ContentRepository/Security/ReadNodePrivilege.feature @@ -6,12 +6,13 @@ Feature: ReadNodePrivilege related features """ privilegeTargets: 'Neos\Neos\Security\Authorization\Privilege\ReadNodePrivilege': - 'Neos.Neos:ReadBlog': - matcher: 'blog' + 'Neos.Neos:ReadSubtreeA': + matcher: 'subtree_a' roles: - 'Neos.Neos:Administrator': + 'Neos.Neos:RoleWithPrivilegeToReadSubtree': privileges: - - privilegeTarget: 'Neos.Neos:ReadBlog' + - + privilegeTarget: 'Neos.Neos:ReadSubtreeA' permission: GRANT """ And using the following content dimensions: @@ -19,11 +20,15 @@ Feature: ReadNodePrivilege related features | language | mul, de, en, gsw, ltz | ltz->de->mul, gsw->de->mul, en->mul | And using the following node types: """yaml - 'Neos.Neos:Document': {} + 'Neos.Neos:Document': + properties: + foo: + type: string + references: + ref: [] """ And using identifier "default", I define a content repository And I am in content repository "default" - And I am user identified by "initiating-user-identifier" And the command CreateRootWorkspace is executed with payload: | Key | Value | | workspaceName | "live" | @@ -47,22 +52,36 @@ Feature: ReadNodePrivilege related features | b | Neos.Neos:Document | root | b | {"language":"de"} | | b1 | Neos.Neos:Document | b | b1 | {"language":"de"} | And the following Neos users exist: - | Id | Username | First name | Last name | Roles | - | janedoe | jane.doe | Jane | Doe | Neos.Neos:Administrator | - | johndoe | john.doe | John | Doe | Neos.Neos:RestrictedEditor,Neos.Neos:UserManager | - | editor | editor | Edward | Editor | Neos.Neos:Editor | - - Scenario: TODO - Given I am in workspace "live" + | Username | First name | Last name | Roles | + | admin | Armin | Admin | Neos.Neos:Administrator | + | restricted_editor | Rich | Restricted | Neos.Neos:RestrictedEditor | + | editor | Edward | Editor | Neos.Neos:Editor | + | editor_with_privilege | Pete | Privileged | Neos.Neos:Editor,Neos.Neos:RoleWithPrivilegeToReadSubtree | + And I am in workspace "live" And I am in dimension space point {"language":"de"} And the command TagSubtree is executed with payload: | Key | Value | | nodeAggregateId | "a" | | nodeVariantSelectionStrategy | "allSpecializations" | - | tag | "blog" | + | tag | "subtree_a" | And the role VIEWER is assigned to workspace "live" for group "Neos.Flow:Everybody" - When content repository security is enabled - And I am authenticated as "john.doe" + When a personal workspace for user "editor" is created + And content repository security is enabled + + Scenario Outline: Read tagged node as user without corresponding ReadNodePrivilege + And I am authenticated as "" Then I should not be able to read node "a1" - When I am authenticated as "jane.doe" + + Examples: + | user | + | admin | + | restricted_editor | + | editor | + + Scenario Outline: Read tagged node as user with corresponding ReadNodePrivilege + And I am authenticated as "" Then I should be able to read node "a1" + + Examples: + | user | + | editor_with_privilege | diff --git a/Neos.Neos/Tests/Behavior/Features/ContentRepository/Security/WorkspaceAccess.feature b/Neos.Neos/Tests/Behavior/Features/ContentRepository/Security/WorkspaceAccess.feature deleted file mode 100644 index 473901a678f..00000000000 --- a/Neos.Neos/Tests/Behavior/Features/ContentRepository/Security/WorkspaceAccess.feature +++ /dev/null @@ -1,108 +0,0 @@ -@flowEntities -Feature: Workspace access related features - - Background: - Given The following additional policies are configured: - """ - privilegeTargets: - 'Neos\Neos\Security\Authorization\Privilege\ReadNodePrivilege': - 'Neos.Neos:ReadBlog': - matcher: 'blog' - roles: - 'Neos.Neos:Administrator': - privileges: - - privilegeTarget: 'Neos.Neos:ReadBlog' - permission: GRANT - """ - And using the following content dimensions: - | Identifier | Values | Generalizations | - | language | mul, de, en, gsw, ltz | ltz->de->mul, gsw->de->mul, en->mul | - And using the following node types: - """yaml - 'Neos.Neos:Document': {} - """ - And using identifier "default", I define a content repository - And I am in content repository "default" - And I am user identified by "initiating-user-identifier" - And the command CreateRootWorkspace is executed with payload: - | Key | Value | - | workspaceName | "live" | - | newContentStreamId | "cs-identifier" | - And I am in workspace "live" and dimension space point {} - And the following Neos users exist: - | Username | Roles | - | admin | Neos.Neos:Administrator | - | editor | Neos.Neos:Editor | - | restricted_editor | Neos.Neos:RestrictedEditor,Neos.Neos:UserManager | - | no_editor | | - - Scenario: TODO - When content repository security is enabled - And I am authenticated as "admin" - And I access the content graph for workspace "live" - Then an exception of type "AccessDenied" should be thrown with message: - """ - Read access denied for workspace "live": Account "admin" is a Neos Administrator without explicit role for workspace "live" - """ - - Scenario: TODO - Given the role MANAGER is assigned to workspace "live" for user "admin" - When content repository security is enabled - And I am authenticated as "admin" - And I access the content graph for workspace "live" - Then no exception should be thrown - - Scenario Outline: Accessing content graph for explicitly assigned workspace role to the authenticated user - Given the role is assigned to workspace "live" for user "" - When content repository security is enabled - And I am authenticated as "" - And I access the content graph for workspace "live" - Then no exception should be thrown - - Examples: - | user | workspace role | - | admin | VIEWER | - | editor | COLLABORATOR | - | editor | VIEWER | - | restricted_editor | MANAGER | - | restricted_editor | VIEWER | - - Scenario Outline: Accessing content graph for workspace role assigned to group of the authenticated user - Given the role is assigned to workspace "live" for group "" - When content repository security is enabled - And I am authenticated as "" - And I access the content graph for workspace "live" - Then no exception should be thrown - - Examples: - | user | group | workspace role | - | admin | Neos.Neos:Editor | COLLABORATOR | - | editor | Neos.Neos:Editor | COLLABORATOR | - | restricted_editor | Neos.Neos:RestrictedEditor | VIEWER | - | no_editor | Neos.Flow:Everybody | VIEWER | - - Scenario Outline: Accessing content graph for workspace role assigned to group the authenticated user is not part of - Given the role is assigned to workspace "live" for group "" - When content repository security is enabled - And I am authenticated as "" - And I access the content graph for workspace "live" - Then an exception of type "AccessDenied" should be thrown - - Examples: - | user | group | workspace role | - | admin | Neos.Flow:Anonymous | COLLABORATOR | - | editor | Neos.Neos:Administrator | MANAGER | - | restricted_editor | Neos.Neos:Editor | VIEWER | - - Scenario Outline: Accessing content graph for workspace that is owned by the authenticated user - Given the personal workspace "user-workspace" is created with the target workspace "live" for user "" - When content repository security is enabled - And I am authenticated as "" - And I access the content graph for workspace "user-workspace" - Then no exception should be thrown - - Examples: - | user | - | admin | - | editor | - | restricted_editor | diff --git a/Neos.Neos/Tests/Behavior/Features/ContentRepository/Security/WorkspacePermissions.feature b/Neos.Neos/Tests/Behavior/Features/ContentRepository/Security/WorkspacePermissions.feature new file mode 100644 index 00000000000..b7ba9bfb5cc --- /dev/null +++ b/Neos.Neos/Tests/Behavior/Features/ContentRepository/Security/WorkspacePermissions.feature @@ -0,0 +1,147 @@ +@flowEntities +Feature: Workspace permission related features + + Background: + When using the following content dimensions: + | Identifier | Values | Generalizations | + | language | mul, de, en, gsw, ltz | ltz->de->mul, gsw->de->mul, en->mul | + And using the following node types: + """yaml + 'Neos.Neos:Document': + properties: + foo: + type: string + references: + ref: [] + 'Neos.Neos:Document2': {} + 'Neos.Neos:CustomRoot': + superTypes: + 'Neos.ContentRepository:Root': true + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + And the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | newContentStreamId | "cs-identifier" | + And I am in workspace "live" and dimension space point {} + And the command CreateRootNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "root" | + | nodeTypeName | "Neos.ContentRepository:Root" | + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeTypeName | parentNodeAggregateId | nodeName | originDimensionSpacePoint | + | a | Neos.Neos:Document | root | a | {"language":"mul"} | + | a1 | Neos.Neos:Document | a | a1 | {"language":"de"} | + | a1a | Neos.Neos:Document | a1 | a1a | {"language":"de"} | + | a1a1 | Neos.Neos:Document | a1a | a1a1 | {"language":"de"} | + | a1a1a | Neos.Neos:Document | a1a1 | a1a1a | {"language":"de"} | + | a1a1b | Neos.Neos:Document | a1a1 | a1a1b | {"language":"de"} | + | a1a2 | Neos.Neos:Document | a1a | a1a2 | {"language":"de"} | + | a1b | Neos.Neos:Document | a1 | a1b | {"language":"de"} | + | a2 | Neos.Neos:Document | a | a2 | {"language":"de"} | + | b | Neos.Neos:Document | root | b | {"language":"de"} | + | b1 | Neos.Neos:Document | b | b1 | {"language":"de"} | + And the following Neos users exist: + | Username | Roles | + | admin | Neos.Neos:Administrator | + | editor | Neos.Neos:Editor | + | restricted_editor | Neos.Neos:RestrictedEditor | + | owner | Neos.Neos:Editor | + | manager | Neos.Neos:Editor | + | collaborator | Neos.Neos:Editor | + | uninvolved | Neos.Neos:Editor | + And I am in workspace "live" + And I am in dimension space point {"language":"de"} + And the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "a" | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "subtree_a" | + And the command DisableNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "a1a1a" | + | nodeVariantSelectionStrategy | "allVariants" | + And the personal workspace "workspace" is created with the target workspace "live" for user "owner" + And I am in workspace "workspace" + And the role MANAGER is assigned to workspace "workspace" for user "manager" + And the role COLLABORATOR is assigned to workspace "workspace" for user "collaborator" + # The following step was added in order to make the `AddDimensionShineThrough` command viable + And I change the content dimensions in content repository "default" to: + | Identifier | Values | Generalizations | + | language | mul, de, ch | ch->de->mul | + And content repository security is enabled + + Scenario Outline: Creating a root workspace + Given I am authenticated as + When the command CreateRootWorkspace is executed with payload '{"workspaceName":"new-ws","newContentStreamId":"new-cs"}' and exceptions are caught + Then the last command should have thrown an exception of type "AccessDenied" with code 1729086686 + + Examples: + | user | + | admin | + | editor | + | restricted_editor | + + Scenario Outline: Deleting a workspace without MANAGE permissions + Given I am authenticated as + When the command DeleteWorkspace is executed with payload '{"workspaceName":"workspace"}' and exceptions are caught + Then the last command should have thrown an exception of type "AccessDenied" with code 1729086686 + + Examples: + | user | + | collaborator | + | uninvolved | + + Scenario Outline: Deleting a workspace with MANAGE permissions + Given I am authenticated as + When the command DeleteWorkspace is executed with payload '{"workspaceName":"workspace"}' + + Examples: + | user | + | admin | + | manager | + | owner | + + Scenario Outline: Handling commands that require WRITE permissions on the workspace + When I am authenticated as "editor" + And the command is executed with payload '' and exceptions are caught + Then the last command should have thrown an exception of type "AccessDenied" with code 1729086686 + + When I am authenticated as "restricted_editor" + And the command is executed with payload '' and exceptions are caught + Then the last command should have thrown an exception of type "AccessDenied" with code 1729086686 + + When I am authenticated as "admin" + And the command is executed with payload '' and exceptions are caught + Then the last command should have thrown an exception of type "AccessDenied" with code 1729086686 + + When I am authenticated as "owner" + And the command is executed with payload '' + + Examples: + | command | command payload | + | CreateNodeAggregateWithNode | {"nodeAggregateId":"a1b1","parentNodeAggregateId":"a1b","nodeTypeName":"Neos.Neos:Document"} | + | CreateNodeVariant | {"nodeAggregateId":"a1","sourceOrigin":{"language":"de"},"targetOrigin":{"language":"mul"}} | + | DisableNodeAggregate | {"nodeAggregateId":"a1","nodeVariantSelectionStrategy":"allVariants"} | + | EnableNodeAggregate | {"nodeAggregateId":"a1a1a","nodeVariantSelectionStrategy":"allVariants"} | + | RemoveNodeAggregate | {"nodeAggregateId":"a1","nodeVariantSelectionStrategy":"allVariants"} | + | TagSubtree | {"nodeAggregateId":"a1","tag":"some_tag","nodeVariantSelectionStrategy":"allVariants"} | + | UntagSubtree | {"nodeAggregateId":"a","tag":"subtree_a","nodeVariantSelectionStrategy":"allVariants"} | + | MoveNodeAggregate | {"nodeAggregateId":"a1","newParentNodeAggregateId":"b"} | + | SetNodeProperties | {"nodeAggregateId":"a1","propertyValues":{"foo":"bar"}} | + | SetNodeReferences | {"sourceNodeAggregateId":"a1","references":[{"referenceName": "ref", "references": [{"target":"b"}]}]} | + + | AddDimensionShineThrough | {"nodeAggregateId":"a1","source":{"language":"de"},"target":{"language":"ch"}} | + | ChangeNodeAggregateName | {"nodeAggregateId":"a1","newNodeName":"changed"} | + | ChangeNodeAggregateType | {"nodeAggregateId":"a1","newNodeTypeName":"Neos.Neos:Document2","strategy":"happypath"} | + | CreateRootNodeAggregateWithNode | {"nodeAggregateId":"c","nodeTypeName":"Neos.Neos:CustomRoot"} | + | MoveDimensionSpacePoint | {"source":{"language":"de"},"target":{"language":"ch"}} | + | UpdateRootNodeAggregateDimensions | {"nodeAggregateId":"root"} | + | DiscardWorkspace | {} | + | DiscardIndividualNodesFromWorkspace | {"nodesToDiscard":[{"nodeAggregateId":"a1"}]} | + | PublishWorkspace | {} | + | PublishIndividualNodesFromWorkspace | {"nodesToPublish":[{"nodeAggregateId":"a1"}]} | + | RebaseWorkspace | {} | + | CreateWorkspace | {"workspaceName":"new-workspace","baseWorkspaceName":"workspace","newContentStreamId":"any"} | + From 810b0a3d430b878db4044ca649a7643cfd0362e3 Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Wed, 13 Nov 2024 13:10:29 +0100 Subject: [PATCH 170/214] Prefix ContentRepositorySecurityTrait fields to avoid naming clashes --- .../ContentRepositorySecurityTrait.php | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/ContentRepositorySecurityTrait.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/ContentRepositorySecurityTrait.php index c8bc89a542e..188186397ae 100644 --- a/Neos.Neos/Tests/Behavior/Features/Bootstrap/ContentRepositorySecurityTrait.php +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/ContentRepositorySecurityTrait.php @@ -54,14 +54,14 @@ trait ContentRepositorySecurityTrait use CRBehavioralTestsSubjectProvider; use ExceptionsTrait; - private bool $flowSecurityEnabled = false; - private bool $contentRepositorySecurityEnabled = false; + private bool $crSecurity_flowSecurityEnabled = false; + private bool $crSecurity_contentRepositorySecurityEnabled = false; - private ?TestingProvider $testingProvider = null; + private ?TestingProvider $crSecurity_testingProvider = null; - private ?ActionRequest $mockActionRequest = null; + private ?ActionRequest $crSecurity_mockActionRequest = null; - private static ?string $testingPolicyPathAndFilename = null; + private static ?string $crSecurity_testingPolicyPathAndFilename = null; /** * @template T of object @@ -74,40 +74,40 @@ abstract private function getObject(string $className): object; public function resetContentRepositorySecurity(): void { TestingAuthProvider::resetAuthProvider(); - $this->contentRepositorySecurityEnabled = false; + $this->crSecurity_contentRepositorySecurityEnabled = false; } #[BeforeFeature] #[AfterFeature] public static function resetPolicies(): void { - if (self::$testingPolicyPathAndFilename !== null && file_exists(self::$testingPolicyPathAndFilename)) { - unlink(self::$testingPolicyPathAndFilename); + if (self::$crSecurity_testingPolicyPathAndFilename !== null && file_exists(self::$crSecurity_testingPolicyPathAndFilename)) { + unlink(self::$crSecurity_testingPolicyPathAndFilename); } } private function enableFlowSecurity(): void { - if ($this->flowSecurityEnabled === true) { + if ($this->crSecurity_flowSecurityEnabled === true) { return; } $this->getObject(PrivilegeManagerInterface::class)->reset(); $tokenAndProviderFactory = $this->getObject(TokenAndProviderFactoryInterface::class); - $this->testingProvider = $tokenAndProviderFactory->getProviders()['TestingProvider']; + $this->crSecurity_testingProvider = $tokenAndProviderFactory->getProviders()['TestingProvider']; $securityContext = $this->getObject(SecurityContext::class); $securityContext->clearContext(); $httpRequest = $this->getObject(ServerRequestFactoryInterface::class)->createServerRequest('GET', 'http://localhost/'); - $this->mockActionRequest = ActionRequest::fromHttpRequest($httpRequest); - $securityContext->setRequest($this->mockActionRequest); - $this->flowSecurityEnabled = true; + $this->crSecurity_mockActionRequest = ActionRequest::fromHttpRequest($httpRequest); + $securityContext->setRequest($this->crSecurity_mockActionRequest); + $this->crSecurity_flowSecurityEnabled = true; } private function enableContentRepositorySecurity(): void { - if ($this->contentRepositorySecurityEnabled === true) { + if ($this->crSecurity_contentRepositorySecurityEnabled === true) { return; } $contentRepositoryAuthProviderFactory = $this->getObject(ContentRepositoryAuthProviderFactory::class); @@ -126,18 +126,18 @@ public function __construct( $contentRepositoryAuthProvider = $contentRepositoryAuthProviderFactory->build($this->currentContentRepository->id, $contentGraphProjection->getState()); TestingAuthProvider::replaceAuthProvider($contentRepositoryAuthProvider); - $this->contentRepositorySecurityEnabled = true; + $this->crSecurity_contentRepositorySecurityEnabled = true; } private function authenticateAccount(Account $account): void { $this->enableFlowSecurity(); - $this->testingProvider->setAuthenticationStatus(TokenInterface::AUTHENTICATION_SUCCESSFUL); - $this->testingProvider->setAccount($account); + $this->crSecurity_testingProvider->setAuthenticationStatus(TokenInterface::AUTHENTICATION_SUCCESSFUL); + $this->crSecurity_testingProvider->setAccount($account); $securityContext = $this->getObject(SecurityContext::class); $securityContext->clearContext(); - $securityContext->setRequest($this->mockActionRequest); + $securityContext->setRequest($this->crSecurity_mockActionRequest); $this->getObject(AuthenticationProviderManager::class)->authenticate(); } @@ -161,8 +161,8 @@ public function theFollowingAdditionalPoliciesAreConfigured(PyStringNode $polici $policyConfiguration = ObjectAccess::getProperty($policyService, 'policyConfiguration', true); $mergedPolicyConfiguration = Arrays::arrayMergeRecursiveOverrule($policyConfiguration, Yaml::parse($policies->getRaw())); - self::$testingPolicyPathAndFilename = $this->getObject(Environment::class)->getPathToTemporaryDirectory() . 'Policy.yaml'; - file_put_contents(self::$testingPolicyPathAndFilename, Yaml::dump($mergedPolicyConfiguration)); + self::$crSecurity_testingPolicyPathAndFilename = $this->getObject(Environment::class)->getPathToTemporaryDirectory() . 'Policy.yaml'; + file_put_contents(self::$crSecurity_testingPolicyPathAndFilename, Yaml::dump($mergedPolicyConfiguration)); ObjectAccess::setProperty($policyService, 'initialized', false, true); $this->getObject(ConfigurationManager::class)->flushConfigurationCache(); From 4e78c384ab4974b2c0169b0f50b5ad1e0d5007e1 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Wed, 13 Nov 2024 16:00:02 +0100 Subject: [PATCH 171/214] BUGFIX: flow neosBetaMigration:reorderNodeAggregateWasRemoved reparation for upgrading to beta 15 see also https://github.com/neos/neos-development-collection/issues/5364 --- .../NeosBetaMigrationCommandController.php | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/Neos.ContentRepositoryRegistry/Classes/Command/NeosBetaMigrationCommandController.php b/Neos.ContentRepositoryRegistry/Classes/Command/NeosBetaMigrationCommandController.php index aa2ff5c65f1..8f80537b78a 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Command/NeosBetaMigrationCommandController.php +++ b/Neos.ContentRepositoryRegistry/Classes/Command/NeosBetaMigrationCommandController.php @@ -4,13 +4,25 @@ namespace Neos\ContentRepositoryRegistry\Command; +use Doctrine\DBAL\ArrayParameterType; use Doctrine\DBAL\Connection; use Neos\ContentGraph\DoctrineDbalAdapter\DoctrineDbalContentGraphProjection; +use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceFactoryDependencies; +use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceFactoryInterface; +use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface; +use Neos\ContentRepository\Core\Feature\ContentStreamEventStreamName; use Neos\ContentRepository\Core\Projection\CatchUpOptions; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; +use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\ContentRepositoryRegistry\Factory\EventStore\DoctrineEventStoreFactory; +use Neos\EventStore\Model\Event\EventType; +use Neos\EventStore\Model\Event\EventTypes; use Neos\EventStore\Model\Event\SequenceNumber; +use Neos\EventStore\Model\EventEnvelope; +use Neos\EventStore\Model\Events; +use Neos\EventStore\Model\EventStream\EventStreamFilter; +use Neos\EventStore\Model\EventStream\ExpectedVersion; use Neos\Flow\Annotations as Flow; use Neos\Flow\Cli\CommandController; use Symfony\Component\Console\Helper\ProgressBar; @@ -23,6 +35,44 @@ class NeosBetaMigrationCommandController extends CommandController #[Flow\Inject()] protected Connection $connection; + public function reorderNodeAggregateWasRemovedCommand(string $contentRepository = 'default', string $workspaceName = 'live'): void + { + $contentRepositoryId = ContentRepositoryId::fromString($contentRepository); + $this->backup($contentRepositoryId); + + $workspace = $this->contentRepositoryRegistry->get($contentRepositoryId)->findWorkspaceByName(WorkspaceName::fromString($workspaceName)); + if (!$workspace) { + $this->outputLine('Workspace not found'); + $this->quit(1); + } + + $streamName = ContentStreamEventStreamName::fromContentStreamId($workspace->currentContentStreamId)->getEventStreamName(); + + $internals = $this->getInternals($contentRepositoryId); + + // get all NodeAggregateWasRemoved from the content stream + $eventsToReorder = iterator_to_array($internals->eventStore->load($streamName, EventStreamFilter::create(EventTypes::create(EventType::fromString('NodeAggregateWasRemoved')))), false); + + // remove all the NodeAggregateWasRemoved events at their sequenceNumbers + $eventTableName = DoctrineEventStoreFactory::databaseTableName($contentRepositoryId); + $this->connection->beginTransaction(); + $this->connection->executeStatement( + 'DELETE FROM ' . $eventTableName . ' WHERE sequencenumber IN (:sequenceNumbers)', + [ + 'sequenceNumbers' => array_map(fn (EventEnvelope $eventEnvelope) => $eventEnvelope->sequenceNumber->value, $eventsToReorder) + ], + [ + 'sequenceNumbers' => ArrayParameterType::STRING + ] + ); + $this->connection->commit(); + + // reapply the NodeAggregateWasRemoved events at the end + $internals->eventStore->commit($streamName, Events::fromArray(array_map(fn (EventEnvelope $eventEnvelope) => $eventEnvelope->event, $eventsToReorder)), ExpectedVersion::ANY()); + + $this->outputLine('Reordered %d removals. Please replay and rebase your other workspaces.', [count($eventsToReorder)]); + } + public function fixReplayCommand(string $contentRepository = 'default', bool $resetProjection = true): void { $contentRepositoryId = ContentRepositoryId::fromString($contentRepository); @@ -149,4 +199,21 @@ private function copyEventTable(string $backupEventTableName, ContentRepositoryI FROM ' . $eventTableName ); } + + private function getInternals(ContentRepositoryId $contentRepositoryId): ContentRepositoryServiceFactoryDependencies + { + // NOT API!!! + $accessor = new class implements ContentRepositoryServiceFactoryInterface { + public ContentRepositoryServiceFactoryDependencies|null $dependencies; + public function build(ContentRepositoryServiceFactoryDependencies $serviceFactoryDependencies): ContentRepositoryServiceInterface + { + $this->dependencies = $serviceFactoryDependencies; + return new class implements ContentRepositoryServiceInterface + { + }; + } + }; + $this->contentRepositoryRegistry->buildService($contentRepositoryId, $accessor); + return $accessor->dependencies; + } } From e924cf74dfaf1850d69bef62578763c55193169f Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Thu, 14 Nov 2024 11:23:23 +0100 Subject: [PATCH 172/214] TASK: Add metadata to migrated events via `reorderNodeAggregateWasRemoved` --- .../NeosBetaMigrationCommandController.php | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/Neos.ContentRepositoryRegistry/Classes/Command/NeosBetaMigrationCommandController.php b/Neos.ContentRepositoryRegistry/Classes/Command/NeosBetaMigrationCommandController.php index 8f80537b78a..b565b6734db 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Command/NeosBetaMigrationCommandController.php +++ b/Neos.ContentRepositoryRegistry/Classes/Command/NeosBetaMigrationCommandController.php @@ -16,6 +16,8 @@ use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\ContentRepositoryRegistry\Factory\EventStore\DoctrineEventStoreFactory; +use Neos\EventStore\Model\Event; +use Neos\EventStore\Model\Event\EventMetadata; use Neos\EventStore\Model\Event\EventType; use Neos\EventStore\Model\Event\EventTypes; use Neos\EventStore\Model\Event\SequenceNumber; @@ -67,8 +69,25 @@ public function reorderNodeAggregateWasRemovedCommand(string $contentRepository ); $this->connection->commit(); - // reapply the NodeAggregateWasRemoved events at the end - $internals->eventStore->commit($streamName, Events::fromArray(array_map(fn (EventEnvelope $eventEnvelope) => $eventEnvelope->event, $eventsToReorder)), ExpectedVersion::ANY()); + $mapper = function (EventEnvelope $eventEnvelope): Event { + $metadata = $event->eventMetadata?->value ?? []; + $metadata['reorderedByMigration'] = sprintf('Originally recorded at %s with sequence number %d', $eventEnvelope->recordedAt->format(\DateTimeInterface::ATOM), $eventEnvelope->sequenceNumber->value); + return new Event( + $eventEnvelope->event->id, + $eventEnvelope->event->type, + $eventEnvelope->event->data, + EventMetadata::fromArray($metadata), + $eventEnvelope->event->causationId, + $eventEnvelope->event->correlationId + ); + }; + + // reapply the NodeAggregateWasRemoved events + $internals->eventStore->commit( + $streamName, + Events::fromArray(array_map($mapper, $eventsToReorder)), + ExpectedVersion::ANY() + ); $this->outputLine('Reordered %d removals. Please replay and rebase your other workspaces.', [count($eventsToReorder)]); } From d06833943ba40940bd1a7e082d972dc4a5af4a5c Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Thu, 14 Nov 2024 11:25:17 +0100 Subject: [PATCH 173/214] TASK: Remove `fixReplay` command we would have to delete too many events that were legit as well, like a move AFTER a deletion https://github.com/neos/neos-development-collection/issues/5364 --- .../NeosBetaMigrationCommandController.php | 113 ------------------ 1 file changed, 113 deletions(-) diff --git a/Neos.ContentRepositoryRegistry/Classes/Command/NeosBetaMigrationCommandController.php b/Neos.ContentRepositoryRegistry/Classes/Command/NeosBetaMigrationCommandController.php index b565b6734db..ac63222d7fe 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Command/NeosBetaMigrationCommandController.php +++ b/Neos.ContentRepositoryRegistry/Classes/Command/NeosBetaMigrationCommandController.php @@ -6,12 +6,10 @@ use Doctrine\DBAL\ArrayParameterType; use Doctrine\DBAL\Connection; -use Neos\ContentGraph\DoctrineDbalAdapter\DoctrineDbalContentGraphProjection; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceFactoryDependencies; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceFactoryInterface; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface; use Neos\ContentRepository\Core\Feature\ContentStreamEventStreamName; -use Neos\ContentRepository\Core\Projection\CatchUpOptions; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; @@ -20,14 +18,12 @@ use Neos\EventStore\Model\Event\EventMetadata; use Neos\EventStore\Model\Event\EventType; use Neos\EventStore\Model\Event\EventTypes; -use Neos\EventStore\Model\Event\SequenceNumber; use Neos\EventStore\Model\EventEnvelope; use Neos\EventStore\Model\Events; use Neos\EventStore\Model\EventStream\EventStreamFilter; use Neos\EventStore\Model\EventStream\ExpectedVersion; use Neos\Flow\Annotations as Flow; use Neos\Flow\Cli\CommandController; -use Symfony\Component\Console\Helper\ProgressBar; class NeosBetaMigrationCommandController extends CommandController { @@ -92,88 +88,6 @@ public function reorderNodeAggregateWasRemovedCommand(string $contentRepository $this->outputLine('Reordered %d removals. Please replay and rebase your other workspaces.', [count($eventsToReorder)]); } - public function fixReplayCommand(string $contentRepository = 'default', bool $resetProjection = true): void - { - $contentRepositoryId = ContentRepositoryId::fromString($contentRepository); - $contentRepository = $this->contentRepositoryRegistry->get($contentRepositoryId); - - $this->backup($contentRepositoryId); - - - $progressBar = new ProgressBar($this->output->getOutput()); - - $progressBar->start($this->highestSequenceNumber($contentRepositoryId)->value, 1); - $options = CatchUpOptions::create(progressCallback: fn () => $progressBar->advance()); - - if ($resetProjection) { - $contentRepository->resetProjectionState(DoctrineDbalContentGraphProjection::class); - } - - $automaticRemovedSequenceNumbers = []; - $manualRemovedSequenceNumbers = []; - - do { - try { - $contentRepository->catchUpProjection(DoctrineDbalContentGraphProjection::class, $options); - } catch (\Throwable $e) { - $this->outputLine(); - - if (preg_match('/^Exception while catching up to sequence number (\d+)/', $e->getMessage(), $matches) !== 1) { - $this->outputLine('Could not replay because of unexpected error'); - $this->outputLine('Removed %d other events: %s', [count($automaticRemovedSequenceNumbers), join(', ', $automaticRemovedSequenceNumbers)]); - throw $e; - } - $failedSequenceNumber = SequenceNumber::fromInteger((int)$matches[1]); - - $eventRow = $this->getEventEnvelopeData($failedSequenceNumber, $contentRepositoryId); - - if ($eventRow['metadata'] !== null) { - $this->outputLine('Did not delete event %s because it doesnt seem to be auto generated', [$failedSequenceNumber->value]); - $this->outputLine('The exception: %s', [$e->getMessage()]); - $this->outputLine(json_encode($eventRow, JSON_PRETTY_PRINT)); - $this->outputLine(); - if ($this->output->askConfirmation(sprintf('> still delete it %d? (y/n) ', $failedSequenceNumber->value), false)) { - $manualRemovedSequenceNumbers[] = $failedSequenceNumber->value; - $this->deleteEvent($failedSequenceNumber, $contentRepositoryId); - continue; - } - $this->outputLine('Removed %d other events: %s', [count($automaticRemovedSequenceNumbers), join(', ', $automaticRemovedSequenceNumbers)]); - throw $e; - } - - $this->outputLine('Deleted event %s because it seems to be invalid and auto generated', [$failedSequenceNumber->value]); - $this->outputLine(json_encode($eventRow)); - - $automaticRemovedSequenceNumbers[] = $failedSequenceNumber->value; - $this->deleteEvent($failedSequenceNumber, $contentRepositoryId); - - $this->outputLine(); - continue; - } - - $progressBar->finish(); - - $this->outputLine(); - $this->outputLine('Replay was successfully.'); - $this->outputLine('Removed %d automatic events: %s', [count($automaticRemovedSequenceNumbers), join(', ', $automaticRemovedSequenceNumbers)]); - if ($manualRemovedSequenceNumbers) { - $this->outputLine('Also removed %d events manually: %s', [count($manualRemovedSequenceNumbers), join(', ', $manualRemovedSequenceNumbers)]); - } - - return; - - } while (true); - } - - public function highestSequenceNumber(ContentRepositoryId $contentRepositoryId): SequenceNumber - { - $eventTableName = DoctrineEventStoreFactory::databaseTableName($contentRepositoryId); - return SequenceNumber::fromInteger((int)$this->connection->fetchOne( - 'SELECT sequencenumber FROM ' . $eventTableName . ' ORDER BY sequencenumber ASC' - )); - } - - private function backup(ContentRepositoryId $contentRepositoryId): void { $backupEventTableName = DoctrineEventStoreFactory::databaseTableName($contentRepositoryId) @@ -182,33 +96,6 @@ private function backup(ContentRepositoryId $contentRepositoryId): void $this->outputLine(sprintf('Copied events table to %s', $backupEventTableName)); } - /** - * @return array - */ - private function getEventEnvelopeData(SequenceNumber $sequenceNumber, ContentRepositoryId $contentRepositoryId): array - { - $eventTableName = DoctrineEventStoreFactory::databaseTableName($contentRepositoryId); - return $this->connection->fetchAssociative( - 'SELECT * FROM ' . $eventTableName . ' WHERE sequencenumber=:sequenceNumber', - [ - 'sequenceNumber' => $sequenceNumber->value, - ] - ); - } - - private function deleteEvent(SequenceNumber $sequenceNumber, ContentRepositoryId $contentRepositoryId): void - { - $eventTableName = DoctrineEventStoreFactory::databaseTableName($contentRepositoryId); - $this->connection->beginTransaction(); - $this->connection->executeStatement( - 'DELETE FROM ' . $eventTableName . ' WHERE sequencenumber=:sequenceNumber', - [ - 'sequenceNumber' => $sequenceNumber->value - ] - ); - $this->connection->commit(); - } - private function copyEventTable(string $backupEventTableName, ContentRepositoryId $contentRepositoryId): void { $eventTableName = DoctrineEventStoreFactory::databaseTableName($contentRepositoryId); From 33c0877d1e33af4d25e949361cf269c3f8518444 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Fri, 15 Nov 2024 07:58:38 +0100 Subject: [PATCH 174/214] TASK: Fix new Neos security tests and fake `PolicyService` correctly - we must not write the settings to the file system via $crSecurity_testingPolicyPathAndFilename as following processes and tests in the same context would be affected. Also flushing and reloading the configuration catch (`flushConfigurationCache`) is expensive and the tests must not mess with that low level flow behaviour - instead we only rely on faking the `PolicyService`'s runtime caches. This is done by injecting a custom configuration into `$policyConfiguration` and then initialising the `PolicyService` --- .../ContentRepositorySecurityTrait.php | 46 ++++++++++++------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/ContentRepositorySecurityTrait.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/ContentRepositorySecurityTrait.php index 188186397ae..7260cd10844 100644 --- a/Neos.Neos/Tests/Behavior/Features/Bootstrap/ContentRepositorySecurityTrait.php +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/ContentRepositorySecurityTrait.php @@ -61,8 +61,6 @@ trait ContentRepositorySecurityTrait private ?ActionRequest $crSecurity_mockActionRequest = null; - private static ?string $crSecurity_testingPolicyPathAndFilename = null; - /** * @template T of object * @param class-string $className @@ -75,15 +73,14 @@ public function resetContentRepositorySecurity(): void { TestingAuthProvider::resetAuthProvider(); $this->crSecurity_contentRepositorySecurityEnabled = false; - } + $this->crSecurity_flowSecurityEnabled = false; + + $policyService = $this->getObject(PolicyService::class); + // reset the $policyConfiguration to the default (fetched from the original ConfigurationManager) + $this->getObject(PolicyService::class)->reset(); // TODO also reset privilegeTargets in ->reset() + ObjectAccess::setProperty($policyService, 'privilegeTargets', [], true); + $policyService->injectConfigurationManager($this->getObject(ConfigurationManager::class)); - #[BeforeFeature] - #[AfterFeature] - public static function resetPolicies(): void - { - if (self::$crSecurity_testingPolicyPathAndFilename !== null && file_exists(self::$crSecurity_testingPolicyPathAndFilename)) { - unlink(self::$crSecurity_testingPolicyPathAndFilename); - } } private function enableFlowSecurity(): void @@ -157,15 +154,30 @@ public function contentRepositorySecurityIsEnabled(): void public function theFollowingAdditionalPoliciesAreConfigured(PyStringNode $policies): void { $policyService = $this->getObject(PolicyService::class); - $policyService->getRoles(); // force initialization - $policyConfiguration = ObjectAccess::getProperty($policyService, 'policyConfiguration', true); - $mergedPolicyConfiguration = Arrays::arrayMergeRecursiveOverrule($policyConfiguration, Yaml::parse($policies->getRaw())); - self::$crSecurity_testingPolicyPathAndFilename = $this->getObject(Environment::class)->getPathToTemporaryDirectory() . 'Policy.yaml'; - file_put_contents(self::$crSecurity_testingPolicyPathAndFilename, Yaml::dump($mergedPolicyConfiguration)); + $mergedPolicyConfiguration = Arrays::arrayMergeRecursiveOverrule( + $this->getObject(ConfigurationManager::class)->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_POLICY), + Yaml::parse($policies->getRaw()) + ); + + // if we de-initialise the PolicyService and set a new $policyConfiguration (by injecting a stub ConfigurationManager which will be used) + // we can change the roles and privileges at runtime :D + $policyService->reset(); // TODO also reset privilegeTargets in ->reset() + ObjectAccess::setProperty($policyService, 'privilegeTargets', [], true); + $policyService->injectConfigurationManager(new class ($mergedPolicyConfiguration) extends ConfigurationManager + { + public function __construct( + private array $mergedPolicyConfiguration + ) { + } - ObjectAccess::setProperty($policyService, 'initialized', false, true); - $this->getObject(ConfigurationManager::class)->flushConfigurationCache(); + public function getConfiguration(string $configurationType, string $configurationPath = null) + { + Assert::assertSame(ConfigurationManager::CONFIGURATION_TYPE_POLICY, $configurationType); + Assert::assertSame(null, $configurationPath); + return $this->mergedPolicyConfiguration; + } + }); } /** From 3e73e68de492e2cdef7b495ac01a2cf53f254a36 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Fri, 15 Nov 2024 08:06:41 +0100 Subject: [PATCH 175/214] TASK: Split `ContentRepositorySecurityTrait` and `FlowSecurityTrait` --- .../ContentRepositorySecurityTrait.php | 95 +------------ .../Features/Bootstrap/FlowSecurityTrait.php | 132 ++++++++++++++++++ 2 files changed, 136 insertions(+), 91 deletions(-) create mode 100644 Neos.Neos/Tests/Behavior/Features/Bootstrap/FlowSecurityTrait.php diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/ContentRepositorySecurityTrait.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/ContentRepositorySecurityTrait.php index 7260cd10844..86fc2bb454e 100644 --- a/Neos.Neos/Tests/Behavior/Features/Bootstrap/ContentRepositorySecurityTrait.php +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/ContentRepositorySecurityTrait.php @@ -12,9 +12,6 @@ * source code. */ -use Behat\Gherkin\Node\PyStringNode; -use Behat\Hook\AfterFeature; -use Behat\Hook\BeforeFeature; use Behat\Hook\BeforeScenario; use Neos\ContentRepository\BehavioralTests\TestSuite\Behavior\CRBehavioralTestsSubjectProvider; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceFactoryDependencies; @@ -24,25 +21,11 @@ use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Helpers\TestingAuthProvider; -use Neos\Flow\Configuration\ConfigurationManager; use Neos\Flow\Mvc\ActionRequest; -use Neos\Flow\Security\Account; -use Neos\Flow\Security\Authentication\AuthenticationProviderManager; use Neos\Flow\Security\Authentication\Provider\TestingProvider; -use Neos\Flow\Security\Authentication\TokenAndProviderFactoryInterface; -use Neos\Flow\Security\Authentication\TokenInterface; -use Neos\Flow\Security\Authorization\PrivilegeManagerInterface; -use Neos\Flow\Security\Context as SecurityContext; -use Neos\Flow\Security\Policy\PolicyService; -use Neos\Flow\Utility\Environment; use Neos\Neos\Domain\Service\UserService; -use Neos\Neos\Security\Authorization\Privilege\ReadNodePrivilege; use Neos\Neos\Security\ContentRepositoryAuthProvider\ContentRepositoryAuthProviderFactory; -use Neos\Utility\Arrays; -use Neos\Utility\ObjectAccess; use PHPUnit\Framework\Assert; -use Psr\Http\Message\ServerRequestFactoryInterface; -use Symfony\Component\Yaml\Yaml; /** * Step implementations and helper for Content Repository Security related tests inside Neos.Neos @@ -53,6 +36,7 @@ trait ContentRepositorySecurityTrait { use CRBehavioralTestsSubjectProvider; use ExceptionsTrait; + use FlowSecurityTrait; private bool $crSecurity_flowSecurityEnabled = false; private bool $crSecurity_contentRepositorySecurityEnabled = false; @@ -68,38 +52,13 @@ trait ContentRepositorySecurityTrait */ abstract private function getObject(string $className): object; - #[BeforeScenario] + /** + * @BeforeScenario + */ public function resetContentRepositorySecurity(): void { TestingAuthProvider::resetAuthProvider(); $this->crSecurity_contentRepositorySecurityEnabled = false; - $this->crSecurity_flowSecurityEnabled = false; - - $policyService = $this->getObject(PolicyService::class); - // reset the $policyConfiguration to the default (fetched from the original ConfigurationManager) - $this->getObject(PolicyService::class)->reset(); // TODO also reset privilegeTargets in ->reset() - ObjectAccess::setProperty($policyService, 'privilegeTargets', [], true); - $policyService->injectConfigurationManager($this->getObject(ConfigurationManager::class)); - - } - - private function enableFlowSecurity(): void - { - if ($this->crSecurity_flowSecurityEnabled === true) { - return; - } - $this->getObject(PrivilegeManagerInterface::class)->reset(); - - $tokenAndProviderFactory = $this->getObject(TokenAndProviderFactoryInterface::class); - - $this->crSecurity_testingProvider = $tokenAndProviderFactory->getProviders()['TestingProvider']; - - $securityContext = $this->getObject(SecurityContext::class); - $securityContext->clearContext(); - $httpRequest = $this->getObject(ServerRequestFactoryInterface::class)->createServerRequest('GET', 'http://localhost/'); - $this->crSecurity_mockActionRequest = ActionRequest::fromHttpRequest($httpRequest); - $securityContext->setRequest($this->crSecurity_mockActionRequest); - $this->crSecurity_flowSecurityEnabled = true; } private function enableContentRepositorySecurity(): void @@ -126,18 +85,6 @@ public function __construct( $this->crSecurity_contentRepositorySecurityEnabled = true; } - private function authenticateAccount(Account $account): void - { - $this->enableFlowSecurity(); - $this->crSecurity_testingProvider->setAuthenticationStatus(TokenInterface::AUTHENTICATION_SUCCESSFUL); - $this->crSecurity_testingProvider->setAccount($account); - - $securityContext = $this->getObject(SecurityContext::class); - $securityContext->clearContext(); - $securityContext->setRequest($this->crSecurity_mockActionRequest); - $this->getObject(AuthenticationProviderManager::class)->authenticate(); - } - /** * @Given content repository security is enabled */ @@ -147,39 +94,6 @@ public function contentRepositorySecurityIsEnabled(): void $this->enableContentRepositorySecurity(); } - - /** - * @Given The following additional policies are configured: - */ - public function theFollowingAdditionalPoliciesAreConfigured(PyStringNode $policies): void - { - $policyService = $this->getObject(PolicyService::class); - - $mergedPolicyConfiguration = Arrays::arrayMergeRecursiveOverrule( - $this->getObject(ConfigurationManager::class)->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_POLICY), - Yaml::parse($policies->getRaw()) - ); - - // if we de-initialise the PolicyService and set a new $policyConfiguration (by injecting a stub ConfigurationManager which will be used) - // we can change the roles and privileges at runtime :D - $policyService->reset(); // TODO also reset privilegeTargets in ->reset() - ObjectAccess::setProperty($policyService, 'privilegeTargets', [], true); - $policyService->injectConfigurationManager(new class ($mergedPolicyConfiguration) extends ConfigurationManager - { - public function __construct( - private array $mergedPolicyConfiguration - ) { - } - - public function getConfiguration(string $configurationType, string $configurationPath = null) - { - Assert::assertSame(ConfigurationManager::CONFIGURATION_TYPE_POLICY, $configurationType); - Assert::assertSame(null, $configurationPath); - return $this->mergedPolicyConfiguration; - } - }); - } - /** * @When I am authenticated as :username */ @@ -189,7 +103,6 @@ public function iAmAuthenticatedAs(string $username): void $this->authenticateAccount($user->getAccounts()->first()); } - /** * @When I access the content graph for workspace :workspaceName */ diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/FlowSecurityTrait.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/FlowSecurityTrait.php new file mode 100644 index 00000000000..9e2123e3c70 --- /dev/null +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/FlowSecurityTrait.php @@ -0,0 +1,132 @@ + $className + * @return T + */ + abstract protected function getObject(string $className): object; + + /** + * @BeforeScenario + */ + final public function resetFlowSecurity(): void + { + $this->flowSecurity_securityEnabled = false; + + $policyService = $this->getObject(PolicyService::class); + // reset the $policyConfiguration to the default (fetched from the original ConfigurationManager) + $this->getObject(PolicyService::class)->reset(); // TODO also reset privilegeTargets in ->reset() + ObjectAccess::setProperty($policyService, 'privilegeTargets', [], true); + $policyService->injectConfigurationManager($this->getObject(ConfigurationManager::class)); + + $this->getObject(SecurityContext::class)->clearContext(); + $this->getObject(PrivilegeManagerInterface::class)->reset(); + } + + final protected function enableFlowSecurity(): void + { + if ($this->flowSecurity_securityEnabled === true) { + return; + } + + $tokenAndProviderFactory = $this->getObject(TokenAndProviderFactoryInterface::class); + + $this->flowSecurity_testingProvider = $tokenAndProviderFactory->getProviders()['TestingProvider']; + + $securityContext = $this->getObject(SecurityContext::class); + $httpRequest = $this->getObject(ServerRequestFactoryInterface::class)->createServerRequest('GET', 'http://localhost/'); + $this->flowSecurity_mockActionRequest = ActionRequest::fromHttpRequest($httpRequest); + $securityContext->setRequest($this->flowSecurity_mockActionRequest); + $this->flowSecurity_securityEnabled = true; + } + + final protected function authenticateAccount(Account $account): void + { + $this->enableFlowSecurity(); + $this->flowSecurity_testingProvider->setAuthenticationStatus(TokenInterface::AUTHENTICATION_SUCCESSFUL); + $this->flowSecurity_testingProvider->setAccount($account); + + $securityContext = $this->getObject(SecurityContext::class); + $securityContext->clearContext(); + $securityContext->setRequest($this->flowSecurity_mockActionRequest); + $this->getObject(AuthenticationProviderManager::class)->authenticate(); + } + + /** + * @Given The following additional policies are configured: + */ + final public function theFollowingAdditionalPoliciesAreConfigured(PyStringNode $policies): void + { + $policyService = $this->getObject(PolicyService::class); + + $mergedPolicyConfiguration = Arrays::arrayMergeRecursiveOverrule( + $this->getObject(ConfigurationManager::class)->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_POLICY), + Yaml::parse($policies->getRaw()) + ); + + // if we de-initialise the PolicyService and set a new $policyConfiguration (by injecting a stub ConfigurationManager which will be used) + // we can change the roles and privileges at runtime :D + $policyService->reset(); // TODO also reset privilegeTargets in ->reset() + ObjectAccess::setProperty($policyService, 'privilegeTargets', [], true); + $policyService->injectConfigurationManager(new class ($mergedPolicyConfiguration) extends ConfigurationManager + { + public function __construct( + private array $mergedPolicyConfiguration + ) { + } + + public function getConfiguration(string $configurationType, string $configurationPath = null) + { + Assert::assertSame(ConfigurationManager::CONFIGURATION_TYPE_POLICY, $configurationType); + Assert::assertSame(null, $configurationPath); + return $this->mergedPolicyConfiguration; + } + }); + } +} From db43d1f26afbb8d229adcf6c77c22a4791dfba98 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Fri, 15 Nov 2024 08:08:04 +0100 Subject: [PATCH 176/214] TASK: Cleanup imports --- .../ContentRepositoryAuthProvider.php | 5 ----- .../ContentRepositoryAuthProviderFactory.php | 1 - 2 files changed, 6 deletions(-) diff --git a/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php index 0ece429d71f..6bd844d8a5a 100644 --- a/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php +++ b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php @@ -8,15 +8,12 @@ use Neos\ContentRepository\Core\Feature\DimensionSpaceAdjustment\Command\AddDimensionShineThrough; use Neos\ContentRepository\Core\Feature\DimensionSpaceAdjustment\Command\MoveDimensionSpacePoint; use Neos\ContentRepository\Core\Feature\NodeCreation\Command\CreateNodeAggregateWithNode; -use Neos\ContentRepository\Core\Feature\NodeCreation\Command\CreateNodeAggregateWithNodeAndSerializedProperties; use Neos\ContentRepository\Core\Feature\NodeDisabling\Command\DisableNodeAggregate; use Neos\ContentRepository\Core\Feature\NodeDisabling\Command\EnableNodeAggregate; use Neos\ContentRepository\Core\Feature\NodeDuplication\Command\CopyNodesRecursively; use Neos\ContentRepository\Core\Feature\NodeModification\Command\SetNodeProperties; -use Neos\ContentRepository\Core\Feature\NodeModification\Command\SetSerializedNodeProperties; use Neos\ContentRepository\Core\Feature\NodeMove\Command\MoveNodeAggregate; use Neos\ContentRepository\Core\Feature\NodeReferencing\Command\SetNodeReferences; -use Neos\ContentRepository\Core\Feature\NodeReferencing\Command\SetSerializedNodeReferences; use Neos\ContentRepository\Core\Feature\NodeRemoval\Command\RemoveNodeAggregate; use Neos\ContentRepository\Core\Feature\NodeRenaming\Command\ChangeNodeAggregateName; use Neos\ContentRepository\Core\Feature\NodeTypeChange\Command\ChangeNodeAggregateType; @@ -38,13 +35,11 @@ use Neos\ContentRepository\Core\Feature\WorkspacePublication\Command\PublishWorkspace; use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Command\RebaseWorkspace; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphReadModelInterface; -use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\Flow\Security\Context as SecurityContext; -use Neos\Neos\Domain\Model\NodePermissions; use Neos\Neos\Domain\Model\WorkspacePermissions; use Neos\Neos\Domain\Service\UserService; use Neos\Neos\Security\Authorization\ContentRepositoryAuthorizationService; diff --git a/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProviderFactory.php b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProviderFactory.php index cf3d2fb9ac5..cc2ebb54dab 100644 --- a/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProviderFactory.php +++ b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProviderFactory.php @@ -4,7 +4,6 @@ namespace Neos\Neos\Security\ContentRepositoryAuthProvider; -use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphProjectionInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphReadModelInterface; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepositoryRegistry\Factory\AuthProvider\AuthProviderFactoryInterface; From f1415b8301266d0fa6f0af7424eadea2fe5b3669 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Fri, 15 Nov 2024 08:19:51 +0100 Subject: [PATCH 177/214] TASK: Cleanup imports Now we throw the cr AccessDenied exception instead of flows access denied in all places of the workspace service to make behaviour consistent. $contentRepository->handle might for example throw the `AccessDenied` when creating a workspace --- Neos.Neos/Classes/Domain/Service/WorkspaceService.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Neos.Neos/Classes/Domain/Service/WorkspaceService.php b/Neos.Neos/Classes/Domain/Service/WorkspaceService.php index 105a3126bff..95be477088c 100644 --- a/Neos.Neos/Classes/Domain/Service/WorkspaceService.php +++ b/Neos.Neos/Classes/Domain/Service/WorkspaceService.php @@ -14,6 +14,7 @@ namespace Neos\Neos\Domain\Service; +use Neos\ContentRepository\Core\Feature\Security\Exception\AccessDenied; use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Command\CreateRootWorkspace; use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Command\CreateWorkspace; use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Exception\WorkspaceAlreadyExists; @@ -24,7 +25,6 @@ use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Annotations as Flow; use Neos\Flow\Security\Context as SecurityContext; -use Neos\Flow\Security\Exception\AccessDeniedException; use Neos\Neos\Domain\Model\User; use Neos\Neos\Domain\Model\UserId; use Neos\Neos\Domain\Model\WorkspaceClassification; @@ -280,7 +280,7 @@ private function requireManagementWorkspacePermission(ContentRepositoryId $conte $this->userService->getCurrentUser()?->getId() ); if (!$workspacePermissions->manage) { - throw new AccessDeniedException(sprintf('The current user does not have manage permissions for workspace "%s" in content repository "%s"', $workspaceName->value, $contentRepositoryId->value), 1731343473); + throw new AccessDenied(sprintf('The current user does not have manage permissions for workspace "%s" in content repository "%s"', $workspaceName->value, $contentRepositoryId->value), 1731654519); } } } From efca1247f240b9d78756dca0cd7827ab08e7a9b0 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Fri, 15 Nov 2024 08:20:34 +0100 Subject: [PATCH 178/214] TASK: Remove `withoutAuthorizationChecks` in tests And introduce the behaviour of the `FunctionalTestCase`: https://github.com/neos/flow-development-collection/commit/b9c89e3e08649cbb5366cb769b2f79b0f13bd68e --- .../Features/Bootstrap/FlowSecurityTrait.php | 5 +- .../Bootstrap/WorkspaceServiceTrait.php | 54 ++++++++----------- 2 files changed, 27 insertions(+), 32 deletions(-) diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/FlowSecurityTrait.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/FlowSecurityTrait.php index 9e2123e3c70..5f4915dfcf1 100644 --- a/Neos.Neos/Tests/Behavior/Features/Bootstrap/FlowSecurityTrait.php +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/FlowSecurityTrait.php @@ -65,7 +65,10 @@ final public function resetFlowSecurity(): void ObjectAccess::setProperty($policyService, 'privilegeTargets', [], true); $policyService->injectConfigurationManager($this->getObject(ConfigurationManager::class)); - $this->getObject(SecurityContext::class)->clearContext(); + $securityContext = $this->getObject(SecurityContext::class); + $securityContext->clearContext(); + // todo add setter! Also used in FunctionalTestCase https://github.com/neos/flow-development-collection/commit/b9c89e3e08649cbb5366cb769b2f79b0f13bd68e + ObjectAccess::setProperty($securityContext, 'authorizationChecksDisabled', true, true); $this->getObject(PrivilegeManagerInterface::class)->reset(); } diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/WorkspaceServiceTrait.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/WorkspaceServiceTrait.php index e1a3ece74af..af046a39443 100644 --- a/Neos.Neos/Tests/Behavior/Features/Bootstrap/WorkspaceServiceTrait.php +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/WorkspaceServiceTrait.php @@ -132,13 +132,11 @@ public function aWorkspaceWithBaseWorkspaceExistsWithoutMetadata(string $workspa */ public function theTitleOfWorkspaceIsSetTo(string $workspaceName, string $newTitle): void { - $this->getObject(SecurityContext::class)->withoutAuthorizationChecks(fn () => - $this->tryCatchingExceptions(fn () => $this->getObject(WorkspaceService::class)->setWorkspaceTitle( - $this->currentContentRepository->id, - WorkspaceName::fromString($workspaceName), - WorkspaceTitle::fromString($newTitle), - )) - ); + $this->tryCatchingExceptions(fn () => $this->getObject(WorkspaceService::class)->setWorkspaceTitle( + $this->currentContentRepository->id, + WorkspaceName::fromString($workspaceName), + WorkspaceTitle::fromString($newTitle), + )); } /** @@ -146,13 +144,11 @@ public function theTitleOfWorkspaceIsSetTo(string $workspaceName, string $newTit */ public function theDescriptionOfWorkspaceIsSetTo(string $workspaceName, string $newDescription): void { - $this->getObject(SecurityContext::class)->withoutAuthorizationChecks(fn () => - $this->tryCatchingExceptions(fn () => $this->getObject(WorkspaceService::class)->setWorkspaceDescription( - $this->currentContentRepository->id, - WorkspaceName::fromString($workspaceName), - WorkspaceDescription::fromString($newDescription), - )) - ); + $this->tryCatchingExceptions(fn () => $this->getObject(WorkspaceService::class)->setWorkspaceDescription( + $this->currentContentRepository->id, + WorkspaceName::fromString($workspaceName), + WorkspaceDescription::fromString($newDescription), + )); } /** @@ -180,16 +176,14 @@ public function theRoleIsAssignedToWorkspaceForGroupOrUser(string $role, string } else { $subject = WorkspaceRoleSubject::createForUser($this->userIdForUsername($username)); } - $this->getObject(SecurityContext::class)->withoutAuthorizationChecks(fn () => - $this->tryCatchingExceptions(fn () => $this->getObject(WorkspaceService::class)->assignWorkspaceRole( - $this->currentContentRepository->id, - WorkspaceName::fromString($workspaceName), - WorkspaceRoleAssignment::create( - $subject, - WorkspaceRole::from($role) - ) - )) - ); + $this->tryCatchingExceptions(fn () => $this->getObject(WorkspaceService::class)->assignWorkspaceRole( + $this->currentContentRepository->id, + WorkspaceName::fromString($workspaceName), + WorkspaceRoleAssignment::create( + $subject, + WorkspaceRole::from($role) + ) + )); } /** @@ -203,13 +197,11 @@ public function theRoleIsUnassignedFromWorkspace(string $workspaceName, string $ } else { $subject = WorkspaceRoleSubject::createForUser($this->userIdForUsername($username)); } - $this->getObject(SecurityContext::class)->withoutAuthorizationChecks(fn () => - $this->tryCatchingExceptions(fn () => $this->getObject(WorkspaceService::class)->unassignWorkspaceRole( - $this->currentContentRepository->id, - WorkspaceName::fromString($workspaceName), - $subject, - )) - ); + $this->tryCatchingExceptions(fn () => $this->getObject(WorkspaceService::class)->unassignWorkspaceRole( + $this->currentContentRepository->id, + WorkspaceName::fromString($workspaceName), + $subject, + )); } /** From 8768298c1ed45802094fc59be20e1e82c9c4a46f Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Fri, 15 Nov 2024 08:54:05 +0100 Subject: [PATCH 179/214] TASK: Add security tests for workspace service assert that base workspace creation is only allowed for writing and that managing (setting title and roles) is only allowed to managers --- .../Domain/Service/WorkspaceService.php | 2 +- .../Features/Bootstrap/ExceptionsTrait.php | 6 +- .../Security/WorkspacePermissions.feature | 69 +++++++++++++++++++ 3 files changed, 75 insertions(+), 2 deletions(-) diff --git a/Neos.Neos/Classes/Domain/Service/WorkspaceService.php b/Neos.Neos/Classes/Domain/Service/WorkspaceService.php index 95be477088c..b4021fae155 100644 --- a/Neos.Neos/Classes/Domain/Service/WorkspaceService.php +++ b/Neos.Neos/Classes/Domain/Service/WorkspaceService.php @@ -280,7 +280,7 @@ private function requireManagementWorkspacePermission(ContentRepositoryId $conte $this->userService->getCurrentUser()?->getId() ); if (!$workspacePermissions->manage) { - throw new AccessDenied(sprintf('The current user does not have manage permissions for workspace "%s" in content repository "%s"', $workspaceName->value, $contentRepositoryId->value), 1731654519); + throw new AccessDenied(sprintf('Managing workspace "%s" in "%s" was denied: %s', $workspaceName->value, $contentRepositoryId->value, $workspacePermissions->getReason()), 1731654519); } } } diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/ExceptionsTrait.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/ExceptionsTrait.php index 5a87e107249..b2257a5c4d2 100644 --- a/Neos.Neos/Tests/Behavior/Features/Bootstrap/ExceptionsTrait.php +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/ExceptionsTrait.php @@ -38,10 +38,11 @@ private function tryCatchingExceptions(\Closure $callback): mixed } /** + * @Then an exception of type :expectedShortExceptionName should be thrown with code :code * @Then an exception of type :expectedShortExceptionName should be thrown with message: * @Then an exception of type :expectedShortExceptionName should be thrown */ - public function anExceptionShouldBeThrown(string $expectedShortExceptionName, PyStringNode $expectedExceptionMessage = null): void + public function anExceptionShouldBeThrown(string $expectedShortExceptionName, ?int $code = null, PyStringNode $expectedExceptionMessage = null): void { Assert::assertNotNull($this->lastCaughtException, 'Expected an exception but none was thrown'); $lastCaughtExceptionShortName = (new \ReflectionClass($this->lastCaughtException))->getShortName(); @@ -49,6 +50,9 @@ public function anExceptionShouldBeThrown(string $expectedShortExceptionName, Py if ($expectedExceptionMessage !== null) { Assert::assertSame($expectedExceptionMessage->getRaw(), $this->lastCaughtException->getMessage()); } + if ($code !== null) { + Assert::assertSame($code, $this->lastCaughtException->getCode()); + } $this->lastCaughtException = null; } diff --git a/Neos.Neos/Tests/Behavior/Features/ContentRepository/Security/WorkspacePermissions.feature b/Neos.Neos/Tests/Behavior/Features/ContentRepository/Security/WorkspacePermissions.feature index b7ba9bfb5cc..7b02cca647b 100644 --- a/Neos.Neos/Tests/Behavior/Features/ContentRepository/Security/WorkspacePermissions.feature +++ b/Neos.Neos/Tests/Behavior/Features/ContentRepository/Security/WorkspacePermissions.feature @@ -82,6 +82,35 @@ Feature: Workspace permission related features | admin | | editor | | restricted_editor | + | owner | + | collaborator | + | uninvolved | + + Scenario Outline: Creating a base workspace without WRITE permissions + Given I am authenticated as + And the shared workspace "some-shared-workspace" is created with the target workspace "workspace" + Then an exception of type "AccessDenied" should be thrown with code 1729086686 + + And the personal workspace "some-other-personal-workspace" is created with the target workspace "workspace" for user + Then an exception of type "AccessDenied" should be thrown with code 1729086686 + + Examples: + | user | + | admin | + | editor | + | restricted_editor | + | uninvolved | + + Scenario Outline: Creating a base workspace with WRITE permissions + Given I am authenticated as + And the shared workspace "some-shared-workspace" is created with the target workspace "workspace" + + And the personal workspace "some-other-personal-workspace" is created with the target workspace "workspace" for user + + Examples: + | user | + | collaborator | + | owner | Scenario Outline: Deleting a workspace without MANAGE permissions Given I am authenticated as @@ -103,7 +132,43 @@ Feature: Workspace permission related features | manager | | owner | + Scenario Outline: Managing metadata and roles of a workspace without MANAGE permissions + Given I am authenticated as + And the title of workspace "workspace" is set to "Some new workspace title" + Then an exception of type "AccessDenied" should be thrown with code 1731654519 + + And the description of workspace "workspace" is set to "Some new workspace description" + Then an exception of type "AccessDenied" should be thrown with code 1731654519 + + When the role COLLABORATOR is assigned to workspace "workspace" for group "Neos.Neos:AbstractEditor" + Then an exception of type "AccessDenied" should be thrown with code 1731654519 + + When the role for group "Neos.Neos:AbstractEditor" is unassigned from workspace "workspace" + Then an exception of type "AccessDenied" should be thrown with code 1731654519 + + Examples: + | user | + | collaborator | + | uninvolved | + + Scenario Outline: Managing metadata and roles of a workspace with MANAGE permissions + Given I am authenticated as + And the title of workspace "workspace" is set to "Some new workspace title" + And the description of workspace "workspace" is set to "Some new workspace description" + When the role COLLABORATOR is assigned to workspace "workspace" for group "Neos.Neos:AbstractEditor" + When the role for group "Neos.Neos:AbstractEditor" is unassigned from workspace "workspace" + + Examples: + | user | + | admin | + | manager | + | owner | + Scenario Outline: Handling commands that require WRITE permissions on the workspace + When I am authenticated as "uninvolved" + And the command is executed with payload '' and exceptions are caught + Then the last command should have thrown an exception of type "AccessDenied" with code 1729086686 + When I am authenticated as "editor" And the command is executed with payload '' and exceptions are caught Then the last command should have thrown an exception of type "AccessDenied" with code 1729086686 @@ -119,6 +184,10 @@ Feature: Workspace permission related features When I am authenticated as "owner" And the command is executed with payload '' + # todo test also collaborator, but cannot commands twice here: + # When I am authenticated as "collaborator" + # And the command is executed with payload '' and exceptions are caught + Examples: | command | command payload | | CreateNodeAggregateWithNode | {"nodeAggregateId":"a1b1","parentNodeAggregateId":"a1b","nodeTypeName":"Neos.Neos:Document"} | From 39de98c03e20fbdf1c1a49a9f90fb51242b6e2b9 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Fri, 15 Nov 2024 10:37:40 +0100 Subject: [PATCH 180/214] TASK: Move migration to `migrateevents:reorderNodeAggregateWasRemoved` --- .../MigrateEventsCommandController.php | 17 +++ .../NeosBetaMigrationCommandController.php | 125 ------------------ .../Classes/Service/EventMigrationService.php | 71 +++++++++- 3 files changed, 87 insertions(+), 126 deletions(-) delete mode 100644 Neos.ContentRepositoryRegistry/Classes/Command/NeosBetaMigrationCommandController.php diff --git a/Neos.ContentRepositoryRegistry/Classes/Command/MigrateEventsCommandController.php b/Neos.ContentRepositoryRegistry/Classes/Command/MigrateEventsCommandController.php index 4825a4420b9..56437a286c6 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Command/MigrateEventsCommandController.php +++ b/Neos.ContentRepositoryRegistry/Classes/Command/MigrateEventsCommandController.php @@ -116,4 +116,21 @@ public function migrateSetReferencesToMultiNameFormatCommand(string $contentRepo $eventMigrationService = $this->contentRepositoryRegistry->buildService($contentRepositoryId, $this->eventMigrationServiceFactory); $eventMigrationService->migrateReferencesToMultiFormat($this->outputLine(...)); } + + /** + * Reorders all NodeAggregateWasMoved events to allow replaying in case orphaned nodes existed in previous betas + * + * Fixes these bugs to allow to migrate to Beta 15: + * + * - #5364 https://github.com/neos/neos-development-collection/issues/5364 + * - #5352 https://github.com/neos/neos-development-collection/issues/5352 + * + * Included in November 2024 - before final Neos 9.0 release + */ + public function reorderNodeAggregateWasRemovedCommand(string $contentRepository = 'default'): void + { + $contentRepositoryId = ContentRepositoryId::fromString($contentRepository); + $eventMigrationService = $this->contentRepositoryRegistry->buildService($contentRepositoryId, $this->eventMigrationServiceFactory); + $eventMigrationService->reorderNodeAggregateWasRemoved($this->outputLine(...)); + } } diff --git a/Neos.ContentRepositoryRegistry/Classes/Command/NeosBetaMigrationCommandController.php b/Neos.ContentRepositoryRegistry/Classes/Command/NeosBetaMigrationCommandController.php deleted file mode 100644 index ac63222d7fe..00000000000 --- a/Neos.ContentRepositoryRegistry/Classes/Command/NeosBetaMigrationCommandController.php +++ /dev/null @@ -1,125 +0,0 @@ -backup($contentRepositoryId); - - $workspace = $this->contentRepositoryRegistry->get($contentRepositoryId)->findWorkspaceByName(WorkspaceName::fromString($workspaceName)); - if (!$workspace) { - $this->outputLine('Workspace not found'); - $this->quit(1); - } - - $streamName = ContentStreamEventStreamName::fromContentStreamId($workspace->currentContentStreamId)->getEventStreamName(); - - $internals = $this->getInternals($contentRepositoryId); - - // get all NodeAggregateWasRemoved from the content stream - $eventsToReorder = iterator_to_array($internals->eventStore->load($streamName, EventStreamFilter::create(EventTypes::create(EventType::fromString('NodeAggregateWasRemoved')))), false); - - // remove all the NodeAggregateWasRemoved events at their sequenceNumbers - $eventTableName = DoctrineEventStoreFactory::databaseTableName($contentRepositoryId); - $this->connection->beginTransaction(); - $this->connection->executeStatement( - 'DELETE FROM ' . $eventTableName . ' WHERE sequencenumber IN (:sequenceNumbers)', - [ - 'sequenceNumbers' => array_map(fn (EventEnvelope $eventEnvelope) => $eventEnvelope->sequenceNumber->value, $eventsToReorder) - ], - [ - 'sequenceNumbers' => ArrayParameterType::STRING - ] - ); - $this->connection->commit(); - - $mapper = function (EventEnvelope $eventEnvelope): Event { - $metadata = $event->eventMetadata?->value ?? []; - $metadata['reorderedByMigration'] = sprintf('Originally recorded at %s with sequence number %d', $eventEnvelope->recordedAt->format(\DateTimeInterface::ATOM), $eventEnvelope->sequenceNumber->value); - return new Event( - $eventEnvelope->event->id, - $eventEnvelope->event->type, - $eventEnvelope->event->data, - EventMetadata::fromArray($metadata), - $eventEnvelope->event->causationId, - $eventEnvelope->event->correlationId - ); - }; - - // reapply the NodeAggregateWasRemoved events - $internals->eventStore->commit( - $streamName, - Events::fromArray(array_map($mapper, $eventsToReorder)), - ExpectedVersion::ANY() - ); - - $this->outputLine('Reordered %d removals. Please replay and rebase your other workspaces.', [count($eventsToReorder)]); - } - - private function backup(ContentRepositoryId $contentRepositoryId): void - { - $backupEventTableName = DoctrineEventStoreFactory::databaseTableName($contentRepositoryId) - . '_bkp_' . date('Y_m_d_H_i_s'); - $this->copyEventTable($backupEventTableName, $contentRepositoryId); - $this->outputLine(sprintf('Copied events table to %s', $backupEventTableName)); - } - - private function copyEventTable(string $backupEventTableName, ContentRepositoryId $contentRepositoryId): void - { - $eventTableName = DoctrineEventStoreFactory::databaseTableName($contentRepositoryId); - $this->connection->executeStatement( - 'CREATE TABLE ' . $backupEventTableName . ' AS - SELECT * - FROM ' . $eventTableName - ); - } - - private function getInternals(ContentRepositoryId $contentRepositoryId): ContentRepositoryServiceFactoryDependencies - { - // NOT API!!! - $accessor = new class implements ContentRepositoryServiceFactoryInterface { - public ContentRepositoryServiceFactoryDependencies|null $dependencies; - public function build(ContentRepositoryServiceFactoryDependencies $serviceFactoryDependencies): ContentRepositoryServiceInterface - { - $this->dependencies = $serviceFactoryDependencies; - return new class implements ContentRepositoryServiceInterface - { - }; - } - }; - $this->contentRepositoryRegistry->buildService($contentRepositoryId, $accessor); - return $accessor->dependencies; - } -} diff --git a/Neos.ContentRepositoryRegistry/Classes/Service/EventMigrationService.php b/Neos.ContentRepositoryRegistry/Classes/Service/EventMigrationService.php index 2a71cbd4333..f7464fee9c2 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Service/EventMigrationService.php +++ b/Neos.ContentRepositoryRegistry/Classes/Service/EventMigrationService.php @@ -4,10 +4,12 @@ namespace Neos\ContentRepositoryRegistry\Service; +use Doctrine\DBAL\ArrayParameterType; use Doctrine\DBAL\Connection; use Doctrine\DBAL\Exception\UniqueConstraintViolationException; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface; use Neos\ContentRepository\Core\Feature\Common\RebasableToOtherWorkspaceInterface; +use Neos\ContentRepository\Core\Feature\ContentStreamEventStreamName; use Neos\ContentRepository\Core\Feature\NodeCreation\Command\CreateNodeAggregateWithNodeAndSerializedProperties; use Neos\ContentRepository\Core\Feature\NodeDisabling\Command\DisableNodeAggregate; use Neos\ContentRepository\Core\Feature\NodeDisabling\Command\EnableNodeAggregate; @@ -15,23 +17,28 @@ use Neos\ContentRepository\Core\Feature\NodeModification\Command\SetSerializedNodeProperties; use Neos\ContentRepository\Core\Feature\NodeMove\Command\MoveNodeAggregate; use Neos\ContentRepository\Core\Feature\NodeReferencing\Command\SetSerializedNodeReferences; -use Neos\ContentRepository\Core\Feature\NodeReferencing\Event\NodeReferencesWereSet; use Neos\ContentRepository\Core\Feature\NodeRemoval\Command\RemoveNodeAggregate; use Neos\ContentRepository\Core\Feature\NodeRenaming\Command\ChangeNodeAggregateName; use Neos\ContentRepository\Core\Feature\NodeTypeChange\Command\ChangeNodeAggregateType; use Neos\ContentRepository\Core\Feature\NodeVariation\Command\CreateNodeVariant; use Neos\ContentRepository\Core\Feature\RootNodeCreation\Command\CreateRootNodeAggregateWithNode; use Neos\ContentRepository\Core\Feature\RootNodeCreation\Command\UpdateRootNodeAggregateDimensions; +use Neos\ContentRepository\Core\Feature\WorkspaceEventStreamName; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; +use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepositoryRegistry\Command\MigrateEventsCommandController; use Neos\ContentRepositoryRegistry\Factory\EventStore\DoctrineEventStoreFactory; use Neos\EventStore\EventStoreInterface; +use Neos\EventStore\Model\Event; +use Neos\EventStore\Model\Event\EventMetadata; use Neos\EventStore\Model\Event\EventType; use Neos\EventStore\Model\Event\EventTypes; use Neos\EventStore\Model\Event\SequenceNumber; use Neos\EventStore\Model\EventEnvelope; +use Neos\EventStore\Model\Events; use Neos\EventStore\Model\EventStream\EventStreamFilter; +use Neos\EventStore\Model\EventStream\ExpectedVersion; use Neos\EventStore\Model\EventStream\VirtualStreamName; use Neos\Neos\Domain\Model\WorkspaceClassification; use Neos\Neos\Domain\Model\WorkspaceRole; @@ -736,6 +743,68 @@ public function migrateWorkspaceMetadataToWorkspaceService(\Closure $outputFn): $outputFn(sprintf('Added metadata & role assignments for %d workspaces.', $addedWorkspaceMetadata)); } + /** + * Reorders all NodeAggregateWasMoved events to allow replaying in case orphaned nodes existed in previous betas + */ + public function reorderNodeAggregateWasRemoved(\Closure $outputFn): void + { + $liveWorkspaceContentStreamId = null; + // hardcoded to LIVE + foreach ($this->eventStore->load(WorkspaceEventStreamName::fromWorkspaceName(WorkspaceName::forLive())->getEventStreamName(), EventStreamFilter::create(EventTypes::create(EventType::fromString('RootWorkspaceWasCreated')))) as $eventEnvelope) { + $rootWorkspaceWasCreated = self::decodeEventPayload($eventEnvelope); + $liveWorkspaceContentStreamId = ContentStreamId::fromString($rootWorkspaceWasCreated['newContentStreamId']); + break; + } + + if (!$liveWorkspaceContentStreamId) { + throw new \RuntimeException('Workspace live does not exist. No migration necessary.'); + } + + $backupEventTableName = DoctrineEventStoreFactory::databaseTableName($this->contentRepositoryId) . '_bkp_' . date('Y_m_d_H_i_s'); + $outputFn('Backup: copying events table to %s', [$backupEventTableName]); + $this->copyEventTable($backupEventTableName); + + $liveContentStreamName = ContentStreamEventStreamName::fromContentStreamId($liveWorkspaceContentStreamId)->getEventStreamName(); + // get all NodeAggregateWasRemoved from the live content stream + $eventsToReorder = iterator_to_array($this->eventStore->load($liveContentStreamName, EventStreamFilter::create(EventTypes::create(EventType::fromString('NodeAggregateWasRemoved')))), false); + + // remove all the NodeAggregateWasRemoved events at their sequenceNumbers + $eventTableName = DoctrineEventStoreFactory::databaseTableName($this->contentRepositoryId); + $this->connection->beginTransaction(); + $this->connection->executeStatement( + 'DELETE FROM ' . $eventTableName . ' WHERE sequencenumber IN (:sequenceNumbers)', + [ + 'sequenceNumbers' => array_map(fn (EventEnvelope $eventEnvelope) => $eventEnvelope->sequenceNumber->value, $eventsToReorder) + ], + [ + 'sequenceNumbers' => ArrayParameterType::STRING + ] + ); + $this->connection->commit(); + + $mapper = function (EventEnvelope $eventEnvelope): Event { + $metadata = $event->eventMetadata?->value ?? []; + $metadata['reorderedByMigration'] = sprintf('Originally recorded at %s with sequence number %d', $eventEnvelope->recordedAt->format(\DateTimeInterface::ATOM), $eventEnvelope->sequenceNumber->value); + return new Event( + $eventEnvelope->event->id, + $eventEnvelope->event->type, + $eventEnvelope->event->data, + EventMetadata::fromArray($metadata), + $eventEnvelope->event->causationId, + $eventEnvelope->event->correlationId + ); + }; + + // reapply the NodeAggregateWasRemoved events + $this->eventStore->commit( + $liveContentStreamName, + Events::fromArray(array_map($mapper, $eventsToReorder)), + ExpectedVersion::ANY() + ); + + $outputFn(sprintf('Reordered %d removals. Please replay and rebase your other workspaces.', count($eventsToReorder))); + } + /** ------------------------ */ /** From 0436ecf3889b96f305739493dd859805d46b8541 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 17 Nov 2024 21:08:46 +0100 Subject: [PATCH 181/214] WIP: Node Copy Version 3, first draft of NodeDuplicationService --- .../Service/NodeDuplication/Commands.php | 58 ++++++ .../Service/NodeDuplication/TransientNode.php | 189 ++++++++++++++++++ .../Domain/Service/NodeDuplicationService.php | 154 ++++++++++++++ .../Features/Bootstrap/FeatureContext.php | 2 + .../Bootstrap/NodeDuplicationTrait.php | 80 ++++++++ ...ode_ConstraintChecks_TetheredNodes.feature | 51 +++++ .../NodeCopying/CopyNode_NoDimensions.feature | 99 +++++++++ .../CopyNode_TetheredNodes.feature | 82 ++++++++ 8 files changed, 715 insertions(+) create mode 100644 Neos.Neos/Classes/Domain/Service/NodeDuplication/Commands.php create mode 100644 Neos.Neos/Classes/Domain/Service/NodeDuplication/TransientNode.php create mode 100644 Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php create mode 100644 Neos.Neos/Tests/Behavior/Features/Bootstrap/NodeDuplicationTrait.php create mode 100644 Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_ConstraintChecks_TetheredNodes.feature create mode 100644 Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_NoDimensions.feature create mode 100644 Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_TetheredNodes.feature diff --git a/Neos.Neos/Classes/Domain/Service/NodeDuplication/Commands.php b/Neos.Neos/Classes/Domain/Service/NodeDuplication/Commands.php new file mode 100644 index 00000000000..f0c8ea77e47 --- /dev/null +++ b/Neos.Neos/Classes/Domain/Service/NodeDuplication/Commands.php @@ -0,0 +1,58 @@ + + */ +final readonly class Commands implements \IteratorAggregate, \Countable +{ + /** @var array */ + private array $items; + + private function __construct( + CommandInterface ...$items + ) { + $this->items = $items; + } + + public static function create(CommandInterface ...$items): self + { + return new self(...$items); + } + + public static function createEmpty(): self + { + return new self(); + } + + /** @param array $array */ + public static function fromArray(array $array): self + { + return new self(...$array); + } + + public function append(CommandInterface $command): self + { + return new self(...[...$this->items, $command]); + } + + public function merge(self $other): self + { + return new self(...$this->items, ...$other->items); + } + + public function getIterator(): \Traversable + { + yield from $this->items; + } + + public function count(): int + { + return count($this->items); + } +} diff --git a/Neos.Neos/Classes/Domain/Service/NodeDuplication/TransientNode.php b/Neos.Neos/Classes/Domain/Service/NodeDuplication/TransientNode.php new file mode 100644 index 00000000000..faa97d78e65 --- /dev/null +++ b/Neos.Neos/Classes/Domain/Service/NodeDuplication/TransientNode.php @@ -0,0 +1,189 @@ +tetheredNodeName !== null) { + assert($this->tetheredParentNodeType !== null); + } + } + + public static function forRegular( + NodeAggregateId $nodeAggregateId, + WorkspaceName $workspaceName, + OriginDimensionSpacePoint $originDimensionSpacePoint, + NodeTypeName $nodeTypeName, + NodeAggregateIdsByNodePaths $tetheredNodeAggregateIds, + NodeTypeManager $nodeTypeManager + ): self { + $nodeType = $nodeTypeManager->getNodeType($nodeTypeName); + return new self( + $nodeAggregateId, + $workspaceName, + $originDimensionSpacePoint, + $nodeType, + $tetheredNodeAggregateIds, + null, + null, + $nodeTypeManager + ); + } + + public function forTetheredChildNode(NodeName $nodeName): self + { + $nodeAggregateId = $this->tetheredNodeAggregateIds->getNodeAggregateId(NodePath::fromNodeNames($nodeName)); + + $tetheredNodeTypeDefinition = $this->nodeType->tetheredNodeTypeDefinitions->get($nodeName); + + if (!$nodeAggregateId || !$tetheredNodeTypeDefinition) { + throw new \InvalidArgumentException('forTetheredChildNode only works for tethered nodes.'); + } + + $childNodeType = $this->nodeTypeManager->getNodeType($tetheredNodeTypeDefinition->nodeTypeName); + if (!$childNodeType) { + throw new \InvalidArgumentException(sprintf('NodeType "%s" for tethered node "%s" does not exist.', $tetheredNodeTypeDefinition->nodeTypeName->value, $nodeName->value), 1718950833); + } + + $descendantTetheredNodeAggregateIds = NodeAggregateIdsByNodePaths::createEmpty(); + foreach ($this->tetheredNodeAggregateIds->getNodeAggregateIds() as $stringNodePath => $descendantNodeAggregateId) { + $nodePath = NodePath::fromString($stringNodePath); + $pathParts = $nodePath->getParts(); + $firstPart = array_shift($pathParts); + if ($firstPart?->equals($nodeName) && count($pathParts)) { + $descendantTetheredNodeAggregateIds = $descendantTetheredNodeAggregateIds->add( + NodePath::fromNodeNames(...$pathParts), + $descendantNodeAggregateId + ); + } + } + + return new self( + $nodeAggregateId, + $this->workspaceName, + $this->originDimensionSpacePoint, + $childNodeType, + $descendantTetheredNodeAggregateIds, + $nodeName, + $this->nodeType, + $this->nodeTypeManager + ); + } + + public function forRegularChildNode(NodeAggregateId $nodeAggregateId, NodeTypeName $nodeTypeName): self + { + $nodeType = $this->nodeTypeManager->getNodeType($nodeTypeName); + $tetheredNodeAggregateIds = NodeAggregateIdsByNodePaths::createForNodeType($nodeTypeName, $this->nodeTypeManager); + return new self( + $nodeAggregateId, + $this->workspaceName, + $this->originDimensionSpacePoint, + $nodeType, + $tetheredNodeAggregateIds, + null, + null, + $this->nodeTypeManager + ); + } + + public function withTetheredNodeAggregateIds(NodeAggregateIdsByNodePaths $nodeAggregateIdsByNodePaths): self + { + return new self( + $this->aggregateId, + $this->workspaceName, + $this->originDimensionSpacePoint, + $this->nodeType, + $nodeAggregateIdsByNodePaths, + $this->tetheredNodeName, + $this->tetheredParentNodeType, + $this->nodeTypeManager + ); + } + + /** + * @throws NodeConstraintException + */ + public function requireConstraintsImposedByAncestorsToBeMet(NodeType $childNodeType): void + { + if ($this->isTethered()) { + $this->requireNodeTypeConstraintsImposedByGrandparentToBeMet($this->tetheredParentNodeType->name, $this->tetheredNodeName, $childNodeType->name); + } else { + self::requireNodeTypeConstraintsImposedByParentToBeMet($this->nodeType, $childNodeType); + } + } + + private static function requireNodeTypeConstraintsImposedByParentToBeMet(NodeType $parentNodeType, NodeType $nodeType): void + { + if (!$parentNodeType->allowsChildNodeType($nodeType)) { + throw new NodeConstraintException( + sprintf( + 'Node type "%s" is not allowed for child nodes of type %s', + $nodeType->name->value, + $parentNodeType->name->value + ), + 1686417627173 + ); + } + } + + private function requireNodeTypeConstraintsImposedByGrandparentToBeMet(NodeTypeName $parentNodeTypeName, NodeName $tetheredNodeName, NodeTypeName $nodeTypeNameToCheck): void + { + if (!$this->nodeTypeManager->isNodeTypeAllowedAsChildToTetheredNode($parentNodeTypeName, $tetheredNodeName, $nodeTypeNameToCheck)) { + throw new NodeConstraintException( + sprintf( + 'Node type "%s" is not allowed below tethered child nodes "%s" of nodes of type "%s"', + $nodeTypeNameToCheck->value, + $tetheredNodeName->value, + $parentNodeTypeName->value + ), + 1687541480146 + ); + } + } + + /** + * @phpstan-assert-if-true !null $this->tetheredNodeName + * @phpstan-assert-if-true !null $this->tetheredParentNodeType + */ + private function isTethered(): bool + { + return $this->tetheredNodeName !== null; + } +} diff --git a/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php b/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php new file mode 100644 index 00000000000..684e996b8e4 --- /dev/null +++ b/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php @@ -0,0 +1,154 @@ +contentRepositoryRegistry->get($contentRepositoryId); + + $subgraph = $contentRepository->getContentGraph($workspaceName)->getSubgraph($sourceDimensionSpacePoint, VisibilityConstraints::withoutRestrictions()); + + $subtree = $subgraph->findSubtree($sourceNodeAggregateId, FindSubtreeFilter::create()); + $targetParentNode = $subgraph->findNodeById($targetParentNodeAggregateId); + + $entryTransientNode = TransientNode::forRegular( + $targetParentNodeAggregateId, + $workspaceName, + $targetDimensionSpacePoint, + $targetParentNode->nodeTypeName, + NodeAggregateIdsByNodePaths::createEmpty(), + $contentRepository->getNodeTypeManager() + ); + + $commands = $this->commandsForSubtreeRecursively($entryTransientNode, $subtree, $nodeAggregateIdMapping, Commands::createEmpty()); + + foreach ($commands as $command) { + $contentRepository->handle($command); + } + } + + private function commandsForSubtreeRecursively(TransientNode $transientParentNode, Subtree $subtree, NodeAggregateIdMapping $nodeAggregateIdMapping, Commands $commands): Commands + { + if ($subtree->node->classification->isTethered()) { + $transientNode = $transientParentNode->forTetheredChildNode( + $subtree->node->name + ); + + $setPropertiesOfTetheredNodeCommand = SetNodeProperties::create( + $transientParentNode->workspaceName, + $transientNode->aggregateId, + $transientParentNode->originDimensionSpacePoint, + PropertyValuesToWrite::fromArray( + iterator_to_array($subtree->node->properties) + ), + ); + // todo references: + + $commands = $commands->append($setPropertiesOfTetheredNodeCommand); + } else { + $transientNode = $transientParentNode->forRegularChildNode( + $nodeAggregateIdMapping->getNewNodeAggregateId($subtree->node->aggregateId) ?? NodeAggregateId::create(), + $subtree->node->nodeTypeName + ); + + $createCopyOfNodeCommand = CreateNodeAggregateWithNode::create( + $transientParentNode->workspaceName, + $transientNode->aggregateId, + $subtree->node->nodeTypeName, + $transientParentNode->originDimensionSpacePoint, + $transientParentNode->aggregateId, + // todo succeedingSiblingNodeAggregateId + // todo skip properties not in schema + initialPropertyValues: PropertyValuesToWrite::fromArray( + iterator_to_array($subtree->node->properties) + ), + // todo references: + ); + + $tetheredDescendantNodeAggregateIds = $this->getTetheredDescendantNodeAggregateIds( + $subtree, + $nodeAggregateIdMapping, + NodePath::forRoot(), + NodeAggregateIdsByNodePaths::createEmpty() + ); + + $createCopyOfNodeCommand = $createCopyOfNodeCommand->withTetheredDescendantNodeAggregateIds( + $tetheredDescendantNodeAggregateIds + ); + + $transientNode = $transientNode->withTetheredNodeAggregateIds($tetheredDescendantNodeAggregateIds); + + $commands = $commands->append($createCopyOfNodeCommand); + } + + foreach ($subtree->children as $childSubtree) { + $commands = $commands->merge( + $this->commandsForSubtreeRecursively($transientNode, $childSubtree, $nodeAggregateIdMapping, $commands) + ); + } + + return $commands; + } + + private function getTetheredDescendantNodeAggregateIds(Subtree $subtree, NodeAggregateIdMapping $nodeAggregateIdMapping, NodePath $nodePath, NodeAggregateIdsByNodePaths $tetheredNodeAggregateIds): NodeAggregateIdsByNodePaths + { + foreach ($subtree->children as $childSubtree) { + if ($childSubtree->node->classification->isTethered()) { + return $tetheredNodeAggregateIds; + } + + $deterministicCopyAggregateId = $nodeAggregateIdMapping->getNewNodeAggregateId($childSubtree->node->aggregateId); + + $childNodePath = $nodePath->appendPathSegment($subtree->node->name); + + if ($deterministicCopyAggregateId) { + $tetheredNodeAggregateIds = $tetheredNodeAggregateIds->add( + $childNodePath, + $deterministicCopyAggregateId + ); + } + + $tetheredNodeAggregateIds = $this->getTetheredDescendantNodeAggregateIds($childSubtree, $nodeAggregateIdMapping, $childNodePath, $tetheredNodeAggregateIds); + } + + return $tetheredNodeAggregateIds; + } +} diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/FeatureContext.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/FeatureContext.php index fce5b7a3278..c8719bb5368 100644 --- a/Neos.Neos/Tests/Behavior/Features/Bootstrap/FeatureContext.php +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/FeatureContext.php @@ -49,6 +49,8 @@ class FeatureContext implements BehatContext use WorkspaceServiceTrait; use UserServiceTrait; + use NodeDuplicationTrait; + protected Environment $environment; protected ContentRepositoryRegistry $contentRepositoryRegistry; diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/NodeDuplicationTrait.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/NodeDuplicationTrait.php new file mode 100644 index 00000000000..ebf7a90856e --- /dev/null +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/NodeDuplicationTrait.php @@ -0,0 +1,80 @@ + $className + * + * @return T + */ + abstract private function getObject(string $className): object; + + abstract protected function readPayloadTable(TableNode $payloadTable): array; + + /** + * @When /^copy nodes recursively is executed with payload:$/ + */ + public function copyNodesRecursivelyIsExecutedWithPayload(TableNode $payloadTable): void + { + $commandArguments = $this->readPayloadTable($payloadTable); + + $workspaceName = isset($commandArguments['workspaceName']) + ? WorkspaceName::fromString($commandArguments['workspaceName']) + : $this->currentWorkspaceName; + + $sourceNodeAggregateId = NodeAggregateId::fromString($commandArguments['sourceNodeAggregateId']); + $sourceDimensionSpacePoint = isset($commandArguments['sourceDimensionSpacePoint']) + ? DimensionSpacePoint::fromArray($commandArguments['sourceDimensionSpacePoint']) + : $this->currentDimensionSpacePoint; + + $targetDimensionSpacePoint = isset($commandArguments['targetDimensionSpacePoint']) + ? OriginDimensionSpacePoint::fromArray($commandArguments['targetDimensionSpacePoint']) + : OriginDimensionSpacePoint::fromDimensionSpacePoint($this->currentDimensionSpacePoint); + + $targetSucceedingSiblingNodeAggregateId = isset($commandArguments['targetSucceedingSiblingNodeAggregateId']) + ? NodeAggregateId::fromString($commandArguments['targetSucceedingSiblingNodeAggregateId']) + : null; + + $this->getObject(NodeDuplicationService::class)->copyNodesRecursively( + $this->currentContentRepository->id, + $workspaceName, + $sourceDimensionSpacePoint, + $sourceNodeAggregateId, + $targetDimensionSpacePoint, + NodeAggregateId::fromString($commandArguments['targetParentNodeAggregateId']), + isset($commandArguments['targetNodeName']) ? NodeName::fromString($commandArguments['targetNodeName']) : null, + $targetSucceedingSiblingNodeAggregateId, + NodeAggregateIdMapping::fromArray($commandArguments['nodeAggregateIdMapping']) + ); + } +} diff --git a/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_ConstraintChecks_TetheredNodes.feature b/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_ConstraintChecks_TetheredNodes.feature new file mode 100644 index 00000000000..e157210c9d5 --- /dev/null +++ b/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_ConstraintChecks_TetheredNodes.feature @@ -0,0 +1,51 @@ +@contentrepository @adapters=DoctrineDBAL +Feature: Copy nodes (without dimensions) + + Background: + Given using no content dimensions + And using the following node types: + """yaml + 'Neos.ContentRepository.Testing:Tethered': [] + 'Neos.ContentRepository.Testing:TetheredDocument': + childNodes: + tethered: + type: 'Neos.ContentRepository.Testing:Tethered' + 'Neos.ContentRepository.Testing:Document': + childNodes: + tethered-document: + type: 'Neos.ContentRepository.Testing:TetheredDocument' + 'Neos.ContentRepository.Testing:DocumentWithoutTetheredChildren': [] + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + And I am user identified by "initiating-user-identifier" + And the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | newContentStreamId | "cs-identifier" | + And I am in workspace "live" and dimension space point {} + And the command CreateRootNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "lady-eleonode-rootford" | + | nodeTypeName | "Neos.ContentRepository:Root" | + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | parentNodeAggregateId | nodeTypeName | tetheredDescendantNodeAggregateIds | + | node-mc-nodeface | lady-eleonode-rootford | Neos.ContentRepository.Testing:DocumentWithoutTetheredChildren | {} | + | node-wan-kenody | lady-eleonode-rootford | Neos.ContentRepository.Testing:Document | {"tethered-document": "nodewyn-tetherton", "tethered-document/tethered": "nodimer-tetherton"} | + + Scenario: Coping fails if the leaf of a nested tethered node is attempted to be copied + And I expect the node aggregate "nodewyn-tetherton" to exist + And I expect this node aggregate to be classified as "tethered" + + And I expect the node aggregate "nodimer-tetherton" to exist + And I expect this node aggregate to be classified as "tethered" + + When copy nodes recursively is executed with payload: + | Key | Value | + | sourceDimensionSpacePoint | {} | + | sourceNodeAggregateId | "nodewyn-tetherton" | + | targetDimensionSpacePoint | {} | + | targetParentNodeAggregateId | "node-mc-nodeface" | + | nodeAggregateIdMapping | {"nodewyn-tetherton": "nodewyn-tetherton-copy", "nodimer-tetherton": "nodimer-tetherton-copy"} | + + Then the last command should have thrown an exception of type "TetheredNodesCannotBePartiallyCopied" diff --git a/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_NoDimensions.feature b/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_NoDimensions.feature new file mode 100644 index 00000000000..1c76f9b9a04 --- /dev/null +++ b/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_NoDimensions.feature @@ -0,0 +1,99 @@ +@contentrepository @adapters=DoctrineDBAL +Feature: Copy nodes (without dimensions) + + Background: + Given using no content dimensions + And using the following node types: + """yaml + 'Neos.ContentRepository.Testing:Document': + references: + ref: [] + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + And the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | newContentStreamId | "cs-identifier" | + And I am in workspace "live" + And the command CreateRootNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "lady-eleonode-rootford" | + | nodeTypeName | "Neos.ContentRepository:Root" | + And the event NodeAggregateWithNodeWasCreated was published with payload: + | Key | Value | + | workspaceName | "live" | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "sir-david-nodenborough" | + | nodeTypeName | "Neos.ContentRepository.Testing:Document" | + | originDimensionSpacePoint | {} | + | coveredDimensionSpacePoints | [{}] | + | parentNodeAggregateId | "lady-eleonode-rootford" | + | nodeName | "document" | + | nodeAggregateClassification | "regular" | + And the event NodeAggregateWithNodeWasCreated was published with payload: + | Key | Value | + | workspaceName | "live" | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | nodeTypeName | "Neos.ContentRepository.Testing:Document" | + | originDimensionSpacePoint | {} | + | coveredDimensionSpacePoints | [{}] | + | parentNodeAggregateId | "sir-david-nodenborough" | + | nodeName | "child-document" | + | nodeAggregateClassification | "regular" | + And the event NodeAggregateWithNodeWasCreated was published with payload: + | Key | Value | + | workspaceName | "live" | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "sir-nodeward-nodington-iii" | + | nodeTypeName | "Neos.ContentRepository.Testing:Document" | + | originDimensionSpacePoint | {} | + | coveredDimensionSpacePoints | [{}] | + | parentNodeAggregateId | "lady-eleonode-rootford" | + | nodeName | "esquire" | + | nodeAggregateClassification | "regular" | + + Scenario: Copy + When I am in workspace "live" and dimension space point {} + When copy nodes recursively is executed with payload: + | Key | Value | + | sourceDimensionSpacePoint | {} | + | sourceNodeAggregateId | "sir-nodeward-nodington-iii" | + | targetDimensionSpacePoint | {} | + | targetParentNodeAggregateId | "nody-mc-nodeface" | + | targetNodeName | "target-nn" | + | targetSucceedingSiblingnodeAggregateId | null | + | nodeAggregateIdMapping | {"sir-nodeward-nodington-iii": "sir-nodeward-nodington-iii-copy"} | + + Then I expect node aggregate identifier "sir-nodeward-nodington-iii-copy" to lead to node cs-identifier;sir-nodeward-nodington-iii-copy;{} + And I expect the node aggregate "sir-nodeward-nodington-iii-copy" to exist + And I expect this node aggregate to be classified as "regular" + And I expect this node aggregate to be named "target-nn" + And I expect this node aggregate to be of type "Neos.ContentRepository.Testing:Document" + And I expect this node aggregate to occupy dimension space points [[]] + And I expect this node aggregate to disable dimension space points [] + And I expect this node aggregate to have no child node aggregates + And I expect this node aggregate to have the parent node aggregates ["nody-mc-nodeface"] + + Scenario: Copy References + When I am in workspace "live" and dimension space point {} + And the command SetNodeReferences is executed with payload: + | Key | Value | + | sourceNodeAggregateId | "sir-nodeward-nodington-iii" | + | references | [{"referenceName": "ref", "references": [{"target": "sir-david-nodenborough"}]}] | + + When copy nodes recursively is executed with payload: + | Key | Value | + | sourceDimensionSpacePoint | {} | + | sourceNodeAggregateId | "sir-nodeward-nodington-iii" | + | targetDimensionSpacePoint | {} | + | targetParentNodeAggregateId | "nody-mc-nodeface" | + | targetNodeName | "target-nn" | + | targetSucceedingSiblingnodeAggregateId | null | + | nodeAggregateIdMapping | {"sir-nodeward-nodington-iii": "sir-nodeward-nodington-iii-copy"} | + + And I expect node aggregate identifier "sir-nodeward-nodington-iii-copy" to lead to node cs-identifier;sir-nodeward-nodington-iii-copy;{} + And I expect this node to have the following references: + | Name | Node | Properties | + | ref | cs-identifier;sir-david-nodenborough;{} | null | diff --git a/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_TetheredNodes.feature b/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_TetheredNodes.feature new file mode 100644 index 00000000000..f1298eda85c --- /dev/null +++ b/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_TetheredNodes.feature @@ -0,0 +1,82 @@ +Feature: Copy nodes with tethered nodes + + Background: + Given using no content dimensions + And using the following node types: + """yaml + 'Neos.ContentRepository.Testing:Document': [] + 'Neos.ContentRepository.Testing:DocumentWithTethered': + childNodes: + tethered: + type: 'Neos.ContentRepository.Testing:Document' + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + And the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | newContentStreamId | "cs-identifier" | + When I am in workspace "live" and dimension space point {} + And I am user identified by "initiating-user-identifier" + And the command CreateRootNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "lady-eleonode-rootford" | + | nodeTypeName | "Neos.ContentRepository:Root" | + + When the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | parentNodeAggregateId | nodeTypeName | tetheredDescendantNodeAggregateIds | + | sir-david-nodenborough | lady-eleonode-rootford | Neos.ContentRepository.Testing:Document | {} | + | nody-mc-nodeface | sir-david-nodenborough | Neos.ContentRepository.Testing:Document | | + | sir-nodeward-nodington-i | lady-eleonode-rootford | Neos.ContentRepository.Testing:DocumentWithTethered | {"tethered": "nodewyn-tetherton"} | + + Scenario: Coping a tethered node turns it into a regular node + And I expect the node aggregate "nodewyn-tetherton" to exist + And I expect this node aggregate to be classified as "tethered" + + When copy nodes recursively is executed with payload: + | Key | Value | + | sourceDimensionSpacePoint | {} | + | sourceNodeAggregateId | "nodewyn-tetherton" | + | targetDimensionSpacePoint | {} | + | targetParentNodeAggregateId | "sir-david-nodenborough" | + | nodeAggregateIdMapping | {"nodewyn-tetherton": "nodewyn-tetherton-copy"} | + + And I expect the node aggregate "nodewyn-tetherton-copy" to exist + # must not be tethered! + And I expect this node aggregate to be classified as "regular" + And I expect this node aggregate to be of type "Neos.ContentRepository.Testing:Document" + And I expect this node aggregate to be unnamed + And I expect this node aggregate to occupy dimension space points [[]] + And I expect this node aggregate to disable dimension space points [] + And I expect this node aggregate to have no child node aggregates + And I expect this node aggregate to have the parent node aggregates ["sir-david-nodenborough"] + + Scenario: Coping a node with tethered node keeps the child node tethered + And I expect the node aggregate "nodewyn-tetherton" to exist + And I expect this node aggregate to be classified as "tethered" + + When copy nodes recursively is executed with payload: + | Key | Value | + | sourceDimensionSpacePoint | {} | + | sourceNodeAggregateId | "sir-nodeward-nodington-i" | + | targetDimensionSpacePoint | {} | + | targetParentNodeAggregateId | "sir-david-nodenborough" | + | nodeAggregateIdMapping | {"sir-nodeward-nodington-i": "sir-nodeward-nodington-ii", "nodewyn-tetherton": "nodewyn-tetherton-copy"} | + + And I expect the node aggregate "sir-nodeward-nodington-ii" to exist + And I expect this node aggregate to be classified as "regular" + And I expect this node aggregate to be unnamed + And I expect this node aggregate to be of type "Neos.ContentRepository.Testing:DocumentWithTethered" + And I expect this node aggregate to occupy dimension space points [[]] + And I expect this node aggregate to disable dimension space points [] + And I expect this node aggregate to have the child node aggregates ["nodewyn-tetherton-copy"] + And I expect this node aggregate to have the parent node aggregates ["sir-david-nodenborough"] + + And I expect the node aggregate "nodewyn-tetherton-copy" to exist + And I expect this node aggregate to be classified as "tethered" + And I expect this node aggregate to be named "tethered" + And I expect this node aggregate to be of type "Neos.ContentRepository.Testing:Document" + And I expect this node aggregate to occupy dimension space points [[]] + And I expect this node aggregate to disable dimension space points [] + And I expect this node aggregate to have no child node aggregates + And I expect this node aggregate to have the parent node aggregates ["sir-nodeward-nodington-ii"] From 2393125a96b68112639c3734f277f4dd9a7f4faf Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 17 Nov 2024 21:54:28 +0100 Subject: [PATCH 182/214] WIP: Fix copy tethered node if leave node or nested --- .../Service/NodeDuplication/TransientNode.php | 23 ++--- .../Domain/Service/NodeDuplicationService.php | 88 ++++++++++++++----- 2 files changed, 71 insertions(+), 40 deletions(-) diff --git a/Neos.Neos/Classes/Domain/Service/NodeDuplication/TransientNode.php b/Neos.Neos/Classes/Domain/Service/NodeDuplication/TransientNode.php index faa97d78e65..05a4df80ef8 100644 --- a/Neos.Neos/Classes/Domain/Service/NodeDuplication/TransientNode.php +++ b/Neos.Neos/Classes/Domain/Service/NodeDuplication/TransientNode.php @@ -71,10 +71,14 @@ public function forTetheredChildNode(NodeName $nodeName): self { $nodeAggregateId = $this->tetheredNodeAggregateIds->getNodeAggregateId(NodePath::fromNodeNames($nodeName)); + if (!$nodeAggregateId) { + throw new \InvalidArgumentException(sprintf('Name "%s" doesnt seem to be a point to a tethered node of "%s", could not determine deterministic node aggregate id.', $nodeName->value, $this->aggregateId->value)); + } + $tetheredNodeTypeDefinition = $this->nodeType->tetheredNodeTypeDefinitions->get($nodeName); - if (!$nodeAggregateId || !$tetheredNodeTypeDefinition) { - throw new \InvalidArgumentException('forTetheredChildNode only works for tethered nodes.'); + if (!$tetheredNodeTypeDefinition) { + throw new \InvalidArgumentException(sprintf('Name "%s" doesnt match any tethered node type definition in the schema. Parent node "%s"', $nodeName->value, $this->aggregateId->value)); } $childNodeType = $this->nodeTypeManager->getNodeType($tetheredNodeTypeDefinition->nodeTypeName); @@ -82,25 +86,12 @@ public function forTetheredChildNode(NodeName $nodeName): self throw new \InvalidArgumentException(sprintf('NodeType "%s" for tethered node "%s" does not exist.', $tetheredNodeTypeDefinition->nodeTypeName->value, $nodeName->value), 1718950833); } - $descendantTetheredNodeAggregateIds = NodeAggregateIdsByNodePaths::createEmpty(); - foreach ($this->tetheredNodeAggregateIds->getNodeAggregateIds() as $stringNodePath => $descendantNodeAggregateId) { - $nodePath = NodePath::fromString($stringNodePath); - $pathParts = $nodePath->getParts(); - $firstPart = array_shift($pathParts); - if ($firstPart?->equals($nodeName) && count($pathParts)) { - $descendantTetheredNodeAggregateIds = $descendantTetheredNodeAggregateIds->add( - NodePath::fromNodeNames(...$pathParts), - $descendantNodeAggregateId - ); - } - } - return new self( $nodeAggregateId, $this->workspaceName, $this->originDimensionSpacePoint, $childNodeType, - $descendantTetheredNodeAggregateIds, + NodeAggregateIdsByNodePaths::createEmpty(), $nodeName, $this->nodeType, $this->nodeTypeManager diff --git a/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php b/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php index 684e996b8e4..7b2e4c187f1 100644 --- a/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php +++ b/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php @@ -48,17 +48,56 @@ public function copyNodesRecursively( $subtree = $subgraph->findSubtree($sourceNodeAggregateId, FindSubtreeFilter::create()); $targetParentNode = $subgraph->findNodeById($targetParentNodeAggregateId); + if ($targetParentNode === null) { + // todo simple constraint checks + throw new \RuntimeException('todo'); + } - $entryTransientNode = TransientNode::forRegular( - $targetParentNodeAggregateId, + $transientNode = TransientNode::forRegular( + $nodeAggregateIdMapping->getNewNodeAggregateId($subtree->node->aggregateId) ?? NodeAggregateId::create(), $workspaceName, $targetDimensionSpacePoint, - $targetParentNode->nodeTypeName, + $subtree->node->nodeTypeName, NodeAggregateIdsByNodePaths::createEmpty(), $contentRepository->getNodeTypeManager() ); - $commands = $this->commandsForSubtreeRecursively($entryTransientNode, $subtree, $nodeAggregateIdMapping, Commands::createEmpty()); + $createCopyOfNodeCommand = CreateNodeAggregateWithNode::create( + $workspaceName, + $transientNode->aggregateId, + $subtree->node->nodeTypeName, + $targetDimensionSpacePoint, + $targetParentNodeAggregateId, + succeedingSiblingNodeAggregateId: $targetSucceedingSiblingNodeAggregateId, + // todo skip properties not in schema + initialPropertyValues: PropertyValuesToWrite::fromArray( + iterator_to_array($subtree->node->properties) + ), + // todo references: + ); + + if ($targetNodeName) { + $createCopyOfNodeCommand = $createCopyOfNodeCommand->withNodeName($targetNodeName); + } + + $tetheredDescendantNodeAggregateIds = $this->getTetheredDescendantNodeAggregateIds( + $subtree, + $nodeAggregateIdMapping, + NodePath::forRoot(), + NodeAggregateIdsByNodePaths::createEmpty() + ); + + $createCopyOfNodeCommand = $createCopyOfNodeCommand->withTetheredDescendantNodeAggregateIds( + $tetheredDescendantNodeAggregateIds + ); + + $transientNode = $transientNode->withTetheredNodeAggregateIds($tetheredDescendantNodeAggregateIds); + + $commands = Commands::create($createCopyOfNodeCommand); + + foreach ($subtree->children as $childSubtree) { + $commands = $this->commandsForSubtreeRecursively($transientNode, $childSubtree, $nodeAggregateIdMapping, $commands); + } foreach ($commands as $command) { $contentRepository->handle($command); @@ -72,17 +111,20 @@ private function commandsForSubtreeRecursively(TransientNode $transientParentNod $subtree->node->name ); - $setPropertiesOfTetheredNodeCommand = SetNodeProperties::create( - $transientParentNode->workspaceName, - $transientNode->aggregateId, - $transientParentNode->originDimensionSpacePoint, - PropertyValuesToWrite::fromArray( - iterator_to_array($subtree->node->properties) - ), - ); - // todo references: + if ($subtree->node->properties->count() !== 0) { + $setPropertiesOfTetheredNodeCommand = SetNodeProperties::create( + $transientParentNode->workspaceName, + $transientNode->aggregateId, + $transientParentNode->originDimensionSpacePoint, + PropertyValuesToWrite::fromArray( + iterator_to_array($subtree->node->properties) + ), + ); + // todo references: + + $commands = $commands->append($setPropertiesOfTetheredNodeCommand); + } - $commands = $commands->append($setPropertiesOfTetheredNodeCommand); } else { $transientNode = $transientParentNode->forRegularChildNode( $nodeAggregateIdMapping->getNewNodeAggregateId($subtree->node->aggregateId) ?? NodeAggregateId::create(), @@ -131,20 +173,18 @@ private function commandsForSubtreeRecursively(TransientNode $transientParentNod private function getTetheredDescendantNodeAggregateIds(Subtree $subtree, NodeAggregateIdMapping $nodeAggregateIdMapping, NodePath $nodePath, NodeAggregateIdsByNodePaths $tetheredNodeAggregateIds): NodeAggregateIdsByNodePaths { foreach ($subtree->children as $childSubtree) { - if ($childSubtree->node->classification->isTethered()) { - return $tetheredNodeAggregateIds; + if (!$childSubtree->node->classification->isTethered()) { + continue; } - $deterministicCopyAggregateId = $nodeAggregateIdMapping->getNewNodeAggregateId($childSubtree->node->aggregateId); + $deterministicCopyAggregateId = $nodeAggregateIdMapping->getNewNodeAggregateId($childSubtree->node->aggregateId) ?? NodeAggregateId::create(); - $childNodePath = $nodePath->appendPathSegment($subtree->node->name); + $childNodePath = $nodePath->appendPathSegment($childSubtree->node->name); - if ($deterministicCopyAggregateId) { - $tetheredNodeAggregateIds = $tetheredNodeAggregateIds->add( - $childNodePath, - $deterministicCopyAggregateId - ); - } + $tetheredNodeAggregateIds = $tetheredNodeAggregateIds->add( + $childNodePath, + $deterministicCopyAggregateId + ); $tetheredNodeAggregateIds = $this->getTetheredDescendantNodeAggregateIds($childSubtree, $nodeAggregateIdMapping, $childNodePath, $tetheredNodeAggregateIds); } From 870c35b175a653c07905a808b616f52ee996bf6d Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 18 Nov 2024 11:01:09 +0100 Subject: [PATCH 183/214] BUGFIX: Only leaf tethered nodes can be copied --- .../TetheredNodesCannotBePartiallyCopied.php | 24 +++++++++++++++++++ .../Domain/Service/NodeDuplicationService.php | 6 +++++ .../Bootstrap/NodeDuplicationTrait.php | 22 +++++++++-------- ...ode_ConstraintChecks_TetheredNodes.feature | 5 +++- 4 files changed, 46 insertions(+), 11 deletions(-) create mode 100644 Neos.Neos/Classes/Domain/Exception/TetheredNodesCannotBePartiallyCopied.php diff --git a/Neos.Neos/Classes/Domain/Exception/TetheredNodesCannotBePartiallyCopied.php b/Neos.Neos/Classes/Domain/Exception/TetheredNodesCannotBePartiallyCopied.php new file mode 100644 index 00000000000..a75ae98ad96 --- /dev/null +++ b/Neos.Neos/Classes/Domain/Exception/TetheredNodesCannotBePartiallyCopied.php @@ -0,0 +1,24 @@ +children as $childSubtree) { + if ($subtree->node->classification->isTethered() && $childSubtree->node->classification->isTethered()) { + // TODO we assume here that the child node is tethered because the grandparent specifies that. + // this is not always fully correct and we could loosen the constraint by checking the node type schema + throw new TetheredNodesCannotBePartiallyCopied(sprintf('Cannot copy tethered node %s because child node %s is also tethered. Only standalone tethered nodes can be copied.', $subtree->node->aggregateId->value, $childSubtree->node->aggregateId->value), 1731264887); + } $commands = $this->commandsForSubtreeRecursively($transientNode, $childSubtree, $nodeAggregateIdMapping, $commands); } diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/NodeDuplicationTrait.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/NodeDuplicationTrait.php index ebf7a90856e..d8b43d5b912 100644 --- a/Neos.Neos/Tests/Behavior/Features/Bootstrap/NodeDuplicationTrait.php +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/NodeDuplicationTrait.php @@ -65,16 +65,18 @@ public function copyNodesRecursivelyIsExecutedWithPayload(TableNode $payloadTabl ? NodeAggregateId::fromString($commandArguments['targetSucceedingSiblingNodeAggregateId']) : null; - $this->getObject(NodeDuplicationService::class)->copyNodesRecursively( - $this->currentContentRepository->id, - $workspaceName, - $sourceDimensionSpacePoint, - $sourceNodeAggregateId, - $targetDimensionSpacePoint, - NodeAggregateId::fromString($commandArguments['targetParentNodeAggregateId']), - isset($commandArguments['targetNodeName']) ? NodeName::fromString($commandArguments['targetNodeName']) : null, - $targetSucceedingSiblingNodeAggregateId, - NodeAggregateIdMapping::fromArray($commandArguments['nodeAggregateIdMapping']) + $this->tryCatchingExceptions( + fn () => $this->getObject(NodeDuplicationService::class)->copyNodesRecursively( + $this->currentContentRepository->id, + $workspaceName, + $sourceDimensionSpacePoint, + $sourceNodeAggregateId, + $targetDimensionSpacePoint, + NodeAggregateId::fromString($commandArguments['targetParentNodeAggregateId']), + isset($commandArguments['targetNodeName']) ? NodeName::fromString($commandArguments['targetNodeName']) : null, + $targetSucceedingSiblingNodeAggregateId, + NodeAggregateIdMapping::fromArray($commandArguments['nodeAggregateIdMapping']) + ) ); } } diff --git a/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_ConstraintChecks_TetheredNodes.feature b/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_ConstraintChecks_TetheredNodes.feature index e157210c9d5..83cb9354108 100644 --- a/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_ConstraintChecks_TetheredNodes.feature +++ b/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_ConstraintChecks_TetheredNodes.feature @@ -48,4 +48,7 @@ Feature: Copy nodes (without dimensions) | targetParentNodeAggregateId | "node-mc-nodeface" | | nodeAggregateIdMapping | {"nodewyn-tetherton": "nodewyn-tetherton-copy", "nodimer-tetherton": "nodimer-tetherton-copy"} | - Then the last command should have thrown an exception of type "TetheredNodesCannotBePartiallyCopied" + Then an exception of type TetheredNodesCannotBePartiallyCopied should be thrown with message: + """ + Cannot copy tethered node nodewyn-tetherton because child node nodimer-tetherton is also tethered. Only standalone tethered nodes can be copied. + """ From d8cde95be42f134fee7c9185e468819762df7ee0 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 18 Nov 2024 11:10:15 +0100 Subject: [PATCH 184/214] TASK: Reintroduce reference copying for first node --- .../Domain/Service/NodeDuplicationService.php | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php b/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php index 51b10e86d73..170b2906cc9 100644 --- a/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php +++ b/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php @@ -11,13 +11,19 @@ use Neos\ContentRepository\Core\Feature\NodeDuplication\Dto\NodeAggregateIdMapping; use Neos\ContentRepository\Core\Feature\NodeModification\Command\SetNodeProperties; use Neos\ContentRepository\Core\Feature\NodeModification\Dto\PropertyValuesToWrite; +use Neos\ContentRepository\Core\Feature\NodeReferencing\Dto\NodeReferencesForName; +use Neos\ContentRepository\Core\Feature\NodeReferencing\Dto\NodeReferencesToWrite; +use Neos\ContentRepository\Core\Feature\NodeReferencing\Dto\NodeReferenceToWrite; +use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindReferencesFilter; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindSubtreeFilter; use Neos\ContentRepository\Core\Projection\ContentGraph\NodePath; +use Neos\ContentRepository\Core\Projection\ContentGraph\References; use Neos\ContentRepository\Core\Projection\ContentGraph\Subtree; use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Node\NodeName; +use Neos\ContentRepository\Core\SharedModel\Node\ReferenceName; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Neos\Domain\Exception\TetheredNodesCannotBePartiallyCopied; @@ -39,8 +45,8 @@ public function copyNodesRecursively( NodeAggregateId $sourceNodeAggregateId, OriginDimensionSpacePoint $targetDimensionSpacePoint, NodeAggregateId $targetParentNodeAggregateId, - ?NodeName $targetNodeName, // todo - ?NodeAggregateId $targetSucceedingSiblingNodeAggregateId, // todo + ?NodeName $targetNodeName, + ?NodeAggregateId $targetSucceedingSiblingNodeAggregateId, NodeAggregateIdMapping $nodeAggregateIdMapping ): void { $contentRepository = $this->contentRepositoryRegistry->get($contentRepositoryId); @@ -74,7 +80,9 @@ public function copyNodesRecursively( initialPropertyValues: PropertyValuesToWrite::fromArray( iterator_to_array($subtree->node->properties) ), - // todo references: + references: $this->serializeProjectedReferences( + $subgraph->findReferences($subtree->node->aggregateId, FindReferencesFilter::create()) + ) ); if ($targetNodeName) { @@ -197,4 +205,22 @@ private function getTetheredDescendantNodeAggregateIds(Subtree $subtree, NodeAgg return $tetheredNodeAggregateIds; } + + private function serializeProjectedReferences(References $references): NodeReferencesToWrite + { + $serializedReferencesByName = []; + foreach ($references as $reference) { + if (!isset($serializedReferencesByName[$reference->name->value])) { + $serializedReferencesByName[$reference->name->value] = []; + } + $serializedReferencesByName[$reference->name->value][] = NodeReferenceToWrite::fromTargetAndProperties($reference->node->aggregateId, $reference->properties?->count() > 0 ? PropertyValuesToWrite::fromArray(iterator_to_array($reference->properties)) : PropertyValuesToWrite::createEmpty()); + } + + $serializedReferences = []; + foreach ($serializedReferencesByName as $name => $referenceObjects) { + $serializedReferences[] = NodeReferencesForName::fromReferences(ReferenceName::fromString($name), $referenceObjects); + } + + return NodeReferencesToWrite::fromArray($serializedReferences); + } } From ea753cab1ec7779bfa3684426bb6d80893e1fb5a Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 18 Nov 2024 12:48:58 +0100 Subject: [PATCH 185/214] TASK: Add further testcases for node copy --- .../NodeCopying/CopyNode_NoDimensions.feature | 77 ++++++++++--------- 1 file changed, 42 insertions(+), 35 deletions(-) diff --git a/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_NoDimensions.feature b/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_NoDimensions.feature index 1c76f9b9a04..d92ede57420 100644 --- a/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_NoDimensions.feature +++ b/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_NoDimensions.feature @@ -6,6 +6,15 @@ Feature: Copy nodes (without dimensions) And using the following node types: """yaml 'Neos.ContentRepository.Testing:Document': + properties: + title: + type: string + array: + type: array + uri: + type: GuzzleHttp\Psr7\Uri + date: + type: DateTimeImmutable references: ref: [] """ @@ -20,41 +29,14 @@ Feature: Copy nodes (without dimensions) | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | | nodeTypeName | "Neos.ContentRepository:Root" | - And the event NodeAggregateWithNodeWasCreated was published with payload: - | Key | Value | - | workspaceName | "live" | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "sir-david-nodenborough" | - | nodeTypeName | "Neos.ContentRepository.Testing:Document" | - | originDimensionSpacePoint | {} | - | coveredDimensionSpacePoints | [{}] | - | parentNodeAggregateId | "lady-eleonode-rootford" | - | nodeName | "document" | - | nodeAggregateClassification | "regular" | - And the event NodeAggregateWithNodeWasCreated was published with payload: - | Key | Value | - | workspaceName | "live" | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "nody-mc-nodeface" | - | nodeTypeName | "Neos.ContentRepository.Testing:Document" | - | originDimensionSpacePoint | {} | - | coveredDimensionSpacePoints | [{}] | - | parentNodeAggregateId | "sir-david-nodenborough" | - | nodeName | "child-document" | - | nodeAggregateClassification | "regular" | - And the event NodeAggregateWithNodeWasCreated was published with payload: - | Key | Value | - | workspaceName | "live" | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "sir-nodeward-nodington-iii" | - | nodeTypeName | "Neos.ContentRepository.Testing:Document" | - | originDimensionSpacePoint | {} | - | coveredDimensionSpacePoints | [{}] | - | parentNodeAggregateId | "lady-eleonode-rootford" | - | nodeName | "esquire" | - | nodeAggregateClassification | "regular" | - Scenario: Copy + When the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | parentNodeAggregateId | nodeTypeName | + | sir-david-nodenborough | lady-eleonode-rootford | Neos.ContentRepository.Testing:Document | + | nody-mc-nodeface | sir-david-nodenborough | Neos.ContentRepository.Testing:Document | + | sir-nodeward-nodington-iii | lady-eleonode-rootford | Neos.ContentRepository.Testing:Document | + + Scenario: Simple singular node aggregate is copied When I am in workspace "live" and dimension space point {} When copy nodes recursively is executed with payload: | Key | Value | @@ -76,7 +58,32 @@ Feature: Copy nodes (without dimensions) And I expect this node aggregate to have no child node aggregates And I expect this node aggregate to have the parent node aggregates ["nody-mc-nodeface"] - Scenario: Copy References + Scenario: Singular node aggregate is copied with (complex) properties + When I am in workspace "live" and dimension space point {} + And the command SetNodeProperties is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-nodeward-nodington-iii" | + | propertyValues | {"title": "Original Text", "array": {"givenName":"Nody", "familyName":"McNodeface"}, "uri": {"__type": "GuzzleHttp\\Psr7\\Uri", "value": "https://neos.de"}, "date": {"__type": "DateTimeImmutable", "value": "2001-09-22T12:00:00+00:00"}} | + + When copy nodes recursively is executed with payload: + | Key | Value | + | sourceDimensionSpacePoint | {} | + | sourceNodeAggregateId | "sir-nodeward-nodington-iii" | + | targetDimensionSpacePoint | {} | + | targetParentNodeAggregateId | "nody-mc-nodeface" | + | targetNodeName | "target-nn" | + | targetSucceedingSiblingnodeAggregateId | null | + | nodeAggregateIdMapping | {"sir-nodeward-nodington-iii": "sir-nodeward-nodington-iii-copy"} | + + And I expect node aggregate identifier "sir-nodeward-nodington-iii-copy" to lead to node cs-identifier;sir-nodeward-nodington-iii-copy;{} + And I expect this node to have the following serialized properties: + | Key | Type | Value | + | title | string | "Original Text" | + | array | array | {"givenName":"Nody","familyName":"McNodeface"} | + | date | DateTimeImmutable | "2001-09-22T12:00:00+00:00" | + | uri | GuzzleHttp\Psr7\Uri | "https://neos.de" | + + Scenario: Singular node aggregate is copied with references When I am in workspace "live" and dimension space point {} And the command SetNodeReferences is executed with payload: | Key | Value | From ded2e9d10537f57c05d6e2f0092f8a9587fa30e5 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 18 Nov 2024 12:49:15 +0100 Subject: [PATCH 186/214] TASK: Test and fix recursive node copying --- .../Domain/Service/NodeDuplicationService.php | 4 +-- .../NodeCopying/CopyNode_NoDimensions.feature | 33 +++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php b/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php index 170b2906cc9..64c35fb4542 100644 --- a/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php +++ b/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php @@ -176,9 +176,7 @@ private function commandsForSubtreeRecursively(TransientNode $transientParentNod } foreach ($subtree->children as $childSubtree) { - $commands = $commands->merge( - $this->commandsForSubtreeRecursively($transientNode, $childSubtree, $nodeAggregateIdMapping, $commands) - ); + $commands = $this->commandsForSubtreeRecursively($transientNode, $childSubtree, $nodeAggregateIdMapping, $commands); } return $commands; diff --git a/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_NoDimensions.feature b/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_NoDimensions.feature index d92ede57420..23c2779f80b 100644 --- a/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_NoDimensions.feature +++ b/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_NoDimensions.feature @@ -104,3 +104,36 @@ Feature: Copy nodes (without dimensions) And I expect this node to have the following references: | Name | Node | Properties | | ref | cs-identifier;sir-david-nodenborough;{} | null | + + Scenario: Node aggregate is copied children recursively + When I am in workspace "live" and dimension space point {} + When the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | child-a | sir-nodeward-nodington-iii | Neos.ContentRepository.Testing:Document | {} | + | child-a1 | child-a | Neos.ContentRepository.Testing:Document | {"title": "I am Node A1"} | + | child-a2 | child-a | Neos.ContentRepository.Testing:Document | {} | + | child-b | sir-nodeward-nodington-iii | Neos.ContentRepository.Testing:Document | {} | + + When copy nodes recursively is executed with payload: + | Key | Value | + | sourceDimensionSpacePoint | {} | + | sourceNodeAggregateId | "sir-nodeward-nodington-iii" | + | targetDimensionSpacePoint | {} | + | targetParentNodeAggregateId | "nody-mc-nodeface" | + | targetNodeName | "target-nn" | + | targetSucceedingSiblingnodeAggregateId | null | + | nodeAggregateIdMapping | {"sir-nodeward-nodington-iii": "sir-nodeward-nodington-iii-copy", "child-a": "child-a-copy", "child-b": "child-b-copy", "child-a1": "child-a1-copy", "child-a2": "child-a2-copy"} | + + And I expect the node aggregate "sir-nodeward-nodington-iii-copy" to exist + And I expect this node aggregate to have the child node aggregates ["child-a-copy","child-b-copy"] + + And I expect the node aggregate "child-a-copy" to exist + And I expect this node aggregate to have the child node aggregates ["child-a1-copy","child-a2-copy"] + + And I expect the node aggregate "child-b-copy" to exist + And I expect this node aggregate to have no child node aggregates + + And I expect node aggregate identifier "child-a1-copy" to lead to node cs-identifier;child-a1-copy;{} + And I expect this node to have the following serialized properties: + | Key | Type | Value | + | title | string | "I am Node A1" | From 7d39367e1a77ac3b725fc1941b6e0a758d358393 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 18 Nov 2024 12:56:23 +0100 Subject: [PATCH 187/214] TASK: Add test for copying tethered nodes deep in the tree --- .../CopyNode_TetheredNodes.feature | 42 ++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_TetheredNodes.feature b/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_TetheredNodes.feature index f1298eda85c..1d996b80e60 100644 --- a/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_TetheredNodes.feature +++ b/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_TetheredNodes.feature @@ -27,7 +27,8 @@ Feature: Copy nodes with tethered nodes | nodeAggregateId | parentNodeAggregateId | nodeTypeName | tetheredDescendantNodeAggregateIds | | sir-david-nodenborough | lady-eleonode-rootford | Neos.ContentRepository.Testing:Document | {} | | nody-mc-nodeface | sir-david-nodenborough | Neos.ContentRepository.Testing:Document | | - | sir-nodeward-nodington-i | lady-eleonode-rootford | Neos.ContentRepository.Testing:DocumentWithTethered | {"tethered": "nodewyn-tetherton"} | + | nodimus-primus | lady-eleonode-rootford | Neos.ContentRepository.Testing:Document | | + | sir-nodeward-nodington-i | nodimus-primus | Neos.ContentRepository.Testing:DocumentWithTethered | {"tethered": "nodewyn-tetherton"} | Scenario: Coping a tethered node turns it into a regular node And I expect the node aggregate "nodewyn-tetherton" to exist @@ -80,3 +81,42 @@ Feature: Copy nodes with tethered nodes And I expect this node aggregate to disable dimension space points [] And I expect this node aggregate to have no child node aggregates And I expect this node aggregate to have the parent node aggregates ["sir-nodeward-nodington-ii"] + + Scenario: Coping a regular node with a child node that has a tethered child node + And I expect the node aggregate "nodewyn-tetherton" to exist + And I expect this node aggregate to be classified as "tethered" + + When copy nodes recursively is executed with payload: + | Key | Value | + | sourceDimensionSpacePoint | {} | + | sourceNodeAggregateId | "nodimus-primus" | + | targetDimensionSpacePoint | {} | + | targetParentNodeAggregateId | "sir-david-nodenborough" | + | nodeAggregateIdMapping | {"nodimus-primus": "nodimus-primus-copy", "sir-nodeward-nodington-i": "sir-nodeward-nodington-i-copy", "nodewyn-tetherton": "nodewyn-tetherton-copy"} | + + And I expect the node aggregate "nodimus-primus-copy" to exist + And I expect this node aggregate to be classified as "regular" + And I expect this node aggregate to be unnamed + And I expect this node aggregate to be of type "Neos.ContentRepository.Testing:Document" + And I expect this node aggregate to occupy dimension space points [[]] + And I expect this node aggregate to disable dimension space points [] + And I expect this node aggregate to have the child node aggregates ["sir-nodeward-nodington-i-copy"] + And I expect this node aggregate to have the parent node aggregates ["sir-david-nodenborough"] + + And I expect the node aggregate "sir-nodeward-nodington-i-copy" to exist + And I expect this node aggregate to be classified as "regular" + And I expect this node aggregate to be unnamed + And I expect this node aggregate to be of type "Neos.ContentRepository.Testing:DocumentWithTethered" + And I expect this node aggregate to occupy dimension space points [[]] + And I expect this node aggregate to disable dimension space points [] + And I expect this node aggregate to have the child node aggregates ["nodewyn-tetherton-copy"] + And I expect this node aggregate to have the parent node aggregates ["nodimus-primus-copy"] + + And I expect the node aggregate "nodewyn-tetherton-copy" to exist + And I expect this node aggregate to be classified as "tethered" + And I expect this node aggregate to be named "tethered" + And I expect this node aggregate to be of type "Neos.ContentRepository.Testing:Document" + And I expect this node aggregate to occupy dimension space points [[]] + And I expect this node aggregate to disable dimension space points [] + And I expect this node aggregate to have no child node aggregates + And I expect this node aggregate to have the parent node aggregates ["sir-nodeward-nodington-i-copy"] From 05fb8690b9798f2d83c543b910533f8a8b5e2396 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 18 Nov 2024 13:50:47 +0100 Subject: [PATCH 188/214] TASK: Add failing test for copying nested references --- .../NodeCopying/CopyNode_NoDimensions.feature | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_NoDimensions.feature b/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_NoDimensions.feature index 23c2779f80b..ef320966052 100644 --- a/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_NoDimensions.feature +++ b/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_NoDimensions.feature @@ -120,8 +120,6 @@ Feature: Copy nodes (without dimensions) | sourceNodeAggregateId | "sir-nodeward-nodington-iii" | | targetDimensionSpacePoint | {} | | targetParentNodeAggregateId | "nody-mc-nodeface" | - | targetNodeName | "target-nn" | - | targetSucceedingSiblingnodeAggregateId | null | | nodeAggregateIdMapping | {"sir-nodeward-nodington-iii": "sir-nodeward-nodington-iii-copy", "child-a": "child-a-copy", "child-b": "child-b-copy", "child-a1": "child-a1-copy", "child-a2": "child-a2-copy"} | And I expect the node aggregate "sir-nodeward-nodington-iii-copy" to exist @@ -137,3 +135,22 @@ Feature: Copy nodes (without dimensions) And I expect this node to have the following serialized properties: | Key | Type | Value | | title | string | "I am Node A1" | + + Scenario: References are copied for child nodes + When I am in workspace "live" and dimension space point {} + When the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | parentNodeAggregateId | nodeTypeName | references | + | child-a | sir-nodeward-nodington-iii | Neos.ContentRepository.Testing:Document | [{"referenceName": "ref", "references": [{"target": "sir-david-nodenborough"}]}] | + + When copy nodes recursively is executed with payload: + | Key | Value | + | sourceDimensionSpacePoint | {} | + | sourceNodeAggregateId | "sir-nodeward-nodington-iii" | + | targetDimensionSpacePoint | {} | + | targetParentNodeAggregateId | "nody-mc-nodeface" | + | nodeAggregateIdMapping | {"sir-nodeward-nodington-iii": "sir-nodeward-nodington-iii-copy", "child-a": "child-a-copy"} | + + And I expect node aggregate identifier "child-a-copy" to lead to node cs-identifier;child-a-copy;{} + And I expect this node to have the following references: + | Name | Node | Properties | + | ref | cs-identifier;sir-david-nodenborough;{} | null | From d7c70c36b6b0a7bedbfe07e52bd738e331142744 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 18 Nov 2024 14:35:20 +0100 Subject: [PATCH 189/214] TASK: Add failing test for copying nested tethered nodes --- .../CopyNode_TetheredNodes.feature | 62 +++++++++++++++---- 1 file changed, 51 insertions(+), 11 deletions(-) diff --git a/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_TetheredNodes.feature b/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_TetheredNodes.feature index 1d996b80e60..c098b3bff99 100644 --- a/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_TetheredNodes.feature +++ b/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_TetheredNodes.feature @@ -4,11 +4,16 @@ Feature: Copy nodes with tethered nodes Given using no content dimensions And using the following node types: """yaml - 'Neos.ContentRepository.Testing:Document': [] + 'Neos.ContentRepository.Testing:Tethered': [] 'Neos.ContentRepository.Testing:DocumentWithTethered': childNodes: tethered: - type: 'Neos.ContentRepository.Testing:Document' + type: 'Neos.ContentRepository.Testing:Tethered' + 'Neos.ContentRepository.Testing:RootDocument': + childNodes: + tethered-document: + type: 'Neos.ContentRepository.Testing:DocumentWithTethered' + 'Neos.ContentRepository.Testing:DocumentWithoutTetheredChildren': [] """ And using identifier "default", I define a content repository And I am in content repository "default" @@ -24,11 +29,12 @@ Feature: Copy nodes with tethered nodes | nodeTypeName | "Neos.ContentRepository:Root" | When the following CreateNodeAggregateWithNode commands are executed: - | nodeAggregateId | parentNodeAggregateId | nodeTypeName | tetheredDescendantNodeAggregateIds | - | sir-david-nodenborough | lady-eleonode-rootford | Neos.ContentRepository.Testing:Document | {} | - | nody-mc-nodeface | sir-david-nodenborough | Neos.ContentRepository.Testing:Document | | - | nodimus-primus | lady-eleonode-rootford | Neos.ContentRepository.Testing:Document | | - | sir-nodeward-nodington-i | nodimus-primus | Neos.ContentRepository.Testing:DocumentWithTethered | {"tethered": "nodewyn-tetherton"} | + | nodeAggregateId | parentNodeAggregateId | nodeTypeName | tetheredDescendantNodeAggregateIds | + | sir-david-nodenborough | lady-eleonode-rootford | Neos.ContentRepository.Testing:DocumentWithoutTetheredChildren | {} | + | nody-mc-nodeface | sir-david-nodenborough | Neos.ContentRepository.Testing:DocumentWithoutTetheredChildren | | + | nodimus-primus | lady-eleonode-rootford | Neos.ContentRepository.Testing:DocumentWithoutTetheredChildren | | + | sir-nodeward-nodington-i | nodimus-primus | Neos.ContentRepository.Testing:DocumentWithTethered | {"tethered": "nodewyn-tetherton"} | + | node-wan-kenodi | lady-eleonode-rootford | Neos.ContentRepository.Testing:RootDocument | {"tethered-document": "tethered-document", "tethered-document/tethered": "tethered-document-child"} | Scenario: Coping a tethered node turns it into a regular node And I expect the node aggregate "nodewyn-tetherton" to exist @@ -45,7 +51,7 @@ Feature: Copy nodes with tethered nodes And I expect the node aggregate "nodewyn-tetherton-copy" to exist # must not be tethered! And I expect this node aggregate to be classified as "regular" - And I expect this node aggregate to be of type "Neos.ContentRepository.Testing:Document" + And I expect this node aggregate to be of type "Neos.ContentRepository.Testing:Tethered" And I expect this node aggregate to be unnamed And I expect this node aggregate to occupy dimension space points [[]] And I expect this node aggregate to disable dimension space points [] @@ -76,12 +82,46 @@ Feature: Copy nodes with tethered nodes And I expect the node aggregate "nodewyn-tetherton-copy" to exist And I expect this node aggregate to be classified as "tethered" And I expect this node aggregate to be named "tethered" - And I expect this node aggregate to be of type "Neos.ContentRepository.Testing:Document" + And I expect this node aggregate to be of type "Neos.ContentRepository.Testing:Tethered" And I expect this node aggregate to occupy dimension space points [[]] And I expect this node aggregate to disable dimension space points [] And I expect this node aggregate to have no child node aggregates And I expect this node aggregate to have the parent node aggregates ["sir-nodeward-nodington-ii"] + Scenario: Coping a node with nested tethered nodes keeps the child nodes tethered + When copy nodes recursively is executed with payload: + | Key | Value | + | sourceDimensionSpacePoint | {} | + | sourceNodeAggregateId | "node-wan-kenodi" | + | targetDimensionSpacePoint | {} | + | targetParentNodeAggregateId | "sir-david-nodenborough" | + | nodeAggregateIdMapping | {"node-wan-kenodi": "node-wan-kenodi-copy", "tethered-document": "tethered-document-copy", "tethered-document-child": "tethered-document-child-copy"} | + + And I expect the node aggregate "node-wan-kenodi-copy" to exist + And I expect this node aggregate to be classified as "regular" + And I expect this node aggregate to be unnamed + And I expect this node aggregate to be of type "Neos.ContentRepository.Testing:RootDocument" + And I expect this node aggregate to occupy dimension space points [[]] + And I expect this node aggregate to disable dimension space points [] + And I expect this node aggregate to have the child node aggregates ["tethered-document"] + And I expect this node aggregate to have the parent node aggregates ["sir-david-nodenborough"] + + And I expect the node aggregate "tethered-document-copy" to exist + And I expect this node aggregate to be classified as "tethered" + And I expect this node aggregate to be named "tethered-document" + And I expect this node aggregate to be of type "Neos.ContentRepository.Testing:TetheredWithTethered" + And I expect this node aggregate to occupy dimension space points [[]] + And I expect this node aggregate to disable dimension space points [] + And I expect this node aggregate to have the parent node aggregates ["tethered-document-child-copy-copy"] + + And I expect the node aggregate "tethered-document-child-copy-copy" to exist + And I expect this node aggregate to be classified as "tethered" + And I expect this node aggregate to be named "tethered" + And I expect this node aggregate to be of type "Neos.ContentRepository.Testing:Tethered" + And I expect this node aggregate to occupy dimension space points [[]] + And I expect this node aggregate to disable dimension space points [] + And I expect this node aggregate to have no child node aggregates + Scenario: Coping a regular node with a child node that has a tethered child node And I expect the node aggregate "nodewyn-tetherton" to exist And I expect this node aggregate to be classified as "tethered" @@ -97,7 +137,7 @@ Feature: Copy nodes with tethered nodes And I expect the node aggregate "nodimus-primus-copy" to exist And I expect this node aggregate to be classified as "regular" And I expect this node aggregate to be unnamed - And I expect this node aggregate to be of type "Neos.ContentRepository.Testing:Document" + And I expect this node aggregate to be of type "Neos.ContentRepository.Testing:DocumentWithoutTetheredChildren" And I expect this node aggregate to occupy dimension space points [[]] And I expect this node aggregate to disable dimension space points [] And I expect this node aggregate to have the child node aggregates ["sir-nodeward-nodington-i-copy"] @@ -115,7 +155,7 @@ Feature: Copy nodes with tethered nodes And I expect the node aggregate "nodewyn-tetherton-copy" to exist And I expect this node aggregate to be classified as "tethered" And I expect this node aggregate to be named "tethered" - And I expect this node aggregate to be of type "Neos.ContentRepository.Testing:Document" + And I expect this node aggregate to be of type "Neos.ContentRepository.Testing:Tethered" And I expect this node aggregate to occupy dimension space points [[]] And I expect this node aggregate to disable dimension space points [] And I expect this node aggregate to have no child node aggregates From f61ab5ebbb7fc9cfe5a3bc93fe7b53c6e7f83b94 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 18 Nov 2024 14:54:06 +0100 Subject: [PATCH 190/214] BUGFIX: Fix copying nested tethered nodes --- ...ransientNode.php => TransientNodeCopy.php} | 97 +++++++++++++------ .../Domain/Service/NodeDuplicationService.php | 68 +++---------- .../CopyNode_TetheredNodes.feature | 7 +- 3 files changed, 82 insertions(+), 90 deletions(-) rename Neos.Neos/Classes/Domain/Service/NodeDuplication/{TransientNode.php => TransientNodeCopy.php} (59%) diff --git a/Neos.Neos/Classes/Domain/Service/NodeDuplication/TransientNode.php b/Neos.Neos/Classes/Domain/Service/NodeDuplication/TransientNodeCopy.php similarity index 59% rename from Neos.Neos/Classes/Domain/Service/NodeDuplication/TransientNode.php rename to Neos.Neos/Classes/Domain/Service/NodeDuplication/TransientNodeCopy.php index 05a4df80ef8..a287c179ab0 100644 --- a/Neos.Neos/Classes/Domain/Service/NodeDuplication/TransientNode.php +++ b/Neos.Neos/Classes/Domain/Service/NodeDuplication/TransientNodeCopy.php @@ -5,10 +5,12 @@ use Flowpack\NodeTemplates\Domain\NodeCreation\NodeConstraintException; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; use Neos\ContentRepository\Core\Feature\NodeCreation\Dto\NodeAggregateIdsByNodePaths; +use Neos\ContentRepository\Core\Feature\NodeDuplication\Dto\NodeAggregateIdMapping; use Neos\ContentRepository\Core\NodeType\NodeType; use Neos\ContentRepository\Core\NodeType\NodeTypeManager; use Neos\ContentRepository\Core\NodeType\NodeTypeName; use Neos\ContentRepository\Core\Projection\ContentGraph\NodePath; +use Neos\ContentRepository\Core\Projection\ContentGraph\Subtree; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Node\NodeName; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; @@ -29,14 +31,21 @@ * * @Flow\Proxy(false) */ -final readonly class TransientNode +final readonly class TransientNodeCopy { private function __construct( public NodeAggregateId $aggregateId, public WorkspaceName $workspaceName, public OriginDimensionSpacePoint $originDimensionSpacePoint, private NodeType $nodeType, - private NodeAggregateIdsByNodePaths $tetheredNodeAggregateIds, + /** + * @var NodeAggregateIdMapping An assignment of "old" to "new" NodeAggregateIds + */ + private NodeAggregateIdMapping $nodeAggregateIdMapping, + /** + * @var NodeAggregateIdsByNodePaths Deterministic NodeAggregate ids for the creation of tethered nodes (empty if there are no tethered nodes) + */ + public NodeAggregateIdsByNodePaths $tetheredNodeAggregateIds, private ?NodeName $tetheredNodeName, private ?NodeType $tetheredParentNodeType, private NodeTypeManager $nodeTypeManager @@ -47,28 +56,29 @@ private function __construct( } public static function forRegular( - NodeAggregateId $nodeAggregateId, - WorkspaceName $workspaceName, - OriginDimensionSpacePoint $originDimensionSpacePoint, - NodeTypeName $nodeTypeName, - NodeAggregateIdsByNodePaths $tetheredNodeAggregateIds, + Subtree $subtree, + WorkspaceName $targetWorkspaceName, + OriginDimensionSpacePoint $targetOriginDimensionSpacePoint, + NodeAggregateIdMapping $nodeAggregateIdMapping, NodeTypeManager $nodeTypeManager ): self { - $nodeType = $nodeTypeManager->getNodeType($nodeTypeName); + $nodeType = $nodeTypeManager->getNodeType($subtree->node->nodeTypeName); return new self( - $nodeAggregateId, - $workspaceName, - $originDimensionSpacePoint, + $nodeAggregateIdMapping->getNewNodeAggregateId($subtree->node->aggregateId) ?? NodeAggregateId::create(), + $targetWorkspaceName, + $targetOriginDimensionSpacePoint, $nodeType, - $tetheredNodeAggregateIds, + $nodeAggregateIdMapping, + self::getTetheredDescendantNodeAggregateIds($subtree->children, $nodeAggregateIdMapping, NodePath::forRoot(), NodeAggregateIdsByNodePaths::createEmpty()), null, null, $nodeTypeManager ); } - public function forTetheredChildNode(NodeName $nodeName): self + public function forTetheredChildNode(Subtree $subtree): self { + $nodeName = $subtree->node->name; $nodeAggregateId = $this->tetheredNodeAggregateIds->getNodeAggregateId(NodePath::fromNodeNames($nodeName)); if (!$nodeAggregateId) { @@ -86,46 +96,73 @@ public function forTetheredChildNode(NodeName $nodeName): self throw new \InvalidArgumentException(sprintf('NodeType "%s" for tethered node "%s" does not exist.', $tetheredNodeTypeDefinition->nodeTypeName->value, $nodeName->value), 1718950833); } + // keep tethered node aggregate ids from parent + $descendantTetheredNodeAggregateIds = NodeAggregateIdsByNodePaths::createEmpty(); + foreach ($this->tetheredNodeAggregateIds->getNodeAggregateIds() as $stringNodePath => $descendantNodeAggregateId) { + $nodePath = NodePath::fromString($stringNodePath); + $pathParts = $nodePath->getParts(); + $firstPart = array_shift($pathParts); + if ($firstPart?->equals($nodeName) && count($pathParts)) { + $descendantTetheredNodeAggregateIds = $descendantTetheredNodeAggregateIds->add( + NodePath::fromNodeNames(...$pathParts), + $descendantNodeAggregateId + ); + } + } + + return new self( $nodeAggregateId, $this->workspaceName, $this->originDimensionSpacePoint, $childNodeType, - NodeAggregateIdsByNodePaths::createEmpty(), + $this->nodeAggregateIdMapping, + self::getTetheredDescendantNodeAggregateIds($subtree->children, $this->nodeAggregateIdMapping, NodePath::forRoot(), $descendantTetheredNodeAggregateIds), $nodeName, $this->nodeType, $this->nodeTypeManager ); } - public function forRegularChildNode(NodeAggregateId $nodeAggregateId, NodeTypeName $nodeTypeName): self + public function forRegularChildNode(Subtree $subtree): self { - $nodeType = $this->nodeTypeManager->getNodeType($nodeTypeName); - $tetheredNodeAggregateIds = NodeAggregateIdsByNodePaths::createForNodeType($nodeTypeName, $this->nodeTypeManager); + $nodeType = $this->nodeTypeManager->getNodeType($subtree->node->nodeTypeName); return new self( - $nodeAggregateId, + $this->nodeAggregateIdMapping->getNewNodeAggregateId($subtree->node->aggregateId) ?? NodeAggregateId::create(), $this->workspaceName, $this->originDimensionSpacePoint, $nodeType, - $tetheredNodeAggregateIds, + $this->nodeAggregateIdMapping, + self::getTetheredDescendantNodeAggregateIds($subtree->children, $this->nodeAggregateIdMapping, NodePath::forRoot(), NodeAggregateIdsByNodePaths::createEmpty()), null, null, $this->nodeTypeManager ); } - public function withTetheredNodeAggregateIds(NodeAggregateIdsByNodePaths $nodeAggregateIdsByNodePaths): self + /** + * @param array $subtreeChildren + */ + private static function getTetheredDescendantNodeAggregateIds(array $subtreeChildren, NodeAggregateIdMapping $nodeAggregateIdMapping, NodePath $nodePath, NodeAggregateIdsByNodePaths $tetheredNodeAggregateIds): NodeAggregateIdsByNodePaths { - return new self( - $this->aggregateId, - $this->workspaceName, - $this->originDimensionSpacePoint, - $this->nodeType, - $nodeAggregateIdsByNodePaths, - $this->tetheredNodeName, - $this->tetheredParentNodeType, - $this->nodeTypeManager - ); + foreach ($subtreeChildren as $childSubtree) { + if (!$childSubtree->node->classification->isTethered()) { + continue; + } + + $deterministicCopyAggregateId = $nodeAggregateIdMapping->getNewNodeAggregateId($childSubtree->node->aggregateId) ?? NodeAggregateId::create(); + + $childNodePath = $nodePath->appendPathSegment($childSubtree->node->name); + + $tetheredNodeAggregateIds = $tetheredNodeAggregateIds->add( + $childNodePath, + $deterministicCopyAggregateId + ); + + $tetheredNodeAggregateIds = self::getTetheredDescendantNodeAggregateIds($childSubtree->children, $nodeAggregateIdMapping, $childNodePath, $tetheredNodeAggregateIds); + } + + return $tetheredNodeAggregateIds; } /** diff --git a/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php b/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php index 64c35fb4542..085feb59ede 100644 --- a/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php +++ b/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php @@ -7,7 +7,6 @@ use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; use Neos\ContentRepository\Core\Feature\NodeCreation\Command\CreateNodeAggregateWithNode; -use Neos\ContentRepository\Core\Feature\NodeCreation\Dto\NodeAggregateIdsByNodePaths; use Neos\ContentRepository\Core\Feature\NodeDuplication\Dto\NodeAggregateIdMapping; use Neos\ContentRepository\Core\Feature\NodeModification\Command\SetNodeProperties; use Neos\ContentRepository\Core\Feature\NodeModification\Dto\PropertyValuesToWrite; @@ -16,7 +15,6 @@ use Neos\ContentRepository\Core\Feature\NodeReferencing\Dto\NodeReferenceToWrite; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindReferencesFilter; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindSubtreeFilter; -use Neos\ContentRepository\Core\Projection\ContentGraph\NodePath; use Neos\ContentRepository\Core\Projection\ContentGraph\References; use Neos\ContentRepository\Core\Projection\ContentGraph\Subtree; use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; @@ -28,7 +26,7 @@ use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Neos\Domain\Exception\TetheredNodesCannotBePartiallyCopied; use Neos\Neos\Domain\Service\NodeDuplication\Commands; -use Neos\Neos\Domain\Service\NodeDuplication\TransientNode; +use Neos\Neos\Domain\Service\NodeDuplication\TransientNodeCopy; final class NodeDuplicationService { @@ -60,18 +58,17 @@ public function copyNodesRecursively( throw new \RuntimeException('todo'); } - $transientNode = TransientNode::forRegular( - $nodeAggregateIdMapping->getNewNodeAggregateId($subtree->node->aggregateId) ?? NodeAggregateId::create(), + $transientNodeCopy = TransientNodeCopy::forRegular( + $subtree, $workspaceName, $targetDimensionSpacePoint, - $subtree->node->nodeTypeName, - NodeAggregateIdsByNodePaths::createEmpty(), + $nodeAggregateIdMapping, $contentRepository->getNodeTypeManager() ); $createCopyOfNodeCommand = CreateNodeAggregateWithNode::create( $workspaceName, - $transientNode->aggregateId, + $transientNodeCopy->aggregateId, $subtree->node->nodeTypeName, $targetDimensionSpacePoint, $targetParentNodeAggregateId, @@ -89,19 +86,10 @@ public function copyNodesRecursively( $createCopyOfNodeCommand = $createCopyOfNodeCommand->withNodeName($targetNodeName); } - $tetheredDescendantNodeAggregateIds = $this->getTetheredDescendantNodeAggregateIds( - $subtree, - $nodeAggregateIdMapping, - NodePath::forRoot(), - NodeAggregateIdsByNodePaths::createEmpty() - ); - $createCopyOfNodeCommand = $createCopyOfNodeCommand->withTetheredDescendantNodeAggregateIds( - $tetheredDescendantNodeAggregateIds + $transientNodeCopy->tetheredNodeAggregateIds ); - $transientNode = $transientNode->withTetheredNodeAggregateIds($tetheredDescendantNodeAggregateIds); - $commands = Commands::create($createCopyOfNodeCommand); foreach ($subtree->children as $childSubtree) { @@ -110,7 +98,7 @@ public function copyNodesRecursively( // this is not always fully correct and we could loosen the constraint by checking the node type schema throw new TetheredNodesCannotBePartiallyCopied(sprintf('Cannot copy tethered node %s because child node %s is also tethered. Only standalone tethered nodes can be copied.', $subtree->node->aggregateId->value, $childSubtree->node->aggregateId->value), 1731264887); } - $commands = $this->commandsForSubtreeRecursively($transientNode, $childSubtree, $nodeAggregateIdMapping, $commands); + $commands = $this->commandsForSubtreeRecursively($transientNodeCopy, $childSubtree, $commands); } foreach ($commands as $command) { @@ -118,11 +106,11 @@ public function copyNodesRecursively( } } - private function commandsForSubtreeRecursively(TransientNode $transientParentNode, Subtree $subtree, NodeAggregateIdMapping $nodeAggregateIdMapping, Commands $commands): Commands + private function commandsForSubtreeRecursively(TransientNodeCopy $transientParentNode, Subtree $subtree, Commands $commands): Commands { if ($subtree->node->classification->isTethered()) { $transientNode = $transientParentNode->forTetheredChildNode( - $subtree->node->name + $subtree ); if ($subtree->node->properties->count() !== 0) { @@ -141,8 +129,7 @@ private function commandsForSubtreeRecursively(TransientNode $transientParentNod } else { $transientNode = $transientParentNode->forRegularChildNode( - $nodeAggregateIdMapping->getNewNodeAggregateId($subtree->node->aggregateId) ?? NodeAggregateId::create(), - $subtree->node->nodeTypeName + $subtree ); $createCopyOfNodeCommand = CreateNodeAggregateWithNode::create( @@ -159,51 +146,20 @@ private function commandsForSubtreeRecursively(TransientNode $transientParentNod // todo references: ); - $tetheredDescendantNodeAggregateIds = $this->getTetheredDescendantNodeAggregateIds( - $subtree, - $nodeAggregateIdMapping, - NodePath::forRoot(), - NodeAggregateIdsByNodePaths::createEmpty() - ); - $createCopyOfNodeCommand = $createCopyOfNodeCommand->withTetheredDescendantNodeAggregateIds( - $tetheredDescendantNodeAggregateIds + $transientNode->tetheredNodeAggregateIds ); - $transientNode = $transientNode->withTetheredNodeAggregateIds($tetheredDescendantNodeAggregateIds); - $commands = $commands->append($createCopyOfNodeCommand); } foreach ($subtree->children as $childSubtree) { - $commands = $this->commandsForSubtreeRecursively($transientNode, $childSubtree, $nodeAggregateIdMapping, $commands); + $commands = $this->commandsForSubtreeRecursively($transientNode, $childSubtree, $commands); } return $commands; } - private function getTetheredDescendantNodeAggregateIds(Subtree $subtree, NodeAggregateIdMapping $nodeAggregateIdMapping, NodePath $nodePath, NodeAggregateIdsByNodePaths $tetheredNodeAggregateIds): NodeAggregateIdsByNodePaths - { - foreach ($subtree->children as $childSubtree) { - if (!$childSubtree->node->classification->isTethered()) { - continue; - } - - $deterministicCopyAggregateId = $nodeAggregateIdMapping->getNewNodeAggregateId($childSubtree->node->aggregateId) ?? NodeAggregateId::create(); - - $childNodePath = $nodePath->appendPathSegment($childSubtree->node->name); - - $tetheredNodeAggregateIds = $tetheredNodeAggregateIds->add( - $childNodePath, - $deterministicCopyAggregateId - ); - - $tetheredNodeAggregateIds = $this->getTetheredDescendantNodeAggregateIds($childSubtree, $nodeAggregateIdMapping, $childNodePath, $tetheredNodeAggregateIds); - } - - return $tetheredNodeAggregateIds; - } - private function serializeProjectedReferences(References $references): NodeReferencesToWrite { $serializedReferencesByName = []; diff --git a/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_TetheredNodes.feature b/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_TetheredNodes.feature index c098b3bff99..a4881bcc6e2 100644 --- a/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_TetheredNodes.feature +++ b/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_TetheredNodes.feature @@ -103,18 +103,17 @@ Feature: Copy nodes with tethered nodes And I expect this node aggregate to be of type "Neos.ContentRepository.Testing:RootDocument" And I expect this node aggregate to occupy dimension space points [[]] And I expect this node aggregate to disable dimension space points [] - And I expect this node aggregate to have the child node aggregates ["tethered-document"] + And I expect this node aggregate to have the child node aggregates ["tethered-document-copy"] And I expect this node aggregate to have the parent node aggregates ["sir-david-nodenborough"] And I expect the node aggregate "tethered-document-copy" to exist And I expect this node aggregate to be classified as "tethered" And I expect this node aggregate to be named "tethered-document" - And I expect this node aggregate to be of type "Neos.ContentRepository.Testing:TetheredWithTethered" + And I expect this node aggregate to be of type "Neos.ContentRepository.Testing:DocumentWithTethered" And I expect this node aggregate to occupy dimension space points [[]] And I expect this node aggregate to disable dimension space points [] - And I expect this node aggregate to have the parent node aggregates ["tethered-document-child-copy-copy"] - And I expect the node aggregate "tethered-document-child-copy-copy" to exist + And I expect the node aggregate "tethered-document-child-copy" to exist And I expect this node aggregate to be classified as "tethered" And I expect this node aggregate to be named "tethered" And I expect this node aggregate to be of type "Neos.ContentRepository.Testing:Tethered" From 4abe0782be38582d75b0f822377115984c30b36a Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 18 Nov 2024 15:09:10 +0100 Subject: [PATCH 191/214] BUGFIX: Copy references for child nodes on copy --- .../Domain/Service/NodeDuplicationService.php | 28 +++++++++++---- .../CopyNode_TetheredNodes.feature | 34 ++++++++++++++++++- 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php b/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php index 085feb59ede..f54b6ef2114 100644 --- a/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php +++ b/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php @@ -10,9 +10,11 @@ use Neos\ContentRepository\Core\Feature\NodeDuplication\Dto\NodeAggregateIdMapping; use Neos\ContentRepository\Core\Feature\NodeModification\Command\SetNodeProperties; use Neos\ContentRepository\Core\Feature\NodeModification\Dto\PropertyValuesToWrite; +use Neos\ContentRepository\Core\Feature\NodeReferencing\Command\SetNodeReferences; use Neos\ContentRepository\Core\Feature\NodeReferencing\Dto\NodeReferencesForName; use Neos\ContentRepository\Core\Feature\NodeReferencing\Dto\NodeReferencesToWrite; use Neos\ContentRepository\Core\Feature\NodeReferencing\Dto\NodeReferenceToWrite; +use Neos\ContentRepository\Core\Projection\ContentGraph\ContentSubgraphInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindReferencesFilter; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindSubtreeFilter; use Neos\ContentRepository\Core\Projection\ContentGraph\References; @@ -98,7 +100,7 @@ public function copyNodesRecursively( // this is not always fully correct and we could loosen the constraint by checking the node type schema throw new TetheredNodesCannotBePartiallyCopied(sprintf('Cannot copy tethered node %s because child node %s is also tethered. Only standalone tethered nodes can be copied.', $subtree->node->aggregateId->value, $childSubtree->node->aggregateId->value), 1731264887); } - $commands = $this->commandsForSubtreeRecursively($transientNodeCopy, $childSubtree, $commands); + $commands = $this->commandsForSubtreeRecursively($transientNodeCopy, $childSubtree, $subgraph, $commands); } foreach ($commands as $command) { @@ -106,14 +108,14 @@ public function copyNodesRecursively( } } - private function commandsForSubtreeRecursively(TransientNodeCopy $transientParentNode, Subtree $subtree, Commands $commands): Commands + private function commandsForSubtreeRecursively(TransientNodeCopy $transientParentNode, Subtree $subtree, ContentSubgraphInterface $subgraph, Commands $commands): Commands { if ($subtree->node->classification->isTethered()) { $transientNode = $transientParentNode->forTetheredChildNode( $subtree ); - if ($subtree->node->properties->count() !== 0) { + if ($subtree->node->properties->count() > 0) { $setPropertiesOfTetheredNodeCommand = SetNodeProperties::create( $transientParentNode->workspaceName, $transientNode->aggregateId, @@ -122,11 +124,23 @@ private function commandsForSubtreeRecursively(TransientNodeCopy $transientParen iterator_to_array($subtree->node->properties) ), ); - // todo references: $commands = $commands->append($setPropertiesOfTetheredNodeCommand); } + $references = $subgraph->findReferences($subtree->node->aggregateId, FindReferencesFilter::create()); + if ($references->count() > 0) { + $setReferencesOfTetheredNodeCommand = SetNodeReferences::create( + $transientParentNode->workspaceName, + $transientNode->aggregateId, + $transientParentNode->originDimensionSpacePoint, + $this->serializeProjectedReferences( + $references + ), + ); + + $commands = $commands->append($setReferencesOfTetheredNodeCommand); + } } else { $transientNode = $transientParentNode->forRegularChildNode( $subtree @@ -143,7 +157,9 @@ private function commandsForSubtreeRecursively(TransientNodeCopy $transientParen initialPropertyValues: PropertyValuesToWrite::fromArray( iterator_to_array($subtree->node->properties) ), - // todo references: + references: $this->serializeProjectedReferences( + $subgraph->findReferences($subtree->node->aggregateId, FindReferencesFilter::create()) + ) ); $createCopyOfNodeCommand = $createCopyOfNodeCommand->withTetheredDescendantNodeAggregateIds( @@ -154,7 +170,7 @@ private function commandsForSubtreeRecursively(TransientNodeCopy $transientParen } foreach ($subtree->children as $childSubtree) { - $commands = $this->commandsForSubtreeRecursively($transientNode, $childSubtree, $commands); + $commands = $this->commandsForSubtreeRecursively($transientNode, $childSubtree, $subgraph, $commands); } return $commands; diff --git a/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_TetheredNodes.feature b/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_TetheredNodes.feature index a4881bcc6e2..1f5462f2413 100644 --- a/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_TetheredNodes.feature +++ b/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_TetheredNodes.feature @@ -4,7 +4,12 @@ Feature: Copy nodes with tethered nodes Given using no content dimensions And using the following node types: """yaml - 'Neos.ContentRepository.Testing:Tethered': [] + 'Neos.ContentRepository.Testing:Tethered': + properties: + title: + type: string + references: + ref: [] 'Neos.ContentRepository.Testing:DocumentWithTethered': childNodes: tethered: @@ -159,3 +164,30 @@ Feature: Copy nodes with tethered nodes And I expect this node aggregate to disable dimension space points [] And I expect this node aggregate to have no child node aggregates And I expect this node aggregate to have the parent node aggregates ["sir-nodeward-nodington-i-copy"] + + Scenario: Properties and references are copied for tethered child nodes + And the command SetNodeReferences is executed with payload: + | Key | Value | + | sourceNodeAggregateId | "nodewyn-tetherton" | + | references | [{"referenceName": "ref", "references": [{"target": "sir-david-nodenborough"}]}] | + + And the command SetNodeProperties is executed with payload: + | Key | Value | + | nodeAggregateId | "nodewyn-tetherton" | + | propertyValues | {"title": "Original Text"} | + + When copy nodes recursively is executed with payload: + | Key | Value | + | sourceDimensionSpacePoint | {} | + | sourceNodeAggregateId | "sir-nodeward-nodington-i" | + | targetDimensionSpacePoint | {} | + | targetParentNodeAggregateId | "sir-david-nodenborough" | + | nodeAggregateIdMapping | {"sir-nodeward-nodington-i": "sir-nodeward-nodington-ii", "nodewyn-tetherton": "nodewyn-tetherton-copy"} | + + And I expect node aggregate identifier "nodewyn-tetherton-copy" to lead to node cs-identifier;nodewyn-tetherton-copy;{} + And I expect this node to have the following properties: + | Key | Value | + | title | "Original Text" | + And I expect this node to have the following references: + | Name | Node | Properties | + | ref | cs-identifier;sir-david-nodenborough;{} | null | From c4447d3d48da8d411b88b5eb687539f14da4e809 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 18 Nov 2024 15:17:08 +0100 Subject: [PATCH 192/214] TASK: Remove unused constraint check helpers from `TransientNodeCopy` --- .../NodeDuplication/TransientNodeCopy.php | 128 ++++-------------- .../Domain/Service/NodeDuplicationService.php | 5 +- 2 files changed, 32 insertions(+), 101 deletions(-) diff --git a/Neos.Neos/Classes/Domain/Service/NodeDuplication/TransientNodeCopy.php b/Neos.Neos/Classes/Domain/Service/NodeDuplication/TransientNodeCopy.php index a287c179ab0..20beaadfef9 100644 --- a/Neos.Neos/Classes/Domain/Service/NodeDuplication/TransientNodeCopy.php +++ b/Neos.Neos/Classes/Domain/Service/NodeDuplication/TransientNodeCopy.php @@ -2,17 +2,12 @@ namespace Neos\Neos\Domain\Service\NodeDuplication; -use Flowpack\NodeTemplates\Domain\NodeCreation\NodeConstraintException; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; use Neos\ContentRepository\Core\Feature\NodeCreation\Dto\NodeAggregateIdsByNodePaths; use Neos\ContentRepository\Core\Feature\NodeDuplication\Dto\NodeAggregateIdMapping; -use Neos\ContentRepository\Core\NodeType\NodeType; -use Neos\ContentRepository\Core\NodeType\NodeTypeManager; -use Neos\ContentRepository\Core\NodeType\NodeTypeName; use Neos\ContentRepository\Core\Projection\ContentGraph\NodePath; use Neos\ContentRepository\Core\Projection\ContentGraph\Subtree; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; -use Neos\ContentRepository\Core\SharedModel\Node\NodeName; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\Flow\Annotations as Flow; @@ -27,7 +22,7 @@ * For example the transient node can be passed as fictional $parentNode. * To create child transient nodes of the $parentNode use {@see self::forRegularChildNode()} and {@see self::forTetheredChildNode()} * - * An initial transient node can be created with {@see self::forRegular} + * An initial transient node can be created with {@see self::forEntry} * * @Flow\Proxy(false) */ @@ -37,7 +32,6 @@ private function __construct( public NodeAggregateId $aggregateId, public WorkspaceName $workspaceName, public OriginDimensionSpacePoint $originDimensionSpacePoint, - private NodeType $nodeType, /** * @var NodeAggregateIdMapping An assignment of "old" to "new" NodeAggregateIds */ @@ -45,55 +39,41 @@ private function __construct( /** * @var NodeAggregateIdsByNodePaths Deterministic NodeAggregate ids for the creation of tethered nodes (empty if there are no tethered nodes) */ - public NodeAggregateIdsByNodePaths $tetheredNodeAggregateIds, - private ?NodeName $tetheredNodeName, - private ?NodeType $tetheredParentNodeType, - private NodeTypeManager $nodeTypeManager + public NodeAggregateIdsByNodePaths $tetheredNodeAggregateIds ) { - if ($this->tetheredNodeName !== null) { - assert($this->tetheredParentNodeType !== null); - } } - public static function forRegular( + public static function forEntry( Subtree $subtree, WorkspaceName $targetWorkspaceName, OriginDimensionSpacePoint $targetOriginDimensionSpacePoint, - NodeAggregateIdMapping $nodeAggregateIdMapping, - NodeTypeManager $nodeTypeManager + NodeAggregateIdMapping $nodeAggregateIdMapping ): self { - $nodeType = $nodeTypeManager->getNodeType($subtree->node->nodeTypeName); return new self( $nodeAggregateIdMapping->getNewNodeAggregateId($subtree->node->aggregateId) ?? NodeAggregateId::create(), $targetWorkspaceName, $targetOriginDimensionSpacePoint, - $nodeType, $nodeAggregateIdMapping, - self::getTetheredDescendantNodeAggregateIds($subtree->children, $nodeAggregateIdMapping, NodePath::forRoot(), NodeAggregateIdsByNodePaths::createEmpty()), - null, - null, - $nodeTypeManager + self::getTetheredDescendantNodeAggregateIds( + $subtree->children, + $nodeAggregateIdMapping, + NodePath::forRoot(), + NodeAggregateIdsByNodePaths::createEmpty() + ) ); } public function forTetheredChildNode(Subtree $subtree): self { $nodeName = $subtree->node->name; - $nodeAggregateId = $this->tetheredNodeAggregateIds->getNodeAggregateId(NodePath::fromNodeNames($nodeName)); - - if (!$nodeAggregateId) { - throw new \InvalidArgumentException(sprintf('Name "%s" doesnt seem to be a point to a tethered node of "%s", could not determine deterministic node aggregate id.', $nodeName->value, $this->aggregateId->value)); + if (!$subtree->node->classification->isTethered() || $nodeName === null) { + throw new \InvalidArgumentException(sprintf('Node "%s" must be tethered if given to "forTetheredChildNode".', $subtree->node->aggregateId->value)); } - $tetheredNodeTypeDefinition = $this->nodeType->tetheredNodeTypeDefinitions->get($nodeName); - - if (!$tetheredNodeTypeDefinition) { - throw new \InvalidArgumentException(sprintf('Name "%s" doesnt match any tethered node type definition in the schema. Parent node "%s"', $nodeName->value, $this->aggregateId->value)); - } + $nodeAggregateId = $this->tetheredNodeAggregateIds->getNodeAggregateId(NodePath::fromNodeNames($nodeName)); - $childNodeType = $this->nodeTypeManager->getNodeType($tetheredNodeTypeDefinition->nodeTypeName); - if (!$childNodeType) { - throw new \InvalidArgumentException(sprintf('NodeType "%s" for tethered node "%s" does not exist.', $tetheredNodeTypeDefinition->nodeTypeName->value, $nodeName->value), 1718950833); + if ($nodeAggregateId === null) { + throw new \InvalidArgumentException(sprintf('Name "%s" doesnt seem to be a point to a tethered node of "%s", could not determine deterministic node aggregate id.', $nodeName->value, $this->aggregateId->value)); } // keep tethered node aggregate ids from parent @@ -110,33 +90,35 @@ public function forTetheredChildNode(Subtree $subtree): self } } - return new self( $nodeAggregateId, $this->workspaceName, $this->originDimensionSpacePoint, - $childNodeType, $this->nodeAggregateIdMapping, - self::getTetheredDescendantNodeAggregateIds($subtree->children, $this->nodeAggregateIdMapping, NodePath::forRoot(), $descendantTetheredNodeAggregateIds), - $nodeName, - $this->nodeType, - $this->nodeTypeManager + self::getTetheredDescendantNodeAggregateIds( + $subtree->children, + $this->nodeAggregateIdMapping, + NodePath::forRoot(), + $descendantTetheredNodeAggregateIds + ), ); } public function forRegularChildNode(Subtree $subtree): self { - $nodeType = $this->nodeTypeManager->getNodeType($subtree->node->nodeTypeName); return new self( - $this->nodeAggregateIdMapping->getNewNodeAggregateId($subtree->node->aggregateId) ?? NodeAggregateId::create(), + $this->nodeAggregateIdMapping->getNewNodeAggregateId( + $subtree->node->aggregateId + ) ?? NodeAggregateId::create(), $this->workspaceName, $this->originDimensionSpacePoint, - $nodeType, $this->nodeAggregateIdMapping, - self::getTetheredDescendantNodeAggregateIds($subtree->children, $this->nodeAggregateIdMapping, NodePath::forRoot(), NodeAggregateIdsByNodePaths::createEmpty()), - null, - null, - $this->nodeTypeManager + self::getTetheredDescendantNodeAggregateIds( + $subtree->children, + $this->nodeAggregateIdMapping, + NodePath::forRoot(), + NodeAggregateIdsByNodePaths::createEmpty() + ), ); } @@ -164,54 +146,4 @@ private static function getTetheredDescendantNodeAggregateIds(array $subtreeChil return $tetheredNodeAggregateIds; } - - /** - * @throws NodeConstraintException - */ - public function requireConstraintsImposedByAncestorsToBeMet(NodeType $childNodeType): void - { - if ($this->isTethered()) { - $this->requireNodeTypeConstraintsImposedByGrandparentToBeMet($this->tetheredParentNodeType->name, $this->tetheredNodeName, $childNodeType->name); - } else { - self::requireNodeTypeConstraintsImposedByParentToBeMet($this->nodeType, $childNodeType); - } - } - - private static function requireNodeTypeConstraintsImposedByParentToBeMet(NodeType $parentNodeType, NodeType $nodeType): void - { - if (!$parentNodeType->allowsChildNodeType($nodeType)) { - throw new NodeConstraintException( - sprintf( - 'Node type "%s" is not allowed for child nodes of type %s', - $nodeType->name->value, - $parentNodeType->name->value - ), - 1686417627173 - ); - } - } - - private function requireNodeTypeConstraintsImposedByGrandparentToBeMet(NodeTypeName $parentNodeTypeName, NodeName $tetheredNodeName, NodeTypeName $nodeTypeNameToCheck): void - { - if (!$this->nodeTypeManager->isNodeTypeAllowedAsChildToTetheredNode($parentNodeTypeName, $tetheredNodeName, $nodeTypeNameToCheck)) { - throw new NodeConstraintException( - sprintf( - 'Node type "%s" is not allowed below tethered child nodes "%s" of nodes of type "%s"', - $nodeTypeNameToCheck->value, - $tetheredNodeName->value, - $parentNodeTypeName->value - ), - 1687541480146 - ); - } - } - - /** - * @phpstan-assert-if-true !null $this->tetheredNodeName - * @phpstan-assert-if-true !null $this->tetheredParentNodeType - */ - private function isTethered(): bool - { - return $this->tetheredNodeName !== null; - } } diff --git a/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php b/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php index f54b6ef2114..0cd89b1467c 100644 --- a/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php +++ b/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php @@ -60,12 +60,11 @@ public function copyNodesRecursively( throw new \RuntimeException('todo'); } - $transientNodeCopy = TransientNodeCopy::forRegular( + $transientNodeCopy = TransientNodeCopy::forEntry( $subtree, $workspaceName, $targetDimensionSpacePoint, - $nodeAggregateIdMapping, - $contentRepository->getNodeTypeManager() + $nodeAggregateIdMapping ); $createCopyOfNodeCommand = CreateNodeAggregateWithNode::create( From 7e5fbf449ef8fffaad267e962a8ef2f7c5809e6f Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 18 Nov 2024 15:22:58 +0100 Subject: [PATCH 193/214] TASK: Move `NodeAggregateIdMapping` to Neos as it will be removed from the cr core soon --- .../NodeDuplication/Command/CopyNodesRecursively.php | 3 +-- .../NodeDuplication/NodeDuplicationCommandHandler.php | 4 ++-- .../Behavior/Features/Bootstrap/Features/NodeCopying.php | 2 +- .../Service/NodeDuplication}/NodeAggregateIdMapping.php | 8 ++++---- .../Domain/Service/NodeDuplication/TransientNodeCopy.php | 3 ++- .../Classes/Domain/Service/NodeDuplicationService.php | 2 +- .../Behavior/Features/Bootstrap/NodeDuplicationTrait.php | 2 +- 7 files changed, 12 insertions(+), 12 deletions(-) rename {Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/Dto => Neos.Neos/Classes/Domain/Service/NodeDuplication}/NodeAggregateIdMapping.php (92%) diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/Command/CopyNodesRecursively.php b/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/Command/CopyNodesRecursively.php index e655014cd0f..f72162b19b2 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/Command/CopyNodesRecursively.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/Command/CopyNodesRecursively.php @@ -18,15 +18,14 @@ use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; use Neos\ContentRepository\Core\Feature\Common\MatchableWithNodeIdToPublishOrDiscardInterface; use Neos\ContentRepository\Core\Feature\Common\RebasableToOtherWorkspaceInterface; -use Neos\ContentRepository\Core\Feature\NodeDuplication\Dto\NodeAggregateIdMapping; use Neos\ContentRepository\Core\Feature\NodeDuplication\Dto\NodeSubtreeSnapshot; use Neos\ContentRepository\Core\Feature\WorkspacePublication\Dto\NodeIdToPublishOrDiscard; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentSubgraphInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Node\NodeName; -use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; +use Neos\Neos\Domain\Service\NodeDuplication\NodeAggregateIdMapping; /** * CopyNodesRecursively command diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/NodeDuplicationCommandHandler.php b/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/NodeDuplicationCommandHandler.php index cce5dd61e29..d102d58e26a 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/NodeDuplicationCommandHandler.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/NodeDuplicationCommandHandler.php @@ -170,7 +170,7 @@ private function handleCopyNodesRecursively( private function requireNewNodeAggregateIdsToNotExist( ContentGraphInterface $contentGraph, - Dto\NodeAggregateIdMapping $nodeAggregateIdMapping + \Neos\Neos\Domain\Service\NodeDuplication\NodeAggregateIdMapping $nodeAggregateIdMapping ): void { foreach ($nodeAggregateIdMapping->getAllNewNodeAggregateIds() as $nodeAggregateId) { $this->requireProjectedNodeAggregateToNotExist( @@ -191,7 +191,7 @@ private function createEventsForNodeToInsert( ?NodeAggregateId $targetSucceedingSiblingNodeAggregateId, ?NodeName $targetNodeName, NodeSubtreeSnapshot $nodeToInsert, - Dto\NodeAggregateIdMapping $nodeAggregateIdMapping, + \Neos\Neos\Domain\Service\NodeDuplication\NodeAggregateIdMapping $nodeAggregateIdMapping, array &$events, ): void { $events[] = new NodeAggregateWithNodeWasCreated( diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeCopying.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeCopying.php index 5b020a33873..9dcb3d3640a 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeCopying.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeCopying.php @@ -17,12 +17,12 @@ use Behat\Gherkin\Node\TableNode; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; use Neos\ContentRepository\Core\Feature\NodeDuplication\Command\CopyNodesRecursively; -use Neos\ContentRepository\Core\Feature\NodeDuplication\Dto\NodeAggregateIdMapping; use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Node\NodeName; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\CRTestSuiteRuntimeVariables; +use Neos\Neos\Domain\Service\NodeDuplication\NodeAggregateIdMapping; /** * The node copying trait for behavioral tests diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/Dto/NodeAggregateIdMapping.php b/Neos.Neos/Classes/Domain/Service/NodeDuplication/NodeAggregateIdMapping.php similarity index 92% rename from Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/Dto/NodeAggregateIdMapping.php rename to Neos.Neos/Classes/Domain/Service/NodeDuplication/NodeAggregateIdMapping.php index 1c5be536e61..02a947f11ba 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/Dto/NodeAggregateIdMapping.php +++ b/Neos.Neos/Classes/Domain/Service/NodeDuplication/NodeAggregateIdMapping.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Neos\ContentRepository\Core\Feature\NodeDuplication\Dto; +namespace Neos\Neos\Domain\Service\NodeDuplication; /* * This file is part of the Neos.ContentRepository package. @@ -14,6 +14,7 @@ * source code. */ +use Neos\ContentRepository\Core\Feature\NodeDuplication\Dto\NodeSubtreeSnapshot; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; /** @@ -21,9 +22,7 @@ * * Usable for predefining NodeAggregateIds if multiple nodes are copied. * - * You'll never create this class yourself; but you use {@see CopyNodesRecursively::createFromSubgraphAndStartNode()} - * - * @internal implementation detail of {@see CopyNodesRecursively} command + * @internal only needed for testing */ final class NodeAggregateIdMapping implements \JsonSerializable { @@ -60,6 +59,7 @@ public function __construct(array $nodeAggregateIds) public static function generateForNodeSubtreeSnapshot(NodeSubtreeSnapshot $nodeSubtreeSnapshot): self { $nodeAggregateIdMapping = []; + /** @phpstan-ignore neos.cr.internal */ $nodeSubtreeSnapshot->walk( function (NodeSubtreeSnapshot $nodeSubtreeSnapshot) use (&$nodeAggregateIdMapping) { // here, we create new random NodeAggregateIds. diff --git a/Neos.Neos/Classes/Domain/Service/NodeDuplication/TransientNodeCopy.php b/Neos.Neos/Classes/Domain/Service/NodeDuplication/TransientNodeCopy.php index 20beaadfef9..2c4e7f4021e 100644 --- a/Neos.Neos/Classes/Domain/Service/NodeDuplication/TransientNodeCopy.php +++ b/Neos.Neos/Classes/Domain/Service/NodeDuplication/TransientNodeCopy.php @@ -1,10 +1,11 @@ Date: Mon, 18 Nov 2024 15:43:44 +0100 Subject: [PATCH 194/214] TASK: Make phpstan happy --- .../Domain/Service/NodeDuplication/TransientNodeCopy.php | 2 +- Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Neos.Neos/Classes/Domain/Service/NodeDuplication/TransientNodeCopy.php b/Neos.Neos/Classes/Domain/Service/NodeDuplication/TransientNodeCopy.php index 2c4e7f4021e..b27b4a7dbc0 100644 --- a/Neos.Neos/Classes/Domain/Service/NodeDuplication/TransientNodeCopy.php +++ b/Neos.Neos/Classes/Domain/Service/NodeDuplication/TransientNodeCopy.php @@ -129,7 +129,7 @@ public function forRegularChildNode(Subtree $subtree): self private static function getTetheredDescendantNodeAggregateIds(array $subtreeChildren, NodeAggregateIdMapping $nodeAggregateIdMapping, NodePath $nodePath, NodeAggregateIdsByNodePaths $tetheredNodeAggregateIds): NodeAggregateIdsByNodePaths { foreach ($subtreeChildren as $childSubtree) { - if (!$childSubtree->node->classification->isTethered()) { + if (!$childSubtree->node->classification->isTethered() || !$childSubtree->node->name) { continue; } diff --git a/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php b/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php index aad6b63236f..9451f4caf14 100644 --- a/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php +++ b/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php @@ -55,7 +55,7 @@ public function copyNodesRecursively( $subtree = $subgraph->findSubtree($sourceNodeAggregateId, FindSubtreeFilter::create()); $targetParentNode = $subgraph->findNodeById($targetParentNodeAggregateId); - if ($targetParentNode === null) { + if ($targetParentNode === null || $subtree === null) { // todo simple constraint checks throw new \RuntimeException('todo'); } From 37e9a51eea60aa0aa4cc46da96f79b85b7984c0c Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 18 Nov 2024 15:44:50 +0100 Subject: [PATCH 195/214] TASK: Fix specifying `references` in a row --- .../Bootstrap/GenericCommandExecutionAndEventPublication.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php index 5d1b7157069..5106dad058b 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php @@ -191,7 +191,9 @@ protected function addDefaultCommandArgumentValues(string $commandClassName, arr } } if ($commandClassName === CreateNodeAggregateWithNode::class || $commandClassName === SetNodeReferences::class) { - if (is_array($commandArguments['references'] ?? null)) { + if (is_string($commandArguments['references'] ?? null)) { + $commandArguments['references'] = iterator_to_array($this->mapRawNodeReferencesToNodeReferencesToWrite(json_decode($commandArguments['references'], true, 512, JSON_THROW_ON_ERROR))); + } elseif (is_array($commandArguments['references'] ?? null)) { $commandArguments['references'] = iterator_to_array($this->mapRawNodeReferencesToNodeReferencesToWrite($commandArguments['references'])); } } From babc6cfcd22ea20a9ff6c90048d0086206a8ff62 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 18 Nov 2024 15:49:13 +0100 Subject: [PATCH 196/214] TASK: Add proper constraint-check exceptions --- .../Domain/Service/NodeDuplicationService.php | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php b/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php index 9451f4caf14..d3c8ac8b378 100644 --- a/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php +++ b/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php @@ -20,6 +20,7 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\Subtree; use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; +use Neos\ContentRepository\Core\SharedModel\Exception\NodeAggregateCurrentlyDoesNotExist; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Node\NodeName; use Neos\ContentRepository\Core\SharedModel\Node\ReferenceName; @@ -53,11 +54,14 @@ public function copyNodesRecursively( $subgraph = $contentRepository->getContentGraph($workspaceName)->getSubgraph($sourceDimensionSpacePoint, VisibilityConstraints::withoutRestrictions()); - $subtree = $subgraph->findSubtree($sourceNodeAggregateId, FindSubtreeFilter::create()); $targetParentNode = $subgraph->findNodeById($targetParentNodeAggregateId); - if ($targetParentNode === null || $subtree === null) { - // todo simple constraint checks - throw new \RuntimeException('todo'); + if ($targetParentNode === null) { + throw new NodeAggregateCurrentlyDoesNotExist(sprintf('The target parent node aggregate "%s" does not exist', $targetParentNodeAggregateId->value)); + } + + $subtree = $subgraph->findSubtree($sourceNodeAggregateId, FindSubtreeFilter::create()); + if ($subtree === null) { + throw new NodeAggregateCurrentlyDoesNotExist(sprintf('The source node aggregate "%s" does not exist', $sourceNodeAggregateId->value)); } $transientNodeCopy = TransientNodeCopy::forEntry( @@ -119,6 +123,7 @@ private function commandsForSubtreeRecursively(TransientNodeCopy $transientParen $transientParentNode->workspaceName, $transientNode->aggregateId, $transientParentNode->originDimensionSpacePoint, + // todo skip properties not in schema PropertyValuesToWrite::fromArray( iterator_to_array($subtree->node->properties) ), From a4b16897c2c2b3041f7ffaf68f95f73efb5ff911 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 18 Nov 2024 15:53:20 +0100 Subject: [PATCH 197/214] TASK: Remove todo regarding additional property values for first node the idea was that command hooks must be able to set a unique uri path segment, but as were using now a high level service we dont need to intercept that. --- Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php | 1 - 1 file changed, 1 deletion(-) diff --git a/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php b/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php index d3c8ac8b378..6d79d2e7996 100644 --- a/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php +++ b/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php @@ -38,7 +38,6 @@ public function __construct( ) { } - // todo, add additional property values for first node! public function copyNodesRecursively( ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, From 2fb6ab783c604b7841af6a82ee19c0c4086f2f7c Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 18 Nov 2024 16:03:25 +0100 Subject: [PATCH 198/214] TASK: (re)Introduce `Subtrees` as typed array for `$children` Related https://github.com/neos/neos-development-collection/issues/4535 --- .../src/Domain/Repository/ContentSubgraph.php | 3 +- .../src/Domain/Repository/NodeFactory.php | 3 +- .../Projection/ContentGraph/Subtree.php | 20 +++++--- .../Projection/ContentGraph/Subtrees.php | 48 +++++++++++++++++++ .../NodeDuplication/TransientNodeCopy.php | 6 +-- 5 files changed, 68 insertions(+), 12 deletions(-) create mode 100644 Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Subtrees.php diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php index b273c36a402..72968a19dd9 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php @@ -49,6 +49,7 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\Nodes; use Neos\ContentRepository\Core\Projection\ContentGraph\References; use Neos\ContentRepository\Core\Projection\ContentGraph\Subtree; +use Neos\ContentRepository\Core\Projection\ContentGraph\Subtrees; use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateClassification; @@ -304,7 +305,7 @@ public function findSubtree(NodeAggregateId $entryNodeAggregateId, FindSubtreeFi $this->dimensionSpacePoint, $this->visibilityConstraints ); - $subtree = new Subtree((int)$nodeData['level'], $node, array_key_exists($nodeAggregateId, $subtreesByParentNodeId) ? array_reverse($subtreesByParentNodeId[$nodeAggregateId]) : []); + $subtree = Subtree::create((int)$nodeData['level'], $node, Subtrees::fromArray(array_key_exists($nodeAggregateId, $subtreesByParentNodeId) ? array_reverse($subtreesByParentNodeId[$nodeAggregateId]) : [])); if ($subtree->level === 0) { return $subtree; } diff --git a/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/NodeFactory.php b/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/NodeFactory.php index 3a8068b68dd..fa05b84f193 100644 --- a/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/NodeFactory.php +++ b/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/NodeFactory.php @@ -33,6 +33,7 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\Reference; use Neos\ContentRepository\Core\Projection\ContentGraph\References; use Neos\ContentRepository\Core\Projection\ContentGraph\Subtree; +use Neos\ContentRepository\Core\Projection\ContentGraph\Subtrees; use Neos\ContentRepository\Core\Projection\ContentGraph\Timestamps; use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; @@ -154,7 +155,7 @@ public function mapNodeRowsToSubtree( $nodeAggregateId = $nodeRow['nodeaggregateid']; $parentNodeAggregateId = $nodeRow['parentnodeaggregateid']; $node = $this->mapNodeRowToNode($nodeRow, $visibilityConstraints); - $subtree = new Subtree((int)$nodeRow['level'], $node, array_key_exists($nodeAggregateId, $subtreesByParentNodeId) ? array_reverse($subtreesByParentNodeId[$nodeAggregateId]) : []); + $subtree = Subtree::create((int)$nodeRow['level'], $node, Subtrees::fromArray(array_key_exists($nodeAggregateId, $subtreesByParentNodeId) ? array_reverse($subtreesByParentNodeId[$nodeAggregateId]) : [])); if ($subtree->level === 0) { return $subtree; } diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Subtree.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Subtree.php index 1f0ea1f5033..db0b5af29ed 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Subtree.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Subtree.php @@ -5,17 +5,25 @@ namespace Neos\ContentRepository\Core\Projection\ContentGraph; /** - * @api returned by {@see ContentSubgraphInterface} + * @api returned by {@see ContentSubgraphInterface::findSubtree()} */ final readonly class Subtree { - /** - * @param array $children - */ - public function __construct( + private function __construct( public int $level, public Node $node, - public array $children + public Subtrees $children ) { } + + /** + * @internal + */ + public static function create( + int $level, + Node $node, + Subtrees $children + ): self { + return new self($level, $node, $children); + } } diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Subtrees.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Subtrees.php new file mode 100644 index 00000000000..644d47b0c02 --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Subtrees.php @@ -0,0 +1,48 @@ + + */ +final readonly class Subtrees implements \IteratorAggregate, \Countable +{ + /** @var array */ + private array $items; + + private function __construct( + Subtree ...$items + ) { + $this->items = $items; + } + + /** + * @internal + */ + public static function create(Subtree ...$items): self + { + return new self(...$items); + } + + /** + * @internal + * @param array $items + */ + public static function fromArray(array $items): self + { + return new self(...$items); + } + + public function getIterator(): \Traversable + { + yield from $this->items; + } + + public function count(): int + { + return count($this->items); + } +} diff --git a/Neos.Neos/Classes/Domain/Service/NodeDuplication/TransientNodeCopy.php b/Neos.Neos/Classes/Domain/Service/NodeDuplication/TransientNodeCopy.php index b27b4a7dbc0..8e3f169c026 100644 --- a/Neos.Neos/Classes/Domain/Service/NodeDuplication/TransientNodeCopy.php +++ b/Neos.Neos/Classes/Domain/Service/NodeDuplication/TransientNodeCopy.php @@ -8,6 +8,7 @@ use Neos\ContentRepository\Core\Feature\NodeCreation\Dto\NodeAggregateIdsByNodePaths; use Neos\ContentRepository\Core\Projection\ContentGraph\NodePath; use Neos\ContentRepository\Core\Projection\ContentGraph\Subtree; +use Neos\ContentRepository\Core\Projection\ContentGraph\Subtrees; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\Flow\Annotations as Flow; @@ -123,10 +124,7 @@ public function forRegularChildNode(Subtree $subtree): self ); } - /** - * @param array $subtreeChildren - */ - private static function getTetheredDescendantNodeAggregateIds(array $subtreeChildren, NodeAggregateIdMapping $nodeAggregateIdMapping, NodePath $nodePath, NodeAggregateIdsByNodePaths $tetheredNodeAggregateIds): NodeAggregateIdsByNodePaths + private static function getTetheredDescendantNodeAggregateIds(Subtrees $subtreeChildren, NodeAggregateIdMapping $nodeAggregateIdMapping, NodePath $nodePath, NodeAggregateIdsByNodePaths $tetheredNodeAggregateIds): NodeAggregateIdsByNodePaths { foreach ($subtreeChildren as $childSubtree) { if (!$childSubtree->node->classification->isTethered() || !$childSubtree->node->name) { From 42c6b6a0e7cf04f5aa538021bfcfdb357714f412 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 18 Nov 2024 16:08:41 +0100 Subject: [PATCH 199/214] !!! TASK: Soft removal of `CopyNodesRecursively` The command `CopyNodesRecursively` still needs to preserved as `RebasableToOtherWorkspaceInterface` "internal" command so publishing from user workspaces continues to work. --- .../NodeCopying/CopyNode_NoDimensions.feature | 90 ------------------- .../Command/CopyNodesRecursively.php | 5 +- .../Features/Bootstrap/CRTestSuiteTrait.php | 1 - .../Bootstrap/Features/NodeCopying.php | 72 --------------- ...ricCommandExecutionAndEventPublication.php | 2 - .../ContentRepositoryAuthProvider.php | 2 - 6 files changed, 2 insertions(+), 170 deletions(-) delete mode 100644 Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeCopying/CopyNode_NoDimensions.feature delete mode 100644 Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeCopying.php diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeCopying/CopyNode_NoDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeCopying/CopyNode_NoDimensions.feature deleted file mode 100644 index c8c036f9ed2..00000000000 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeCopying/CopyNode_NoDimensions.feature +++ /dev/null @@ -1,90 +0,0 @@ -@contentrepository @adapters=DoctrineDBAL -Feature: Copy nodes (without dimensions) - - Background: - Given using no content dimensions - And using the following node types: - """yaml - 'Neos.ContentRepository.Testing:Document': - references: - ref: [] - """ - And using identifier "default", I define a content repository - And I am in content repository "default" - And the command CreateRootWorkspace is executed with payload: - | Key | Value | - | workspaceName | "live" | - | newContentStreamId | "cs-identifier" | - And I am in workspace "live" - And the command CreateRootNodeAggregateWithNode is executed with payload: - | Key | Value | - | nodeAggregateId | "lady-eleonode-rootford" | - | nodeTypeName | "Neos.ContentRepository:Root" | - And the event NodeAggregateWithNodeWasCreated was published with payload: - | Key | Value | - | workspaceName | "live" | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "sir-david-nodenborough" | - | nodeTypeName | "Neos.ContentRepository.Testing:Document" | - | originDimensionSpacePoint | {} | - | coveredDimensionSpacePoints | [{}] | - | parentNodeAggregateId | "lady-eleonode-rootford" | - | nodeName | "document" | - | nodeAggregateClassification | "regular" | - And the event NodeAggregateWithNodeWasCreated was published with payload: - | Key | Value | - | workspaceName | "live" | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "nody-mc-nodeface" | - | nodeTypeName | "Neos.ContentRepository.Testing:Document" | - | originDimensionSpacePoint | {} | - | coveredDimensionSpacePoints | [{}] | - | parentNodeAggregateId | "sir-david-nodenborough" | - | nodeName | "child-document" | - | nodeAggregateClassification | "regular" | - And the event NodeAggregateWithNodeWasCreated was published with payload: - | Key | Value | - | workspaceName | "live" | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "sir-nodeward-nodington-iii" | - | nodeTypeName | "Neos.ContentRepository.Testing:Document" | - | originDimensionSpacePoint | {} | - | coveredDimensionSpacePoints | [{}] | - | parentNodeAggregateId | "lady-eleonode-rootford" | - | nodeName | "esquire" | - | nodeAggregateClassification | "regular" | - - Scenario: Copy - When I am in workspace "live" and dimension space point {} - # node to copy (currentNode): "sir-nodeward-nodington-iii" - Then I expect node aggregate identifier "sir-nodeward-nodington-iii" to lead to node cs-identifier;sir-nodeward-nodington-iii;{} - When the command CopyNodesRecursively is executed, copying the current node aggregate with payload: - | Key | Value | - | targetDimensionSpacePoint | {} | - | targetParentNodeAggregateId | "nody-mc-nodeface" | - | targetNodeName | "target-nn" | - | targetSucceedingSiblingnodeAggregateId | null | - | nodeAggregateIdMapping | {"sir-nodeward-nodington-iii": "sir-nodeward-nodington-iii-copy"} | - - Then I expect node aggregate identifier "sir-nodeward-nodington-iii-copy" to lead to node cs-identifier;sir-nodeward-nodington-iii-copy;{} - - Scenario: Copy References - When I am in workspace "live" and dimension space point {} - And the command SetNodeReferences is executed with payload: - | Key | Value | - | sourceNodeAggregateId | "sir-nodeward-nodington-iii" | - | references | [{"referenceName": "ref", "references": [{"target": "sir-david-nodenborough"}]}] | - - Then I expect node aggregate identifier "sir-nodeward-nodington-iii" to lead to node cs-identifier;sir-nodeward-nodington-iii;{} - And the command CopyNodesRecursively is executed, copying the current node aggregate with payload: - | Key | Value | - | targetDimensionSpacePoint | {} | - | targetParentNodeAggregateId | "nody-mc-nodeface" | - | targetNodeName | "target-nn" | - | targetSucceedingSiblingnodeAggregateId | null | - | nodeAggregateIdMapping | {"sir-nodeward-nodington-iii": "sir-nodeward-nodington-iii-copy"} | - - And I expect node aggregate identifier "sir-nodeward-nodington-iii-copy" to lead to node cs-identifier;sir-nodeward-nodington-iii-copy;{} - And I expect this node to have the following references: - | Name | Node | Properties | - | ref | cs-identifier;sir-david-nodenborough;{} | null | diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/Command/CopyNodesRecursively.php b/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/Command/CopyNodesRecursively.php index f72162b19b2..e3219fa695a 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/Command/CopyNodesRecursively.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/Command/CopyNodesRecursively.php @@ -14,7 +14,6 @@ namespace Neos\ContentRepository\Core\Feature\NodeDuplication\Command; -use Neos\ContentRepository\Core\CommandHandler\CommandInterface; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; use Neos\ContentRepository\Core\Feature\Common\MatchableWithNodeIdToPublishOrDiscardInterface; use Neos\ContentRepository\Core\Feature\Common\RebasableToOtherWorkspaceInterface; @@ -34,10 +33,10 @@ * The node will be appended as child node of the given `parentNodeId` which must cover the given * `dimensionSpacePoint`. * - * @api commands are the write-API of the ContentRepository + * @internal + * @deprecated with Neos 9 Beta 16, please use Neos's {@see \Neos\Neos\Domain\Service\NodeDuplicationService} instead. */ final readonly class CopyNodesRecursively implements - CommandInterface, \JsonSerializable, MatchableWithNodeIdToPublishOrDiscardInterface, RebasableToOtherWorkspaceInterface diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteTrait.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteTrait.php index 68e1075f1c1..a6cac6dc8b2 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteTrait.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteTrait.php @@ -61,7 +61,6 @@ trait CRTestSuiteTrait use ContentStreamClosing; use NodeCreation; - use NodeCopying; use SubtreeTagging; use NodeModification; use NodeMove; diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeCopying.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeCopying.php deleted file mode 100644 index 9dcb3d3640a..00000000000 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeCopying.php +++ /dev/null @@ -1,72 +0,0 @@ -readPayloadTable($payloadTable); - $subgraph = $this->currentContentRepository->getContentGraph($this->currentWorkspaceName)->getSubgraph( - $this->currentDimensionSpacePoint, - VisibilityConstraints::withoutRestrictions() - ); - $targetDimensionSpacePoint = isset($commandArguments['targetDimensionSpacePoint']) - ? OriginDimensionSpacePoint::fromArray($commandArguments['targetDimensionSpacePoint']) - : OriginDimensionSpacePoint::fromDimensionSpacePoint($this->currentDimensionSpacePoint); - $targetSucceedingSiblingNodeAggregateId = isset($commandArguments['targetSucceedingSiblingNodeAggregateId']) - ? NodeAggregateId::fromString($commandArguments['targetSucceedingSiblingNodeAggregateId']) - : null; - - $workspaceName = isset($commandArguments['workspaceName']) - ? WorkspaceName::fromString($commandArguments['workspaceName']) - : $this->currentWorkspaceName; - - $command = CopyNodesRecursively::createFromSubgraphAndStartNode( - $subgraph, - $workspaceName, - $this->currentNode, - $targetDimensionSpacePoint, - NodeAggregateId::fromString($commandArguments['targetParentNodeAggregateId']), - $targetSucceedingSiblingNodeAggregateId - ); - if (isset($commandArguments['targetNodeName'])) { - $command = $command->withTargetNodeName(NodeName::fromString($commandArguments['targetNodeName'])); - } - $command = $command->withNodeAggregateIdMapping(NodeAggregateIdMapping::fromArray($commandArguments['nodeAggregateIdMapping'])); - - $this->currentContentRepository->handle($command); - } -} diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php index 5106dad058b..0afbf60c150 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php @@ -29,7 +29,6 @@ use Neos\ContentRepository\Core\Feature\NodeCreation\Command\CreateNodeAggregateWithNode; use Neos\ContentRepository\Core\Feature\NodeDisabling\Command\DisableNodeAggregate; use Neos\ContentRepository\Core\Feature\NodeDisabling\Command\EnableNodeAggregate; -use Neos\ContentRepository\Core\Feature\NodeDuplication\Command\CopyNodesRecursively; use Neos\ContentRepository\Core\Feature\NodeModification\Command\SetNodeProperties; use Neos\ContentRepository\Core\Feature\NodeModification\Dto\PropertyValuesToWrite; use Neos\ContentRepository\Core\Feature\NodeMove\Command\MoveNodeAggregate; @@ -245,7 +244,6 @@ protected static function resolveShortCommandName(string $shortCommandName): str ChangeBaseWorkspace::class, ChangeNodeAggregateName::class, ChangeNodeAggregateType::class, - CopyNodesRecursively::class, CreateNodeAggregateWithNode::class, CreateNodeVariant::class, CreateRootNodeAggregateWithNode::class, diff --git a/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php index 6bd844d8a5a..74bfd788aa4 100644 --- a/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php +++ b/Neos.Neos/Classes/Security/ContentRepositoryAuthProvider/ContentRepositoryAuthProvider.php @@ -10,7 +10,6 @@ use Neos\ContentRepository\Core\Feature\NodeCreation\Command\CreateNodeAggregateWithNode; use Neos\ContentRepository\Core\Feature\NodeDisabling\Command\DisableNodeAggregate; use Neos\ContentRepository\Core\Feature\NodeDisabling\Command\EnableNodeAggregate; -use Neos\ContentRepository\Core\Feature\NodeDuplication\Command\CopyNodesRecursively; use Neos\ContentRepository\Core\Feature\NodeModification\Command\SetNodeProperties; use Neos\ContentRepository\Core\Feature\NodeMove\Command\MoveNodeAggregate; use Neos\ContentRepository\Core\Feature\NodeReferencing\Command\SetNodeReferences; @@ -152,7 +151,6 @@ public function canExecuteCommand(CommandInterface $command): Privilege private function nodeThatRequiresEditPrivilegeForCommand(CommandInterface $command): ?NodeAddress { return match ($command::class) { - CopyNodesRecursively::class => NodeAddress::create($this->contentRepositoryId, $command->workspaceName, $command->targetDimensionSpacePoint->toDimensionSpacePoint(), $command->targetParentNodeAggregateId), CreateNodeAggregateWithNode::class => NodeAddress::create($this->contentRepositoryId, $command->workspaceName, $command->originDimensionSpacePoint->toDimensionSpacePoint(), $command->parentNodeAggregateId), CreateNodeVariant::class => NodeAddress::create($this->contentRepositoryId, $command->workspaceName, $command->sourceOrigin->toDimensionSpacePoint(), $command->nodeAggregateId), DisableNodeAggregate::class, From d5d9dbe63f0836d30ec0eaab1df790a0824704b2 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 18 Nov 2024 16:26:18 +0100 Subject: [PATCH 200/214] TASK: Make `NodeAggregateIdMapping` usable and API, as its used in the Neos ui to fetch the newly create node --- .../NodeAggregateIdMapping.php | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/Neos.Neos/Classes/Domain/Service/NodeDuplication/NodeAggregateIdMapping.php b/Neos.Neos/Classes/Domain/Service/NodeDuplication/NodeAggregateIdMapping.php index 02a947f11ba..713985da98a 100644 --- a/Neos.Neos/Classes/Domain/Service/NodeDuplication/NodeAggregateIdMapping.php +++ b/Neos.Neos/Classes/Domain/Service/NodeDuplication/NodeAggregateIdMapping.php @@ -20,9 +20,7 @@ /** * An assignment of "old" to "new" NodeAggregateIds * - * Usable for predefining NodeAggregateIds if multiple nodes are copied. - * - * @internal only needed for testing + * Usable for predefining NodeAggregateIds for deterministic testing, or fetching the newly inserted node. */ final class NodeAggregateIdMapping implements \JsonSerializable { @@ -33,7 +31,7 @@ final class NodeAggregateIdMapping implements \JsonSerializable * * @var array */ - protected array $nodeAggregateIds = []; + private array $nodeAggregateIds = []; /** * @param array $nodeAggregateIds @@ -53,6 +51,18 @@ public function __construct(array $nodeAggregateIds) } } + public static function createEmpty(): self + { + return new self([]); + } + + public function withNewNodeAggregateId(NodeAggregateId $oldNodeAggregateId, NodeAggregateId $newNodeAggregateId): self + { + $nodeAggregateIds = $this->nodeAggregateIds; + $nodeAggregateIds[$oldNodeAggregateId->value] = $newNodeAggregateId; + return new self($nodeAggregateIds); + } + /** * Create a new id mapping, *GENERATING* new ids. */ @@ -71,13 +81,13 @@ function (NodeSubtreeSnapshot $nodeSubtreeSnapshot) use (&$nodeAggregateIdMappin } /** - * @param array $array + * @param array $array */ public static function fromArray(array $array): self { $nodeAggregateIds = []; foreach ($array as $oldNodeAggregateId => $newNodeAggregateId) { - $nodeAggregateIds[$oldNodeAggregateId] = NodeAggregateId::fromString($newNodeAggregateId); + $nodeAggregateIds[$oldNodeAggregateId] = $newNodeAggregateId instanceof NodeAggregateId ? $newNodeAggregateId : NodeAggregateId::fromString($newNodeAggregateId); } return new self($nodeAggregateIds); From 3d5e9632b025a3e1517cae98ea1b089e80880579 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 18 Nov 2024 16:29:51 +0100 Subject: [PATCH 201/214] TASK: Remove `$targetNodeName` from first level api option in `copyNodesRecursively` as node names are deprecated. Instead we should rather expose the to be run `Commands` and then one can change the node name --- Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php | 5 ----- .../Behavior/Features/Bootstrap/NodeDuplicationTrait.php | 1 - .../NodeCopying/CopyNode_NoDimensions.feature | 5 +---- 3 files changed, 1 insertion(+), 10 deletions(-) diff --git a/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php b/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php index 6d79d2e7996..9a5ab12a301 100644 --- a/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php +++ b/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php @@ -45,7 +45,6 @@ public function copyNodesRecursively( NodeAggregateId $sourceNodeAggregateId, OriginDimensionSpacePoint $targetDimensionSpacePoint, NodeAggregateId $targetParentNodeAggregateId, - ?NodeName $targetNodeName, ?NodeAggregateId $targetSucceedingSiblingNodeAggregateId, NodeAggregateIdMapping $nodeAggregateIdMapping ): void { @@ -86,10 +85,6 @@ public function copyNodesRecursively( ) ); - if ($targetNodeName) { - $createCopyOfNodeCommand = $createCopyOfNodeCommand->withNodeName($targetNodeName); - } - $createCopyOfNodeCommand = $createCopyOfNodeCommand->withTetheredDescendantNodeAggregateIds( $transientNodeCopy->tetheredNodeAggregateIds ); diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/NodeDuplicationTrait.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/NodeDuplicationTrait.php index 6481337aaae..9543e068913 100644 --- a/Neos.Neos/Tests/Behavior/Features/Bootstrap/NodeDuplicationTrait.php +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/NodeDuplicationTrait.php @@ -73,7 +73,6 @@ public function copyNodesRecursivelyIsExecutedWithPayload(TableNode $payloadTabl $sourceNodeAggregateId, $targetDimensionSpacePoint, NodeAggregateId::fromString($commandArguments['targetParentNodeAggregateId']), - isset($commandArguments['targetNodeName']) ? NodeName::fromString($commandArguments['targetNodeName']) : null, $targetSucceedingSiblingNodeAggregateId, NodeAggregateIdMapping::fromArray($commandArguments['nodeAggregateIdMapping']) ) diff --git a/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_NoDimensions.feature b/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_NoDimensions.feature index ef320966052..d09d3c73f68 100644 --- a/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_NoDimensions.feature +++ b/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_NoDimensions.feature @@ -44,14 +44,13 @@ Feature: Copy nodes (without dimensions) | sourceNodeAggregateId | "sir-nodeward-nodington-iii" | | targetDimensionSpacePoint | {} | | targetParentNodeAggregateId | "nody-mc-nodeface" | - | targetNodeName | "target-nn" | | targetSucceedingSiblingnodeAggregateId | null | | nodeAggregateIdMapping | {"sir-nodeward-nodington-iii": "sir-nodeward-nodington-iii-copy"} | Then I expect node aggregate identifier "sir-nodeward-nodington-iii-copy" to lead to node cs-identifier;sir-nodeward-nodington-iii-copy;{} And I expect the node aggregate "sir-nodeward-nodington-iii-copy" to exist And I expect this node aggregate to be classified as "regular" - And I expect this node aggregate to be named "target-nn" + And I expect this node aggregate to be unnamed And I expect this node aggregate to be of type "Neos.ContentRepository.Testing:Document" And I expect this node aggregate to occupy dimension space points [[]] And I expect this node aggregate to disable dimension space points [] @@ -71,7 +70,6 @@ Feature: Copy nodes (without dimensions) | sourceNodeAggregateId | "sir-nodeward-nodington-iii" | | targetDimensionSpacePoint | {} | | targetParentNodeAggregateId | "nody-mc-nodeface" | - | targetNodeName | "target-nn" | | targetSucceedingSiblingnodeAggregateId | null | | nodeAggregateIdMapping | {"sir-nodeward-nodington-iii": "sir-nodeward-nodington-iii-copy"} | @@ -96,7 +94,6 @@ Feature: Copy nodes (without dimensions) | sourceNodeAggregateId | "sir-nodeward-nodington-iii" | | targetDimensionSpacePoint | {} | | targetParentNodeAggregateId | "nody-mc-nodeface" | - | targetNodeName | "target-nn" | | targetSucceedingSiblingnodeAggregateId | null | | nodeAggregateIdMapping | {"sir-nodeward-nodington-iii": "sir-nodeward-nodington-iii-copy"} | From 00966893e6fdcb9d4dee018a1f3cd78596d0410c Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 18 Nov 2024 16:34:15 +0100 Subject: [PATCH 202/214] TASK: Move generic `Commands` into cr core --- .../Classes/CommandHandler}/Commands.php | 14 +++++++++----- .../Domain/Service/NodeDuplicationService.php | 3 +-- 2 files changed, 10 insertions(+), 7 deletions(-) rename {Neos.Neos/Classes/Domain/Service/NodeDuplication => Neos.ContentRepository.Core/Classes/CommandHandler}/Commands.php (76%) diff --git a/Neos.Neos/Classes/Domain/Service/NodeDuplication/Commands.php b/Neos.ContentRepository.Core/Classes/CommandHandler/Commands.php similarity index 76% rename from Neos.Neos/Classes/Domain/Service/NodeDuplication/Commands.php rename to Neos.ContentRepository.Core/Classes/CommandHandler/Commands.php index f0c8ea77e47..c76eb1955e7 100644 --- a/Neos.Neos/Classes/Domain/Service/NodeDuplication/Commands.php +++ b/Neos.ContentRepository.Core/Classes/CommandHandler/Commands.php @@ -2,22 +2,26 @@ declare(strict_types=1); -namespace Neos\Neos\Domain\Service\NodeDuplication; - -use Neos\ContentRepository\Core\CommandHandler\CommandInterface; +namespace Neos\ContentRepository\Core\CommandHandler; /** + * @api can be used as collection of commands to be individually handled: + * + * foreach ($commands as $command) { + * $contentRepository->handle($command); + * } + * * @implements \IteratorAggregate */ final readonly class Commands implements \IteratorAggregate, \Countable { - /** @var array */ + /** @var array */ private array $items; private function __construct( CommandInterface ...$items ) { - $this->items = $items; + $this->items = array_values($items); } public static function create(CommandInterface ...$items): self diff --git a/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php b/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php index 9a5ab12a301..422178037df 100644 --- a/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php +++ b/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php @@ -4,6 +4,7 @@ namespace Neos\Neos\Domain\Service; +use Neos\ContentRepository\Core\CommandHandler\Commands; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; use Neos\ContentRepository\Core\Feature\NodeCreation\Command\CreateNodeAggregateWithNode; @@ -22,12 +23,10 @@ use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\Exception\NodeAggregateCurrentlyDoesNotExist; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; -use Neos\ContentRepository\Core\SharedModel\Node\NodeName; use Neos\ContentRepository\Core\SharedModel\Node\ReferenceName; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Neos\Domain\Exception\TetheredNodesCannotBePartiallyCopied; -use Neos\Neos\Domain\Service\NodeDuplication\Commands; use Neos\Neos\Domain\Service\NodeDuplication\NodeAggregateIdMapping; use Neos\Neos\Domain\Service\NodeDuplication\TransientNodeCopy; From cb6b523ca234b9c4e06b966bac5a27fb2cfa10cc Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 18 Nov 2024 17:12:07 +0100 Subject: [PATCH 203/214] TASK: Document `NodeDuplicationService` --- .../Domain/Service/NodeDuplicationService.php | 40 ++++++++++++++++++- .../Bootstrap/NodeDuplicationTrait.php | 2 +- ...ode_ConstraintChecks_TetheredNodes.feature | 18 ++++----- 3 files changed, 48 insertions(+), 12 deletions(-) diff --git a/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php b/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php index 422178037df..ea7a4b8ddba 100644 --- a/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php +++ b/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php @@ -26,10 +26,17 @@ use Neos\ContentRepository\Core\SharedModel\Node\ReferenceName; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; +use Neos\Flow\Annotations as Flow; use Neos\Neos\Domain\Exception\TetheredNodesCannotBePartiallyCopied; use Neos\Neos\Domain\Service\NodeDuplication\NodeAggregateIdMapping; use Neos\Neos\Domain\Service\NodeDuplication\TransientNodeCopy; +/** + * Service to copy node recursively - as there is no equivalent content repository core command. + * + * @Flow\Scope("singleton") + * @api + */ final class NodeDuplicationService { public function __construct( @@ -37,6 +44,35 @@ public function __construct( ) { } + /** + * Copies the specified source node and its children to the target node + * + * Note about dimensions: + * --------------------- + * Currently the copying is primitive as that we take the read-model of the dimension to copy (the subgraph). and paste that into the target dimension. + * That means that the copy does not alter other dimensions and that virtual variants are materialised. + * For more information see {@link https://github.com/neos/neos-development-collection/issues/5054} + * + * Note about constraints: + * ---------------------- + * As we cannot rely on the full integrate on the subgraph regarding the current node type schema it might not be possible to copy a node and its children. + * For example copying a node with tethered children that is not tethered according to the current node type schema, or copying properties that are not defined + * in the current node type schema anymore. In those cases the structure adjustments have to be executed. (todo only copy what is applicable and be graceful) + * + * Note about partial copy on error: + * -------------------------------- + * As the above mentioned constraints can fail and we handle the determined content repository commands one by one, a failure will lead to a partially evaluated copy. + * The content repository is still consistent but the intent is only partially fulfilled. + * + * @param ContentRepositoryId $contentRepositoryId The content repository the copy operation is performed in + * @param WorkspaceName $workspaceName The name of the workspace where the node is copied and from and into (todo permit cross workspace copying?) + * @param DimensionSpacePoint $sourceDimensionSpacePoint The dimension to copy from + * @param NodeAggregateId $sourceNodeAggregateId The node aggregate which to copy (including its children) + * @param OriginDimensionSpacePoint $targetDimensionSpacePoint the dimension space point which is the target of the copy + * @param NodeAggregateId $targetParentNodeAggregateId Node aggregate id of the target node's parent. If not given, the node will be added as the parent's first child + * @param NodeAggregateId|null $targetSucceedingSiblingNodeAggregateId Node aggregate id of the target node's succeeding sibling (optional) + * @param NodeAggregateIdMapping|null $nodeAggregateIdMapping An assignment of "old" to "new" NodeAggregateIds + */ public function copyNodesRecursively( ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, @@ -45,7 +81,7 @@ public function copyNodesRecursively( OriginDimensionSpacePoint $targetDimensionSpacePoint, NodeAggregateId $targetParentNodeAggregateId, ?NodeAggregateId $targetSucceedingSiblingNodeAggregateId, - NodeAggregateIdMapping $nodeAggregateIdMapping + ?NodeAggregateIdMapping $nodeAggregateIdMapping = null ): void { $contentRepository = $this->contentRepositoryRegistry->get($contentRepositoryId); @@ -65,7 +101,7 @@ public function copyNodesRecursively( $subtree, $workspaceName, $targetDimensionSpacePoint, - $nodeAggregateIdMapping + $nodeAggregateIdMapping ?? NodeAggregateIdMapping::createEmpty() ); $createCopyOfNodeCommand = CreateNodeAggregateWithNode::create( diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/NodeDuplicationTrait.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/NodeDuplicationTrait.php index 9543e068913..2b3c00eb496 100644 --- a/Neos.Neos/Tests/Behavior/Features/Bootstrap/NodeDuplicationTrait.php +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/NodeDuplicationTrait.php @@ -74,7 +74,7 @@ public function copyNodesRecursivelyIsExecutedWithPayload(TableNode $payloadTabl $targetDimensionSpacePoint, NodeAggregateId::fromString($commandArguments['targetParentNodeAggregateId']), $targetSucceedingSiblingNodeAggregateId, - NodeAggregateIdMapping::fromArray($commandArguments['nodeAggregateIdMapping']) + NodeAggregateIdMapping::fromArray($commandArguments['nodeAggregateIdMapping'] ?? []) ) ); } diff --git a/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_ConstraintChecks_TetheredNodes.feature b/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_ConstraintChecks_TetheredNodes.feature index 83cb9354108..a19e34219cc 100644 --- a/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_ConstraintChecks_TetheredNodes.feature +++ b/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_ConstraintChecks_TetheredNodes.feature @@ -29,9 +29,10 @@ Feature: Copy nodes (without dimensions) | nodeAggregateId | "lady-eleonode-rootford" | | nodeTypeName | "Neos.ContentRepository:Root" | And the following CreateNodeAggregateWithNode commands are executed: - | nodeAggregateId | parentNodeAggregateId | nodeTypeName | tetheredDescendantNodeAggregateIds | - | node-mc-nodeface | lady-eleonode-rootford | Neos.ContentRepository.Testing:DocumentWithoutTetheredChildren | {} | - | node-wan-kenody | lady-eleonode-rootford | Neos.ContentRepository.Testing:Document | {"tethered-document": "nodewyn-tetherton", "tethered-document/tethered": "nodimer-tetherton"} | + | nodeAggregateId | parentNodeAggregateId | nodeTypeName | tetheredDescendantNodeAggregateIds | + | node-mc-nodeface | lady-eleonode-rootford | Neos.ContentRepository.Testing:DocumentWithoutTetheredChildren | {} | + | node-wan-kenody | lady-eleonode-rootford | Neos.ContentRepository.Testing:Document | {"tethered-document": "nodewyn-tetherton", "tethered-document/tethered": "nodimer-tetherton"} | + | sir-david-nodenburg | lady-eleonode-rootford | Neos.ContentRepository.Testing:TetheredDocument | {"tethered": "davids-tether"} | Scenario: Coping fails if the leaf of a nested tethered node is attempted to be copied And I expect the node aggregate "nodewyn-tetherton" to exist @@ -41,12 +42,11 @@ Feature: Copy nodes (without dimensions) And I expect this node aggregate to be classified as "tethered" When copy nodes recursively is executed with payload: - | Key | Value | - | sourceDimensionSpacePoint | {} | - | sourceNodeAggregateId | "nodewyn-tetherton" | - | targetDimensionSpacePoint | {} | - | targetParentNodeAggregateId | "node-mc-nodeface" | - | nodeAggregateIdMapping | {"nodewyn-tetherton": "nodewyn-tetherton-copy", "nodimer-tetherton": "nodimer-tetherton-copy"} | + | Key | Value | + | sourceDimensionSpacePoint | {} | + | sourceNodeAggregateId | "nodewyn-tetherton" | + | targetDimensionSpacePoint | {} | + | targetParentNodeAggregateId | "node-mc-nodeface" | Then an exception of type TetheredNodesCannotBePartiallyCopied should be thrown with message: """ From 8d14bae0234006ec0eccde9018154b1ba036e0c1 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 18 Nov 2024 17:39:22 +0100 Subject: [PATCH 204/214] FEATURE: Copy subtree tags during copy --- .../Domain/Service/NodeDuplicationService.php | 52 +++++++++++--- .../NodeCopying/CopyNode_NoDimensions.feature | 70 ++++++++++++++++++- 2 files changed, 113 insertions(+), 9 deletions(-) diff --git a/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php b/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php index ea7a4b8ddba..5edb2b78bdf 100644 --- a/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php +++ b/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php @@ -14,6 +14,7 @@ use Neos\ContentRepository\Core\Feature\NodeReferencing\Dto\NodeReferencesForName; use Neos\ContentRepository\Core\Feature\NodeReferencing\Dto\NodeReferencesToWrite; use Neos\ContentRepository\Core\Feature\NodeReferencing\Dto\NodeReferenceToWrite; +use Neos\ContentRepository\Core\Feature\SubtreeTagging\Command\TagSubtree; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentSubgraphInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindReferencesFilter; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindSubtreeFilter; @@ -23,6 +24,7 @@ use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\Exception\NodeAggregateCurrentlyDoesNotExist; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; +use Neos\ContentRepository\Core\SharedModel\Node\NodeVariantSelectionStrategy; use Neos\ContentRepository\Core\SharedModel\Node\ReferenceName; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; @@ -105,7 +107,7 @@ public function copyNodesRecursively( ); $createCopyOfNodeCommand = CreateNodeAggregateWithNode::create( - $workspaceName, + $transientNodeCopy->workspaceName, $transientNodeCopy->aggregateId, $subtree->node->nodeTypeName, $targetDimensionSpacePoint, @@ -126,6 +128,18 @@ public function copyNodesRecursively( $commands = Commands::create($createCopyOfNodeCommand); + foreach ($subtree->node->tags->withoutInherited() as $explicitTag) { + $commands = $commands->append( + TagSubtree::create( + $transientNodeCopy->workspaceName, + $transientNodeCopy->aggregateId, + $transientNodeCopy->originDimensionSpacePoint->toDimensionSpacePoint(), + NodeVariantSelectionStrategy::STRATEGY_ALL_VARIANTS, + $explicitTag + ) + ); + } + foreach ($subtree->children as $childSubtree) { if ($subtree->node->classification->isTethered() && $childSubtree->node->classification->isTethered()) { // TODO we assume here that the child node is tethered because the grandparent specifies that. @@ -143,14 +157,17 @@ public function copyNodesRecursively( private function commandsForSubtreeRecursively(TransientNodeCopy $transientParentNode, Subtree $subtree, ContentSubgraphInterface $subgraph, Commands $commands): Commands { if ($subtree->node->classification->isTethered()) { - $transientNode = $transientParentNode->forTetheredChildNode( + /** + * Case Node is tethered + */ + $transientNodeCopy = $transientParentNode->forTetheredChildNode( $subtree ); if ($subtree->node->properties->count() > 0) { $setPropertiesOfTetheredNodeCommand = SetNodeProperties::create( $transientParentNode->workspaceName, - $transientNode->aggregateId, + $transientNodeCopy->aggregateId, $transientParentNode->originDimensionSpacePoint, // todo skip properties not in schema PropertyValuesToWrite::fromArray( @@ -165,7 +182,7 @@ private function commandsForSubtreeRecursively(TransientNodeCopy $transientParen if ($references->count() > 0) { $setReferencesOfTetheredNodeCommand = SetNodeReferences::create( $transientParentNode->workspaceName, - $transientNode->aggregateId, + $transientNodeCopy->aggregateId, $transientParentNode->originDimensionSpacePoint, $this->serializeProjectedReferences( $references @@ -175,13 +192,16 @@ private function commandsForSubtreeRecursively(TransientNodeCopy $transientParen $commands = $commands->append($setReferencesOfTetheredNodeCommand); } } else { - $transientNode = $transientParentNode->forRegularChildNode( + /** + * Case Node is a regular child + */ + $transientNodeCopy = $transientParentNode->forRegularChildNode( $subtree ); $createCopyOfNodeCommand = CreateNodeAggregateWithNode::create( $transientParentNode->workspaceName, - $transientNode->aggregateId, + $transientNodeCopy->aggregateId, $subtree->node->nodeTypeName, $transientParentNode->originDimensionSpacePoint, $transientParentNode->aggregateId, @@ -196,14 +216,30 @@ private function commandsForSubtreeRecursively(TransientNodeCopy $transientParen ); $createCopyOfNodeCommand = $createCopyOfNodeCommand->withTetheredDescendantNodeAggregateIds( - $transientNode->tetheredNodeAggregateIds + $transientNodeCopy->tetheredNodeAggregateIds ); $commands = $commands->append($createCopyOfNodeCommand); } + /** + * common logic + */ + + foreach ($subtree->node->tags->withoutInherited() as $explicitTag) { + $commands = $commands->append( + TagSubtree::create( + $transientNodeCopy->workspaceName, + $transientNodeCopy->aggregateId, + $transientNodeCopy->originDimensionSpacePoint->toDimensionSpacePoint(), + NodeVariantSelectionStrategy::STRATEGY_ALL_VARIANTS, + $explicitTag + ) + ); + } + foreach ($subtree->children as $childSubtree) { - $commands = $this->commandsForSubtreeRecursively($transientNode, $childSubtree, $subgraph, $commands); + $commands = $this->commandsForSubtreeRecursively($transientNodeCopy, $childSubtree, $subgraph, $commands); } return $commands; diff --git a/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_NoDimensions.feature b/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_NoDimensions.feature index d09d3c73f68..43ca625d22d 100644 --- a/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_NoDimensions.feature +++ b/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_NoDimensions.feature @@ -34,7 +34,8 @@ Feature: Copy nodes (without dimensions) | nodeAggregateId | parentNodeAggregateId | nodeTypeName | | sir-david-nodenborough | lady-eleonode-rootford | Neos.ContentRepository.Testing:Document | | nody-mc-nodeface | sir-david-nodenborough | Neos.ContentRepository.Testing:Document | - | sir-nodeward-nodington-iii | lady-eleonode-rootford | Neos.ContentRepository.Testing:Document | + | node-wan-kenodi | lady-eleonode-rootford | Neos.ContentRepository.Testing:Document | + | sir-nodeward-nodington-iii | node-wan-kenodi | Neos.ContentRepository.Testing:Document | Scenario: Simple singular node aggregate is copied When I am in workspace "live" and dimension space point {} @@ -102,6 +103,49 @@ Feature: Copy nodes (without dimensions) | Name | Node | Properties | | ref | cs-identifier;sir-david-nodenborough;{} | null | + Scenario: Singular node aggregate is copied with subtree tags (and disabled state) + When I am in workspace "live" and dimension space point {} + + Given the command DisableNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-nodeward-nodington-iii" | + | coveredDimensionSpacePoint | {} | + | nodeVariantSelectionStrategy | "allVariants" | + + Given the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-nodeward-nodington-iii" | + | coveredDimensionSpacePoint | {} | + | nodeVariantSelectionStrategy | "allVariants" | + | tag | "tag1" | + + Given the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "node-wan-kenodi" | + | coveredDimensionSpacePoint | {} | + | nodeVariantSelectionStrategy | "allVariants" | + | tag | "parent-tag" | + + And VisibilityConstraints are set to "withoutRestrictions" + + # we inherit the tag here but DONT copy it! + Then I expect the node with aggregate identifier "sir-nodeward-nodington-iii" to inherit the tag "parent-tag" + + When copy nodes recursively is executed with payload: + | Key | Value | + | sourceDimensionSpacePoint | {} | + | sourceNodeAggregateId | "sir-nodeward-nodington-iii" | + | targetDimensionSpacePoint | {} | + | targetParentNodeAggregateId | "nody-mc-nodeface" | + | targetSucceedingSiblingnodeAggregateId | null | + | nodeAggregateIdMapping | {"sir-nodeward-nodington-iii": "sir-nodeward-nodington-iii-copy"} | + + And I expect the node aggregate "sir-nodeward-nodington-iii-copy" to exist + And I expect this node aggregate to disable dimension space points [{}] + + And I expect node aggregate identifier "sir-nodeward-nodington-iii-copy" to lead to node cs-identifier;sir-nodeward-nodington-iii-copy;{} + And I expect this node to be exactly explicitly tagged "disabled,tag1" + Scenario: Node aggregate is copied children recursively When I am in workspace "live" and dimension space point {} When the following CreateNodeAggregateWithNode commands are executed: @@ -151,3 +195,27 @@ Feature: Copy nodes (without dimensions) And I expect this node to have the following references: | Name | Node | Properties | | ref | cs-identifier;sir-david-nodenborough;{} | null | + + Scenario: Subtree tags are copied for child nodes + When I am in workspace "live" and dimension space point {} + When the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | parentNodeAggregateId | nodeTypeName | + | child-a | sir-nodeward-nodington-iii | Neos.ContentRepository.Testing:Document | + + Given the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "child-a" | + | coveredDimensionSpacePoint | {} | + | nodeVariantSelectionStrategy | "allVariants" | + | tag | "tag1" | + + When copy nodes recursively is executed with payload: + | Key | Value | + | sourceDimensionSpacePoint | {} | + | sourceNodeAggregateId | "sir-nodeward-nodington-iii" | + | targetDimensionSpacePoint | {} | + | targetParentNodeAggregateId | "nody-mc-nodeface" | + | nodeAggregateIdMapping | {"sir-nodeward-nodington-iii": "sir-nodeward-nodington-iii-copy", "child-a": "child-a-copy"} | + + And I expect node aggregate identifier "child-a-copy" to lead to node cs-identifier;child-a-copy;{} + And I expect this node to be exactly explicitly tagged "tag1" From fdea0c633c237cb4e1f387ec62eaabdb80a9d33a Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 18 Nov 2024 17:57:50 +0100 Subject: [PATCH 205/214] BUGFIX: Migration to fix previously copied tethered nodes #5350 In case a leaf node that is tethered is copied, we un-tether it. A migration `flow migrateevents:migratecopytetherednode` fixes this for previous cases. --- .../MigrateEventsCommandController.php | 16 ++++++ .../Classes/Service/EventMigrationService.php | 49 +++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/Neos.ContentRepositoryRegistry/Classes/Command/MigrateEventsCommandController.php b/Neos.ContentRepositoryRegistry/Classes/Command/MigrateEventsCommandController.php index 56437a286c6..6432eb979b4 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Command/MigrateEventsCommandController.php +++ b/Neos.ContentRepositoryRegistry/Classes/Command/MigrateEventsCommandController.php @@ -133,4 +133,20 @@ public function reorderNodeAggregateWasRemovedCommand(string $contentRepository $eventMigrationService = $this->contentRepositoryRegistry->buildService($contentRepositoryId, $this->eventMigrationServiceFactory); $eventMigrationService->reorderNodeAggregateWasRemoved($this->outputLine(...)); } + + /** + * Migrates "nodeAggregateClassification":"tethered" to "regular", in case for copied tethered nodes. + * + * Needed for #5350: https://github.com/neos/neos-development-collection/issues/5350 + * + * Included in November 2024 - before final Neos 9.0 release + * + * @param string $contentRepository Identifier of the Content Repository to migrate + */ + public function migrateCopyTetheredNodeCommand(string $contentRepository = 'default'): void + { + $contentRepositoryId = ContentRepositoryId::fromString($contentRepository); + $eventMigrationService = $this->contentRepositoryRegistry->buildService($contentRepositoryId, $this->eventMigrationServiceFactory); + $eventMigrationService->migrateCopyTetheredNode($this->outputLine(...)); + } } diff --git a/Neos.ContentRepositoryRegistry/Classes/Service/EventMigrationService.php b/Neos.ContentRepositoryRegistry/Classes/Service/EventMigrationService.php index be7cfcc6292..d2880126d70 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Service/EventMigrationService.php +++ b/Neos.ContentRepositoryRegistry/Classes/Service/EventMigrationService.php @@ -25,6 +25,7 @@ use Neos\ContentRepository\Core\Feature\RootNodeCreation\Command\UpdateRootNodeAggregateDimensions; use Neos\ContentRepository\Core\Feature\WorkspaceEventStreamName; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateClassification; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepositoryRegistry\Command\MigrateEventsCommandController; @@ -810,6 +811,54 @@ public function reorderNodeAggregateWasRemoved(\Closure $outputFn): void $outputFn(sprintf('Reordered %d removals. Please replay and rebase your other workspaces.', count($eventsToReorder))); } + public function migrateCopyTetheredNode(\Closure $outputFn): void + { + $this->eventsModified = []; + + $backupEventTableName = DoctrineEventStoreFactory::databaseTableName($this->contentRepositoryId) + . '_bkp_' . date('Y_m_d_H_i_s'); + $outputFn(sprintf('Backup: copying events table to %s', $backupEventTableName)); + + $this->copyEventTable($backupEventTableName); + + $streamName = VirtualStreamName::all(); + $eventStream = $this->eventStore->load($streamName, EventStreamFilter::create(EventTypes::create(EventType::fromString('NodeAggregateWithNodeWasCreated')))); + foreach ($eventStream as $eventEnvelope) { + $outputRewriteNotice = fn(string $message) => $outputFn(sprintf('%s@%s %s', $eventEnvelope->sequenceNumber->value, $eventEnvelope->event->type->value, $message)); + if ($eventEnvelope->event->type->value !== 'NodeAggregateWithNodeWasCreated') { + throw new \RuntimeException(sprintf('Unhandled event: %s', $eventEnvelope->event->type->value)); + } + + $eventMetaData = $eventEnvelope->event->metadata?->value; + // a copy is basically a NodeAggregateWithNodeWasCreated with CopyNodesRecursively command, so we skip others: + if (!$eventMetaData || ($eventMetaData['commandClass'] ?? null) !== CopyNodesRecursively::class) { + continue; + } + + $eventData = self::decodeEventPayload($eventEnvelope); + if ($eventData['nodeAggregateClassification'] !== NodeAggregateClassification::CLASSIFICATION_TETHERED->value) { + // this copy is okay + continue; + } + + $eventData['nodeAggregateClassification'] = NodeAggregateClassification::CLASSIFICATION_REGULAR->value; + $this->updateEventPayload($eventEnvelope->sequenceNumber, $eventData); + + $eventMetaData['commandPayload']['nodeTreeToInsert']['nodeAggregateClassification'] = NodeAggregateClassification::CLASSIFICATION_REGULAR->value; + + $this->updateEventMetaData($eventEnvelope->sequenceNumber, $eventMetaData); + $outputRewriteNotice(sprintf('Copied tethered node "%s" of type "%s" (name: %s) was migrated', $eventData['nodeAggregateId'], $eventData['nodeTypeName'], json_encode($eventData['nodeName']))); + } + + if (!count($this->eventsModified)) { + $outputFn('Migration was not necessary.'); + return; + } + + $outputFn(); + $outputFn(sprintf('Migration applied to %s events. Please replay the projections `./flow cr:projectionReplayAll`', count($this->eventsModified))); + } + /** ------------------------ */ /** From 44c18dcb11c55ef9d435b2830cb37d3ddefb69bf Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 18 Nov 2024 19:07:43 +0100 Subject: [PATCH 206/214] TASK: Introduce `migrateevents:copyNodesStatus` --- .../MigrateEventsCommandController.php | 21 +++++++++- .../Classes/Service/EventMigrationService.php | 39 +++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/Neos.ContentRepositoryRegistry/Classes/Command/MigrateEventsCommandController.php b/Neos.ContentRepositoryRegistry/Classes/Command/MigrateEventsCommandController.php index 6432eb979b4..01cbc8ccf87 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Command/MigrateEventsCommandController.php +++ b/Neos.ContentRepositoryRegistry/Classes/Command/MigrateEventsCommandController.php @@ -100,7 +100,7 @@ public function migratePayloadToWorkspaceNameCommand(string $contentRepository = * Needed for feature "Stabilize WorkspaceName value object": https://github.com/neos/neos-development-collection/pull/5193 * * Included in August 2024 - before final Neos 9.0 release - * + * * @param string $contentRepository Identifier of the Content Repository to migrate */ public function migratePayloadToValidWorkspaceNamesCommand(string $contentRepository = 'default'): void @@ -149,4 +149,23 @@ public function migrateCopyTetheredNodeCommand(string $contentRepository = 'defa $eventMigrationService = $this->contentRepositoryRegistry->buildService($contentRepositoryId, $this->eventMigrationServiceFactory); $eventMigrationService->migrateCopyTetheredNode($this->outputLine(...)); } + + /** + * Status information if content streams still contain legacy copy node events + * + * Needed for #5371: https://github.com/neos/neos-development-collection/pull/5371 + * + * Included in November 2024 - before final Neos 9.0 release + * + * NOTE: To reduce the number of matched content streams and to cleanup the event store run + * `./flow contentStream:removeDangling` and `./flow contentStream:pruneRemovedFromEventStream` + * + * @param string $contentRepository Identifier of the Content Repository to check + */ + public function copyNodesStatusCommand(string $contentRepository = 'default'): void + { + $contentRepositoryId = ContentRepositoryId::fromString($contentRepository); + $eventMigrationService = $this->contentRepositoryRegistry->buildService($contentRepositoryId, $this->eventMigrationServiceFactory); + $eventMigrationService->copyNodesStatus($this->outputLine(...)); + } } diff --git a/Neos.ContentRepositoryRegistry/Classes/Service/EventMigrationService.php b/Neos.ContentRepositoryRegistry/Classes/Service/EventMigrationService.php index d2880126d70..1e7364dcee2 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Service/EventMigrationService.php +++ b/Neos.ContentRepositoryRegistry/Classes/Service/EventMigrationService.php @@ -859,6 +859,45 @@ public function migrateCopyTetheredNode(\Closure $outputFn): void $outputFn(sprintf('Migration applied to %s events. Please replay the projections `./flow cr:projectionReplayAll`', count($this->eventsModified))); } + public function copyNodesStatus(\Closure $outputFn): void + { + $unpublishedCopyNodesInWorkspaces = []; + + $streamName = VirtualStreamName::all(); + $eventStream = $this->eventStore->load($streamName, EventStreamFilter::create(EventTypes::create(EventType::fromString('NodeAggregateWithNodeWasCreated')))); + foreach ($eventStream as $eventEnvelope) { + $eventMetaData = $eventEnvelope->event->metadata?->value; + // a copy is basically a NodeAggregateWithNodeWasCreated with CopyNodesRecursively command, so we skip others: + if (!$eventMetaData || ($eventMetaData['commandClass'] ?? null) !== CopyNodesRecursively::class) { + continue; + } + + $eventData = self::decodeEventPayload($eventEnvelope); + + if ($eventData['workspaceName'] !== 'live') { + $unpublishedCopyNodesInWorkspaces[$eventData['contentStreamId'] . ' (' . $eventData['workspaceName'] . ')'][] = sprintf( + '@%s copy node %s to %s', + $eventEnvelope->sequenceNumber->value, + $eventMetaData['commandPayload']['nodeTreeToInsert']['nodeAggregateId'], + $eventMetaData['commandPayload']['targetParentNodeAggregateId'] + ); + } + } + + if ($unpublishedCopyNodesInWorkspaces === []) { + $outputFn('Everything regarding copy nodes okay.'); + return; + } + $outputFn('WARNING: %d content streams contain unpublished legacy copy node events. They MUST be published before migrating to Neos 9 (stable) and will not be publishable afterward.', [count($unpublishedCopyNodesInWorkspaces)]); + foreach ($unpublishedCopyNodesInWorkspaces as $contentStream => $unpublishedCopyNodesInWorkspace) { + $outputFn('Content stream %s', [$contentStream]); + foreach ($unpublishedCopyNodesInWorkspace as $unpublishedCopyNode) { + $outputFn(' - %s', [$unpublishedCopyNode]); + } + } + $outputFn('NOTE: To reduce the number of matched content streams and to cleanup the event store run `./flow contentStream:removeDangling` and `./flow contentStream:pruneRemovedFromEventStream`'); + } + /** ------------------------ */ /** From 8ae575589219b902ca5279bac709d31f52fd5da0 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Tue, 19 Nov 2024 09:47:10 +0100 Subject: [PATCH 207/214] TASK: Introduce `Subtrees::createEmpty` --- .../src/Domain/Repository/ContentSubgraph.php | 6 +++++- .../src/Domain/Repository/NodeFactory.php | 6 +++++- .../Classes/Projection/ContentGraph/Subtrees.php | 4 ++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php index 72968a19dd9..b6b760811e6 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php @@ -305,7 +305,11 @@ public function findSubtree(NodeAggregateId $entryNodeAggregateId, FindSubtreeFi $this->dimensionSpacePoint, $this->visibilityConstraints ); - $subtree = Subtree::create((int)$nodeData['level'], $node, Subtrees::fromArray(array_key_exists($nodeAggregateId, $subtreesByParentNodeId) ? array_reverse($subtreesByParentNodeId[$nodeAggregateId]) : [])); + $subtree = Subtree::create( + (int)$nodeData['level'], + $node, + array_key_exists($nodeAggregateId, $subtreesByParentNodeId) ? Subtrees::fromArray(array_reverse($subtreesByParentNodeId[$nodeAggregateId])) : Subtrees::createEmpty() + ); if ($subtree->level === 0) { return $subtree; } diff --git a/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/NodeFactory.php b/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/NodeFactory.php index fa05b84f193..e991c81d29e 100644 --- a/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/NodeFactory.php +++ b/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/NodeFactory.php @@ -155,7 +155,11 @@ public function mapNodeRowsToSubtree( $nodeAggregateId = $nodeRow['nodeaggregateid']; $parentNodeAggregateId = $nodeRow['parentnodeaggregateid']; $node = $this->mapNodeRowToNode($nodeRow, $visibilityConstraints); - $subtree = Subtree::create((int)$nodeRow['level'], $node, Subtrees::fromArray(array_key_exists($nodeAggregateId, $subtreesByParentNodeId) ? array_reverse($subtreesByParentNodeId[$nodeAggregateId]) : [])); + $subtree = Subtree::create( + (int)$nodeRow['level'], + $node, + array_key_exists($nodeAggregateId, $subtreesByParentNodeId) ? Subtrees::fromArray(array_reverse($subtreesByParentNodeId[$nodeAggregateId])) : Subtrees::createEmpty() + ); if ($subtree->level === 0) { return $subtree; } diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Subtrees.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Subtrees.php index 644d47b0c02..2a946946b82 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Subtrees.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Subtrees.php @@ -22,9 +22,9 @@ private function __construct( /** * @internal */ - public static function create(Subtree ...$items): self + public static function createEmpty(): self { - return new self(...$items); + return new self(); } /** From 38f336dbf67f603d014e7fa021f896697de3ed5c Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Tue, 19 Nov 2024 09:49:16 +0100 Subject: [PATCH 208/214] TASK: Adjust to new behat gherkin version to use quadruple back-slash --- .../NodeCopying/CopyNode_NoDimensions.feature | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_NoDimensions.feature b/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_NoDimensions.feature index 43ca625d22d..f061b789a00 100644 --- a/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_NoDimensions.feature +++ b/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_NoDimensions.feature @@ -61,9 +61,9 @@ Feature: Copy nodes (without dimensions) Scenario: Singular node aggregate is copied with (complex) properties When I am in workspace "live" and dimension space point {} And the command SetNodeProperties is executed with payload: - | Key | Value | - | nodeAggregateId | "sir-nodeward-nodington-iii" | - | propertyValues | {"title": "Original Text", "array": {"givenName":"Nody", "familyName":"McNodeface"}, "uri": {"__type": "GuzzleHttp\\Psr7\\Uri", "value": "https://neos.de"}, "date": {"__type": "DateTimeImmutable", "value": "2001-09-22T12:00:00+00:00"}} | + | Key | Value | + | nodeAggregateId | "sir-nodeward-nodington-iii" | + | propertyValues | {"title": "Original Text", "array": {"givenName":"Nody", "familyName":"McNodeface"}, "uri": {"__type": "GuzzleHttp\\\\Psr7\\\\Uri", "value": "https://neos.de"}, "date": {"__type": "DateTimeImmutable", "value": "2001-09-22T12:00:00+00:00"}} | When copy nodes recursively is executed with payload: | Key | Value | From 3de7d043a95b0fa52ab016fb84b8f166872d1b1b Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Tue, 19 Nov 2024 10:06:21 +0100 Subject: [PATCH 209/214] TASK: Add test for simple constraint checks --- .../Domain/Service/NodeDuplicationService.php | 4 +- ...Node_ConstraintChecks_NoDimensions.feature | 55 +++++++++++++++++++ ...ode_ConstraintChecks_TetheredNodes.feature | 4 +- 3 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_ConstraintChecks_NoDimensions.feature diff --git a/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php b/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php index 5edb2b78bdf..48729415779 100644 --- a/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php +++ b/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php @@ -91,12 +91,12 @@ public function copyNodesRecursively( $targetParentNode = $subgraph->findNodeById($targetParentNodeAggregateId); if ($targetParentNode === null) { - throw new NodeAggregateCurrentlyDoesNotExist(sprintf('The target parent node aggregate "%s" does not exist', $targetParentNodeAggregateId->value)); + throw new NodeAggregateCurrentlyDoesNotExist(sprintf('The target parent node aggregate "%s" does not exist', $targetParentNodeAggregateId->value), 1732006769); } $subtree = $subgraph->findSubtree($sourceNodeAggregateId, FindSubtreeFilter::create()); if ($subtree === null) { - throw new NodeAggregateCurrentlyDoesNotExist(sprintf('The source node aggregate "%s" does not exist', $sourceNodeAggregateId->value)); + throw new NodeAggregateCurrentlyDoesNotExist(sprintf('The source node aggregate "%s" does not exist', $sourceNodeAggregateId->value), 1732006772); } $transientNodeCopy = TransientNodeCopy::forEntry( diff --git a/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_ConstraintChecks_NoDimensions.feature b/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_ConstraintChecks_NoDimensions.feature new file mode 100644 index 00000000000..edfdda081ca --- /dev/null +++ b/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_ConstraintChecks_NoDimensions.feature @@ -0,0 +1,55 @@ +@contentrepository +Feature: Copy nodes constraint checks + + Background: + Given using no content dimensions + And using the following node types: + """yaml + 'Neos.ContentRepository.Testing:Document': {} + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + And the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | newContentStreamId | "cs-identifier" | + And I am in workspace "live" + And the command CreateRootNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "lady-eleonode-rootford" | + | nodeTypeName | "Neos.ContentRepository:Root" | + + When the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | parentNodeAggregateId | nodeTypeName | + | nody-mc-nodeface | lady-eleonode-rootford | Neos.ContentRepository.Testing:Document | + | sir-nodeward-nodington-iii | lady-eleonode-rootford | Neos.ContentRepository.Testing:Document | + + Scenario: Coping fails if the source node does not exist + When copy nodes recursively is executed with payload: + | Key | Value | + | sourceDimensionSpacePoint | {} | + | sourceNodeAggregateId | "not-existing" | + | targetDimensionSpacePoint | {} | + | targetParentNodeAggregateId | "nody-mc-nodeface" | + + Then an exception of type NodeAggregateCurrentlyDoesNotExist should be thrown with code 1732006772 + + Scenario: Coping fails if the target node does not exist + When copy nodes recursively is executed with payload: + | Key | Value | + | sourceDimensionSpacePoint | {} | + | sourceNodeAggregateId | "nody-mc-nodeface" | + | targetDimensionSpacePoint | {} | + | targetParentNodeAggregateId | "not-existing" | + + Then an exception of type NodeAggregateCurrentlyDoesNotExist should be thrown with code 1732006769 + + Scenario: Coping fails if the source node is a root + When copy nodes recursively is executed with payload: + | Key | Value | + | sourceDimensionSpacePoint | {} | + | sourceNodeAggregateId | "lady-eleonode-rootford" | + | targetDimensionSpacePoint | {} | + | targetParentNodeAggregateId | "nody-mc-nodeface" | + + Then an exception of type NodeTypeIsOfTypeRoot should be thrown with code 1541765806 diff --git a/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_ConstraintChecks_TetheredNodes.feature b/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_ConstraintChecks_TetheredNodes.feature index a19e34219cc..4bb4ea277c7 100644 --- a/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_ConstraintChecks_TetheredNodes.feature +++ b/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_ConstraintChecks_TetheredNodes.feature @@ -1,5 +1,5 @@ -@contentrepository @adapters=DoctrineDBAL -Feature: Copy nodes (without dimensions) +@contentrepository +Feature: Copy nodes constraint checks Background: Given using no content dimensions From 85230100dc75d3a28dac8b2f938739d764d80204 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Tue, 19 Nov 2024 10:18:43 +0100 Subject: [PATCH 210/214] TASK: Introduce internal `calculateCopyNodesRecursively` to support usecases like https://github.com/Flowpack/Flowpack.NodeTemplates/issues/60 were we need to return a list of commands and cant handle them ourselves, it would be practical to expose a way to get the commands that would be handled --- .../Domain/Service/NodeDuplicationService.php | 47 ++++++++++++++----- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php b/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php index 48729415779..dbd7d21460e 100644 --- a/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php +++ b/Neos.Neos/Classes/Domain/Service/NodeDuplicationService.php @@ -99,26 +99,53 @@ public function copyNodesRecursively( throw new NodeAggregateCurrentlyDoesNotExist(sprintf('The source node aggregate "%s" does not exist', $sourceNodeAggregateId->value), 1732006772); } - $transientNodeCopy = TransientNodeCopy::forEntry( + $commands = $this->calculateCopyNodesRecursively( $subtree, + $subgraph, $workspaceName, $targetDimensionSpacePoint, + $targetParentNodeAggregateId, + $targetSucceedingSiblingNodeAggregateId, + $nodeAggregateIdMapping + ); + + foreach ($commands as $command) { + $contentRepository->handle($command); + } + } + + /** + * @internal implementation detail of {@see NodeDuplicationService::copyNodesRecursively}, exposed for EXPERIMENTAL use cases, the API can change any time! + */ + public function calculateCopyNodesRecursively( + Subtree $subtreeToCopy, + ContentSubgraphInterface $subgraph, + WorkspaceName $targetWorkspaceName, + OriginDimensionSpacePoint $targetDimensionSpacePoint, + NodeAggregateId $targetParentNodeAggregateId, + ?NodeAggregateId $targetSucceedingSiblingNodeAggregateId, + ?NodeAggregateIdMapping $nodeAggregateIdMapping = null + ): Commands { + $transientNodeCopy = TransientNodeCopy::forEntry( + $subtreeToCopy, + $targetWorkspaceName, + $targetDimensionSpacePoint, $nodeAggregateIdMapping ?? NodeAggregateIdMapping::createEmpty() ); $createCopyOfNodeCommand = CreateNodeAggregateWithNode::create( $transientNodeCopy->workspaceName, $transientNodeCopy->aggregateId, - $subtree->node->nodeTypeName, + $subtreeToCopy->node->nodeTypeName, $targetDimensionSpacePoint, $targetParentNodeAggregateId, succeedingSiblingNodeAggregateId: $targetSucceedingSiblingNodeAggregateId, // todo skip properties not in schema initialPropertyValues: PropertyValuesToWrite::fromArray( - iterator_to_array($subtree->node->properties) + iterator_to_array($subtreeToCopy->node->properties) ), references: $this->serializeProjectedReferences( - $subgraph->findReferences($subtree->node->aggregateId, FindReferencesFilter::create()) + $subgraph->findReferences($subtreeToCopy->node->aggregateId, FindReferencesFilter::create()) ) ); @@ -128,7 +155,7 @@ public function copyNodesRecursively( $commands = Commands::create($createCopyOfNodeCommand); - foreach ($subtree->node->tags->withoutInherited() as $explicitTag) { + foreach ($subtreeToCopy->node->tags->withoutInherited() as $explicitTag) { $commands = $commands->append( TagSubtree::create( $transientNodeCopy->workspaceName, @@ -140,18 +167,16 @@ public function copyNodesRecursively( ); } - foreach ($subtree->children as $childSubtree) { - if ($subtree->node->classification->isTethered() && $childSubtree->node->classification->isTethered()) { + foreach ($subtreeToCopy->children as $childSubtree) { + if ($subtreeToCopy->node->classification->isTethered() && $childSubtree->node->classification->isTethered()) { // TODO we assume here that the child node is tethered because the grandparent specifies that. // this is not always fully correct and we could loosen the constraint by checking the node type schema - throw new TetheredNodesCannotBePartiallyCopied(sprintf('Cannot copy tethered node %s because child node %s is also tethered. Only standalone tethered nodes can be copied.', $subtree->node->aggregateId->value, $childSubtree->node->aggregateId->value), 1731264887); + throw new TetheredNodesCannotBePartiallyCopied(sprintf('Cannot copy tethered node %s because child node %s is also tethered. Only standalone tethered nodes can be copied.', $subtreeToCopy->node->aggregateId->value, $childSubtree->node->aggregateId->value), 1731264887); } $commands = $this->commandsForSubtreeRecursively($transientNodeCopy, $childSubtree, $subgraph, $commands); } - foreach ($commands as $command) { - $contentRepository->handle($command); - } + return $commands; } private function commandsForSubtreeRecursively(TransientNodeCopy $transientParentNode, Subtree $subtree, ContentSubgraphInterface $subgraph, Commands $commands): Commands From 79794dbe2466504dc9cecaa80eb76df3d5130011 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Tue, 19 Nov 2024 11:46:44 +0100 Subject: [PATCH 211/214] TASK: Rename `NodePath::forRoot` to `NodePath::createEmpty()` Because ROOT paths are only a concept of `AbsoluteNodePath` and not by a singular `NodePath` itself. --- .../Projection/ContentGraph/AbsoluteNodePath.php | 4 ++-- .../Classes/Projection/ContentGraph/NodePath.php | 10 ++++++---- .../Unit/Projection/ContentGraph/NodePathTest.php | 8 ++++---- .../Service/NodeDuplication/NodeAggregateIdMapping.php | 2 +- .../Service/NodeDuplication/TransientNodeCopy.php | 6 +++--- 5 files changed, 16 insertions(+), 14 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/AbsoluteNodePath.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/AbsoluteNodePath.php index abb0858233e..3bae2f7828f 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/AbsoluteNodePath.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/AbsoluteNodePath.php @@ -70,7 +70,7 @@ public static function fromRootNodeTypeNameAndRelativePath( public static function fromLeafNodeAndAncestors(Node $leafNode, Nodes $ancestors): self { if ($leafNode->classification->isRoot()) { - return new self($leafNode->nodeTypeName, NodePath::forRoot()); + return new self($leafNode->nodeTypeName, NodePath::createEmpty()); } $rootNode = $ancestors->first(); if (!$rootNode || !$rootNode->classification->isRoot()) { @@ -147,7 +147,7 @@ public function appendPathSegment(NodeName $nodeName): self */ public function isRoot(): bool { - return $this->path->isRoot(); + return $this->path->isEmpty(); } /** diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/NodePath.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/NodePath.php index 6ef34fe7d8a..43be4c36620 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/NodePath.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/NodePath.php @@ -17,7 +17,9 @@ use Neos\ContentRepository\Core\SharedModel\Node\NodeName; /** - * The relative node path is a collection of node names {@see NodeName}. If it contains no elements, it is considered root. + * The relative node path is a collection of node names {@see NodeName}. + * + * If it contains no elements, it is considered root in combination with {@see AbsoluteNodePath}. * * Example: * root path: '' is resolved to [] @@ -48,7 +50,7 @@ private function __construct(NodeName ...$nodeNames) $this->nodeNames = $nodeNames; } - public static function forRoot(): self + public static function createEmpty(): self { return new self(); } @@ -57,7 +59,7 @@ public static function fromString(string $path): self { $path = ltrim($path, '/'); if ($path === '') { - return self::forRoot(); + return self::createEmpty(); } return self::fromPathSegments( @@ -92,7 +94,7 @@ public static function fromNodeNames(NodeName ...$nodeNames): self return new self(...$nodeNames); } - public function isRoot(): bool + public function isEmpty(): bool { return $this->getLength() === 0; } diff --git a/Neos.ContentRepository.Core/Tests/Unit/Projection/ContentGraph/NodePathTest.php b/Neos.ContentRepository.Core/Tests/Unit/Projection/ContentGraph/NodePathTest.php index 79f20e2d5b7..9eb5da58ddf 100644 --- a/Neos.ContentRepository.Core/Tests/Unit/Projection/ContentGraph/NodePathTest.php +++ b/Neos.ContentRepository.Core/Tests/Unit/Projection/ContentGraph/NodePathTest.php @@ -22,7 +22,7 @@ class NodePathTest extends TestCase */ public function testDeserialization( string $serializedPath, - bool $expectedRootState, + bool $expectedEmptyState, /** @var array $expectedParts */ array $expectedParts, int $expectedLength @@ -30,7 +30,7 @@ public function testDeserialization( $subject = NodePath::fromString($serializedPath); self::assertSame($serializedPath, $subject->serializeToString()); - self::assertSame($expectedRootState, $subject->isRoot()); + self::assertSame($expectedEmptyState, $subject->isEmpty()); self::assertEquals($expectedParts, $subject->getParts()); self::assertSame($expectedLength, $subject->getLength()); } @@ -39,7 +39,7 @@ public static function serializedPathProvider(): iterable { yield 'nonRoot' => [ 'serializedPath' => 'child/grandchild', - 'expectedRootState' => false, + 'isEmpty' => false, 'expectedParts' => [ NodeName::fromString('child'), NodeName::fromString('grandchild'), @@ -49,7 +49,7 @@ public static function serializedPathProvider(): iterable yield 'root' => [ 'serializedPath' => '', - 'expectedRootState' => true, + 'isEmpty' => true, 'expectedParts' => [], 'expectedLength' => 0 ]; diff --git a/Neos.Neos/Classes/Domain/Service/NodeDuplication/NodeAggregateIdMapping.php b/Neos.Neos/Classes/Domain/Service/NodeDuplication/NodeAggregateIdMapping.php index 713985da98a..88cac7635a1 100644 --- a/Neos.Neos/Classes/Domain/Service/NodeDuplication/NodeAggregateIdMapping.php +++ b/Neos.Neos/Classes/Domain/Service/NodeDuplication/NodeAggregateIdMapping.php @@ -36,7 +36,7 @@ final class NodeAggregateIdMapping implements \JsonSerializable /** * @param array $nodeAggregateIds */ - public function __construct(array $nodeAggregateIds) + private function __construct(array $nodeAggregateIds) { foreach ($nodeAggregateIds as $oldNodeAggregateId => $newNodeAggregateId) { $oldNodeAggregateId = NodeAggregateId::fromString($oldNodeAggregateId); diff --git a/Neos.Neos/Classes/Domain/Service/NodeDuplication/TransientNodeCopy.php b/Neos.Neos/Classes/Domain/Service/NodeDuplication/TransientNodeCopy.php index 8e3f169c026..d85ff72d4ff 100644 --- a/Neos.Neos/Classes/Domain/Service/NodeDuplication/TransientNodeCopy.php +++ b/Neos.Neos/Classes/Domain/Service/NodeDuplication/TransientNodeCopy.php @@ -59,7 +59,7 @@ public static function forEntry( self::getTetheredDescendantNodeAggregateIds( $subtree->children, $nodeAggregateIdMapping, - NodePath::forRoot(), + NodePath::createEmpty(), NodeAggregateIdsByNodePaths::createEmpty() ) ); @@ -100,7 +100,7 @@ public function forTetheredChildNode(Subtree $subtree): self self::getTetheredDescendantNodeAggregateIds( $subtree->children, $this->nodeAggregateIdMapping, - NodePath::forRoot(), + NodePath::createEmpty(), $descendantTetheredNodeAggregateIds ), ); @@ -118,7 +118,7 @@ public function forRegularChildNode(Subtree $subtree): self self::getTetheredDescendantNodeAggregateIds( $subtree->children, $this->nodeAggregateIdMapping, - NodePath::forRoot(), + NodePath::createEmpty(), NodeAggregateIdsByNodePaths::createEmpty() ), ); From d3115ce7a070ca878d1e250090d89cc4a71b1b98 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Tue, 19 Nov 2024 12:21:07 +0100 Subject: [PATCH 212/214] TASK: Remove obsolete logic to keep tethered node ids --- .../NodeDuplication/TransientNodeCopy.php | 17 ++------------ .../CopyNode_TetheredNodes.feature | 22 ++++++++++++++++++- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/Neos.Neos/Classes/Domain/Service/NodeDuplication/TransientNodeCopy.php b/Neos.Neos/Classes/Domain/Service/NodeDuplication/TransientNodeCopy.php index d85ff72d4ff..3efcb5d94be 100644 --- a/Neos.Neos/Classes/Domain/Service/NodeDuplication/TransientNodeCopy.php +++ b/Neos.Neos/Classes/Domain/Service/NodeDuplication/TransientNodeCopy.php @@ -78,20 +78,6 @@ public function forTetheredChildNode(Subtree $subtree): self throw new \InvalidArgumentException(sprintf('Name "%s" doesnt seem to be a point to a tethered node of "%s", could not determine deterministic node aggregate id.', $nodeName->value, $this->aggregateId->value)); } - // keep tethered node aggregate ids from parent - $descendantTetheredNodeAggregateIds = NodeAggregateIdsByNodePaths::createEmpty(); - foreach ($this->tetheredNodeAggregateIds->getNodeAggregateIds() as $stringNodePath => $descendantNodeAggregateId) { - $nodePath = NodePath::fromString($stringNodePath); - $pathParts = $nodePath->getParts(); - $firstPart = array_shift($pathParts); - if ($firstPart?->equals($nodeName) && count($pathParts)) { - $descendantTetheredNodeAggregateIds = $descendantTetheredNodeAggregateIds->add( - NodePath::fromNodeNames(...$pathParts), - $descendantNodeAggregateId - ); - } - } - return new self( $nodeAggregateId, $this->workspaceName, @@ -101,7 +87,8 @@ public function forTetheredChildNode(Subtree $subtree): self $subtree->children, $this->nodeAggregateIdMapping, NodePath::createEmpty(), - $descendantTetheredNodeAggregateIds + // we don't have to keep the relative $this->tetheredNodeAggregateIds for the current $nodName as we will just recalculate them from the subtree + NodeAggregateIdsByNodePaths::createEmpty() ), ); } diff --git a/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_TetheredNodes.feature b/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_TetheredNodes.feature index 1f5462f2413..90359de22c4 100644 --- a/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_TetheredNodes.feature +++ b/Neos.Neos/Tests/Behavior/Features/ContentRepository/NodeCopying/CopyNode_TetheredNodes.feature @@ -39,7 +39,7 @@ Feature: Copy nodes with tethered nodes | nody-mc-nodeface | sir-david-nodenborough | Neos.ContentRepository.Testing:DocumentWithoutTetheredChildren | | | nodimus-primus | lady-eleonode-rootford | Neos.ContentRepository.Testing:DocumentWithoutTetheredChildren | | | sir-nodeward-nodington-i | nodimus-primus | Neos.ContentRepository.Testing:DocumentWithTethered | {"tethered": "nodewyn-tetherton"} | - | node-wan-kenodi | lady-eleonode-rootford | Neos.ContentRepository.Testing:RootDocument | {"tethered-document": "tethered-document", "tethered-document/tethered": "tethered-document-child"} | + | node-wan-kenodi | sir-david-nodenborough | Neos.ContentRepository.Testing:RootDocument | {"tethered-document": "tethered-document", "tethered-document/tethered": "tethered-document-child"} | Scenario: Coping a tethered node turns it into a regular node And I expect the node aggregate "nodewyn-tetherton" to exist @@ -117,6 +117,7 @@ Feature: Copy nodes with tethered nodes And I expect this node aggregate to be of type "Neos.ContentRepository.Testing:DocumentWithTethered" And I expect this node aggregate to occupy dimension space points [[]] And I expect this node aggregate to disable dimension space points [] + And I expect this node aggregate to have the child node aggregates ["tethered-document-child-copy"] And I expect the node aggregate "tethered-document-child-copy" to exist And I expect this node aggregate to be classified as "tethered" @@ -191,3 +192,22 @@ Feature: Copy nodes with tethered nodes And I expect this node to have the following references: | Name | Node | Properties | | ref | cs-identifier;sir-david-nodenborough;{} | null | + + Scenario: Properties are copied for deeply nested tethered nodes + And the command SetNodeProperties is executed with payload: + | Key | Value | + | nodeAggregateId | "tethered-document-child" | + | propertyValues | {"title": "Original Text"} | + + When copy nodes recursively is executed with payload: + | Key | Value | + | sourceDimensionSpacePoint | {} | + | sourceNodeAggregateId | "sir-david-nodenborough" | + | targetDimensionSpacePoint | {} | + | targetParentNodeAggregateId | "nodimus-primus" | + | nodeAggregateIdMapping | {"sir-david-nodenborough": "sir-david-nodenborough-copy", "node-wan-kenodi": "node-wan-kenodi-copy", "tethered-document": "tethered-document-copy", "tethered-document-child": "tethered-document-child-copy"} | + + And I expect node aggregate identifier "tethered-document-child-copy" to lead to node cs-identifier;tethered-document-child-copy;{} + And I expect this node to have the following properties: + | Key | Value | + | title | "Original Text" | From eaeb695c22445666e802cfa7b2efe8bcfa55b37e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Mu=CC=88ller?= Date: Tue, 19 Nov 2024 13:10:28 +0100 Subject: [PATCH 213/214] Keep ExportedEvent as in 9.0 --- .../src/Event/ValueObject/ExportedEvent.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Neos.ContentRepository.Export/src/Event/ValueObject/ExportedEvent.php b/Neos.ContentRepository.Export/src/Event/ValueObject/ExportedEvent.php index 7b565a8dece..f3bdf74ed35 100644 --- a/Neos.ContentRepository.Export/src/Event/ValueObject/ExportedEvent.php +++ b/Neos.ContentRepository.Export/src/Event/ValueObject/ExportedEvent.php @@ -24,10 +24,14 @@ public function __construct( public static function fromRawEvent(Event $event): self { + $payload = \json_decode($event->data->value, true, 512, JSON_THROW_ON_ERROR); + // unset content stream id as this is overwritten during import + unset($payload['contentStreamId'], $payload['workspaceName']); + return new self( $event->id->value, $event->type->value, - \json_decode($event->data->value, true, 512, JSON_THROW_ON_ERROR), + $payload, $event->metadata?->value ?? [], ); } @@ -40,6 +44,7 @@ public static function fromJson(string $json): self } catch (\JsonException $e) { throw new \InvalidArgumentException(sprintf('Failed to decode JSON "%s": %s', $json, $e->getMessage()), 1638432979, $e); } + return new self( $data['identifier'], $data['type'], @@ -55,6 +60,7 @@ public function withIdentifier(string $identifier): self /** * @param \Closure(array): array $processor + * @return self */ public function processPayload(\Closure $processor): self { @@ -63,6 +69,7 @@ public function processPayload(\Closure $processor): self /** * @param \Closure(array): array $processor + * @return self */ public function processMetadata(\Closure $processor): self { From ff6cf215bed9113ba8ec236ab2c021773cfe0aeb Mon Sep 17 00:00:00 2001 From: Jenkins Date: Tue, 19 Nov 2024 12:14:06 +0000 Subject: [PATCH 214/214] TASK: Update references [skip ci] --- Neos.Neos/Documentation/References/CommandReference.rst | 5 +++-- .../Documentation/References/ViewHelpers/FluidAdaptor.rst | 2 +- Neos.Neos/Documentation/References/ViewHelpers/Form.rst | 2 +- Neos.Neos/Documentation/References/ViewHelpers/Media.rst | 2 +- Neos.Neos/Documentation/References/ViewHelpers/Neos.rst | 2 +- .../Documentation/References/ViewHelpers/TYPO3Fluid.rst | 2 +- 6 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Neos.Neos/Documentation/References/CommandReference.rst b/Neos.Neos/Documentation/References/CommandReference.rst index cb220a6bc92..c4f45f3200b 100644 --- a/Neos.Neos/Documentation/References/CommandReference.rst +++ b/Neos.Neos/Documentation/References/CommandReference.rst @@ -19,7 +19,7 @@ commands that may be available, use:: ./flow help -The following reference was automatically generated from code on 2024-11-12 +The following reference was automatically generated from code on 2024-11-19 .. _`Neos Command Reference: NEOS.FLOW`: @@ -2761,6 +2761,7 @@ Options Without explicit workspace roles, only administrators can change the corresponding workspace. With this command, a user or group (represented by a Flow role identifier) can be granted one of the two roles: +- viewer: Can read from the workspace - collaborator: Can read from and write to the workspace - manager: Can read from and write to the workspace and manage it (i.e. change metadata & role assignments) @@ -2780,7 +2781,7 @@ Arguments ``--subject`` The user/group that should be assigned. By default, this is expected to be a Flow role identifier (e.g. 'Neos.Neos:AbstractEditor') – if $type is 'user', this is the username (aka account identifier) of a Neos user ``--role`` - Role to assign, either 'collaborator' or 'manager' – a collaborator can read and write from/to the workspace. A manager can _on top_ change the workspace metadata & roles itself + Role to assign, either 'viewer', 'collaborator' or 'manager' – a viewer can only read from the workspace, a collaborator can read and write from/to the workspace. A manager can _on top_ change the workspace metadata & roles itself diff --git a/Neos.Neos/Documentation/References/ViewHelpers/FluidAdaptor.rst b/Neos.Neos/Documentation/References/ViewHelpers/FluidAdaptor.rst index dd993f659eb..d47388c29ee 100644 --- a/Neos.Neos/Documentation/References/ViewHelpers/FluidAdaptor.rst +++ b/Neos.Neos/Documentation/References/ViewHelpers/FluidAdaptor.rst @@ -3,7 +3,7 @@ FluidAdaptor ViewHelper Reference ################################# -This reference was automatically generated from code on 2024-11-12 +This reference was automatically generated from code on 2024-11-19 .. _`FluidAdaptor ViewHelper Reference: f:debug`: diff --git a/Neos.Neos/Documentation/References/ViewHelpers/Form.rst b/Neos.Neos/Documentation/References/ViewHelpers/Form.rst index d513ff0fe85..50df5726140 100644 --- a/Neos.Neos/Documentation/References/ViewHelpers/Form.rst +++ b/Neos.Neos/Documentation/References/ViewHelpers/Form.rst @@ -3,7 +3,7 @@ Form ViewHelper Reference ######################### -This reference was automatically generated from code on 2024-11-12 +This reference was automatically generated from code on 2024-11-19 .. _`Form ViewHelper Reference: neos.form:form`: diff --git a/Neos.Neos/Documentation/References/ViewHelpers/Media.rst b/Neos.Neos/Documentation/References/ViewHelpers/Media.rst index add07aae4fa..de2f66c7208 100644 --- a/Neos.Neos/Documentation/References/ViewHelpers/Media.rst +++ b/Neos.Neos/Documentation/References/ViewHelpers/Media.rst @@ -3,7 +3,7 @@ Media ViewHelper Reference ########################## -This reference was automatically generated from code on 2024-11-12 +This reference was automatically generated from code on 2024-11-19 .. _`Media ViewHelper Reference: neos.media:fileTypeIcon`: diff --git a/Neos.Neos/Documentation/References/ViewHelpers/Neos.rst b/Neos.Neos/Documentation/References/ViewHelpers/Neos.rst index 7addbdf3cb8..c9999dbcc68 100644 --- a/Neos.Neos/Documentation/References/ViewHelpers/Neos.rst +++ b/Neos.Neos/Documentation/References/ViewHelpers/Neos.rst @@ -3,7 +3,7 @@ Neos ViewHelper Reference ######################### -This reference was automatically generated from code on 2024-11-12 +This reference was automatically generated from code on 2024-11-19 .. _`Neos ViewHelper Reference: neos:backend.authenticationProviderLabel`: diff --git a/Neos.Neos/Documentation/References/ViewHelpers/TYPO3Fluid.rst b/Neos.Neos/Documentation/References/ViewHelpers/TYPO3Fluid.rst index fac6148952b..e1e7b3cb2d4 100644 --- a/Neos.Neos/Documentation/References/ViewHelpers/TYPO3Fluid.rst +++ b/Neos.Neos/Documentation/References/ViewHelpers/TYPO3Fluid.rst @@ -3,7 +3,7 @@ TYPO3 Fluid ViewHelper Reference ################################ -This reference was automatically generated from code on 2024-11-12 +This reference was automatically generated from code on 2024-11-19 .. _`TYPO3 Fluid ViewHelper Reference: f:alias`: