diff --git a/core/modules/block_content/src/BlockContentAccessControlHandler.php b/core/modules/block_content/src/BlockContentAccessControlHandler.php index 8821080a9ce8bbb26edafaad9d042251124a6e30..fa5f7730c8a0bdf7d80860d64ad8112e42be937a 100644 --- a/core/modules/block_content/src/BlockContentAccessControlHandler.php +++ b/core/modules/block_content/src/BlockContentAccessControlHandler.php @@ -103,7 +103,7 @@ protected function checkAccess(EntityInterface $entity, $operation, AccountInter $dependency = $entity->getAccessDependency(); if (empty($dependency)) { // If an access dependency has not been set let modules set one. - $event = new BlockContentGetDependencyEvent($entity); + $event = new BlockContentGetDependencyEvent($entity, $operation); $this->eventDispatcher->dispatch($event, BlockContentEvents::BLOCK_CONTENT_GET_DEPENDENCY); $dependency = $event->getAccessDependency(); if (empty($dependency)) { diff --git a/core/modules/block_content/src/Event/BlockContentGetDependencyEvent.php b/core/modules/block_content/src/Event/BlockContentGetDependencyEvent.php index 423d89f2f815466e7a364e375929d5546f619c10..21adba392c7bf55db80053aef8628155e35155f6 100644 --- a/core/modules/block_content/src/Event/BlockContentGetDependencyEvent.php +++ b/core/modules/block_content/src/Event/BlockContentGetDependencyEvent.php @@ -13,13 +13,6 @@ */ class BlockContentGetDependencyEvent extends Event { - /** - * The block content entity. - * - * @var \Drupal\block_content\BlockContentInterface - */ - protected $blockContent; - /** * The dependency. * @@ -28,14 +21,18 @@ class BlockContentGetDependencyEvent extends Event { protected $accessDependency; /** - * BlockContentGetDependencyEvent constructor. + * Constructs a new BlockContentGetDependencyEvent object. * * @param \Drupal\block_content\BlockContentInterface $blockContent * The block content entity. + * @param string $operation + * The access operation for which to load the block content dependency. + * Defaults to 'view'. */ - public function __construct(BlockContentInterface $blockContent) { - $this->blockContent = $blockContent; - } + public function __construct( + protected BlockContentInterface $blockContent, + protected readonly string $operation = 'view', + ) {} /** * Gets the block content entity. @@ -47,6 +44,16 @@ public function getBlockContentEntity() { return $this->blockContent; } + /** + * Gets the access operation for this dependency event. + * + * @return string + * The access operation. + */ + public function getOperation(): string { + return $this->operation; + } + /** * Gets the access dependency. * diff --git a/core/modules/layout_builder/src/EventSubscriber/SetInlineBlockDependency.php b/core/modules/layout_builder/src/EventSubscriber/SetInlineBlockDependency.php index a1fb950be7c7014be38d70a2341d3db45f65ee48..74aa98db1a37d4d90c458a72098ea0b2df89157f 100644 --- a/core/modules/layout_builder/src/EventSubscriber/SetInlineBlockDependency.php +++ b/core/modules/layout_builder/src/EventSubscriber/SetInlineBlockDependency.php @@ -5,9 +5,13 @@ use Drupal\block_content\BlockContentEvents; use Drupal\block_content\BlockContentInterface; use Drupal\block_content\Event\BlockContentGetDependencyEvent; +use Drupal\Core\Access\AccessibleInterface; +use Drupal\Core\Ajax\AjaxHelperTrait; use Drupal\Core\Database\Connection; use Drupal\Core\Entity\EntityInterface; -use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Entity\EntityRepositoryInterface; +use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\layout_builder\Access\LayoutPreviewAccessAllowed; use Drupal\layout_builder\InlineBlockUsageInterface; use Drupal\layout_builder\LayoutEntityHelperTrait; use Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface; @@ -35,45 +39,56 @@ class SetInlineBlockDependency implements EventSubscriberInterface { use LayoutEntityHelperTrait; + use AjaxHelperTrait; /** - * The entity type manager. + * The entity repository. * - * @var \Drupal\Core\Entity\EntityTypeManagerInterface + * @var \Drupal\Core\Entity\EntityRepositoryInterface */ - protected $entityTypeManager; + protected EntityRepositoryInterface $entityRepository; /** - * The database connection. + * The current route match service. * - * @var \Drupal\Core\Database\Connection + * @var \Drupal\Core\Routing\RouteMatchInterface */ - protected $database; + protected RouteMatchInterface $currentRouteMatch; /** - * The inline block usage service. + * Constructs a new SetInlineBlockDependency object. * - * @var \Drupal\layout_builder\InlineBlockUsageInterface - */ - protected $usage; - - /** - * Constructs SetInlineBlockDependency object. - * - * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager - * The entity type manager. + * @param \Drupal\Core\Entity\EntityRepositoryInterface $entityRepository + * The entity repository * @param \Drupal\Core\Database\Connection $database * The database connection. * @param \Drupal\layout_builder\InlineBlockUsageInterface $usage * The inline block usage service. - * @param \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface $section_storage_manager + * @param \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface $sectionStorageManager * The section storage manager. + * @param \Drupal\Core\Routing\RouteMatchInterface|null $currentRouteMatch + * The current route match service. */ - public function __construct(EntityTypeManagerInterface $entity_type_manager, Connection $database, InlineBlockUsageInterface $usage, SectionStorageManagerInterface $section_storage_manager) { - $this->entityTypeManager = $entity_type_manager; - $this->database = $database; - $this->usage = $usage; - $this->sectionStorageManager = $section_storage_manager; + public function __construct( + mixed $entityRepository, + protected readonly Connection $database, + protected readonly InlineBlockUsageInterface $usage, + SectionStorageManagerInterface $sectionStorageManager, + ?RouteMatchInterface $currentRouteMatch, + ) { + if (!$entityRepository instanceof EntityRepositoryInterface) { + // @todo Replace link with a link to the change record. + @trigger_error('Calling ' . __METHOD__ . ' without passing the entity repository as the first argument is deprecated in drupal:11.0.0 and will be required in drupal:12.0.0. See https://www.drupal.org/node/3047022', E_USER_DEPRECATED); + $entityRepository = \Drupal::service('entity.repository'); + } + $this->entityRepository = $entityRepository; + $this->sectionStorageManager = $sectionStorageManager; + if (empty($currentRouteMatch)) { + // @todo Replace link with a link to the change record. + @trigger_error('Calling ' . __METHOD__ . ' without the $currentRouteMatch argument is deprecated in drupal:11.0.0 and will be required in drupal:12.0.0. See https://www.drupal.org/node/3047022', E_USER_DEPRECATED); + $currentRouteMatch = \Drupal::service('current_route_match'); + } + $this->currentRouteMatch = $currentRouteMatch; } /** @@ -92,7 +107,7 @@ public static function getSubscribedEvents(): array { * The event. */ public function onGetDependency(BlockContentGetDependencyEvent $event) { - if ($dependency = $this->getInlineBlockDependency($event->getBlockContentEntity())) { + if ($dependency = $this->getInlineBlockDependency($event->getBlockContentEntity(), $event->getOperation())) { $event->setAccessDependency($dependency); } } @@ -115,27 +130,41 @@ public function onGetDependency(BlockContentGetDependencyEvent $event) { * * @param \Drupal\block_content\BlockContentInterface $block_content * The block content entity. + * @param string $operation + * The access operation to load the inline block dependency for. * - * @return \Drupal\Core\Entity\EntityInterface|null - * Returns the layout dependency. + * @return \Drupal\Core\Access\AccessibleInterface|null + * Returns the access dependency. * * @see \Drupal\block_content\BlockContentAccessControlHandler::checkAccess() * @see \Drupal\layout_builder\EventSubscriber\BlockComponentRenderArray::onBuildRender() */ - protected function getInlineBlockDependency(BlockContentInterface $block_content) { + protected function getInlineBlockDependency(BlockContentInterface $block_content, string $operation): ?AccessibleInterface { + $active_operations = ['update', 'delete']; + $current_route = $this->currentRouteMatch->getRouteObject(); + if ('view' === $operation && ($current_route && $current_route->getOption('_layout_builder'))) { + $active_operations[] = 'view'; + } $layout_entity_info = $this->usage->getUsage($block_content->id()); - if (empty($layout_entity_info)) { + if (empty($layout_entity_info) || empty($layout_entity_info->layout_entity_type) || empty($layout_entity_info->layout_entity_id)) { // If the block does not have usage information then we cannot set a // dependency. It may be used by another module besides layout builder. return NULL; } - $layout_entity_storage = $this->entityTypeManager->getStorage($layout_entity_info->layout_entity_type); - $layout_entity = $layout_entity_storage->load($layout_entity_info->layout_entity_id); + // When updating or deleting an inline block, resolve the inline block + // dependency via the active revision, since it is the revision that should + // be loaded for editing purposes. + if (in_array($operation, $active_operations, TRUE)) { + $layout_entity = $this->entityRepository->getActive($layout_entity_info->layout_entity_type, $layout_entity_info->layout_entity_id); + } + else { + $layout_entity = $this->entityRepository->getCanonical($layout_entity_info->layout_entity_type, $layout_entity_info->layout_entity_id); + } if ($this->isLayoutCompatibleEntity($layout_entity)) { if ($this->isBlockRevisionUsedInEntity($layout_entity, $block_content)) { - return $layout_entity; + // Allow components to be viewed when rendered via AJAX (preview mode). + return 'view' === $operation && $this->isAjax() ? new LayoutPreviewAccessAllowed() : $layout_entity; } - } return NULL; } diff --git a/core/modules/layout_builder/src/LayoutBuilderServiceProvider.php b/core/modules/layout_builder/src/LayoutBuilderServiceProvider.php index f0b078c48ebd7677ba18b3a3b4423b8e7984c142..24efff92eaf10858c1146b74430d2a1f836adcd2 100644 --- a/core/modules/layout_builder/src/LayoutBuilderServiceProvider.php +++ b/core/modules/layout_builder/src/LayoutBuilderServiceProvider.php @@ -31,10 +31,11 @@ public function register(ContainerBuilder $container) { if (isset($modules['block_content'])) { $definition = new Definition(SetInlineBlockDependency::class); $definition->setArguments([ - new Reference('entity_type.manager'), + new Reference('entity.repository'), new Reference('database'), new Reference('inline_block.usage'), new Reference('plugin.manager.layout_builder.section_storage'), + new Reference('current_route_match'), ]); $definition->addTag('event_subscriber'); $definition->setPublic(TRUE); diff --git a/core/modules/layout_builder/tests/src/Kernel/SetInlineBlockDependencyTest.php b/core/modules/layout_builder/tests/src/Kernel/SetInlineBlockDependencyTest.php new file mode 100644 index 0000000000000000000000000000000000000000..022958fcd69f1bb609b1977b53296eb56eba6ffd --- /dev/null +++ b/core/modules/layout_builder/tests/src/Kernel/SetInlineBlockDependencyTest.php @@ -0,0 +1,216 @@ +setUpCurrentUser(); + $this->installSchema('layout_builder', ['inline_block_usage']); + + $this->installEntitySchema('entity_test_mulrevpub'); + $this->installEntitySchema('block_content'); + $this->installEntitySchema('content_moderation_state'); + + BlockContentType::create([ + 'id' => 'basic', + 'label' => 'Basic block', + 'revision' => 1, + ])->save(); + + $display = LayoutBuilderEntityViewDisplay::create([ + 'targetEntityType' => 'entity_test_mulrevpub', + 'bundle' => 'entity_test_mulrevpub', + 'mode' => 'default', + 'status' => TRUE, + ]); + $display->enableLayoutBuilder(); + $display->setOverridable(); + $display->save(); + + $workflow = $this->createEditorialWorkflow(); + $workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_mulrevpub', 'entity_test_mulrevpub'); + $workflow->save(); + } + + /** + * Test inline block dependencies with no route object. + */ + public function testInlineBlockDependencyWithNoRouteObject() { + // Create a mock route match service to return a NULL route object. + $current_route_match = $this->prophesize(CurrentRouteMatch::class); + $current_route_match->getRouteObject()->willReturn(NULL); + + $container = \Drupal::getContainer(); + $container->set('current_route_match', $current_route_match->reveal()); + \Drupal::setContainer($container); + + // Create a test entity, block, & account for running access checks. + $entity = EntityTestMulRevPub::create(); + $entity->save(); + $block = $this->addInlineBlockToOverrideLayout($entity); + $account = $this->createUser([ + 'create and edit custom blocks', + 'view test entity', + 'use editorial transition create_new_draft', + 'use editorial transition publish', + ]); + + // The access check that is ran here doesn't really matter; we're just + // looking to confirm that no adverse effects result from a NULL route + // object when checking block access. + // + // When confirming this, we want to ensure that the NULL route object is + // retrieved and a failure doesn't occur as a result of running the check. + $current_route_match->getRouteObject()->shouldNotHaveBeenCalled(); + $block->access('view', $account); + $current_route_match->getRouteObject()->shouldHaveBeenCalled(); + } + + /** + * Test inline block dependencies with a default revision entity host. + */ + public function testInlineBlockDependencyDefaultRevision() { + $entity = EntityTestMulRevPub::create(); + $entity->save(); + $block = $this->addInlineBlockToOverrideLayout($entity); + $account = $this->createUser([ + 'create and edit custom blocks', + 'view test entity', + 'use editorial transition create_new_draft', + 'use editorial transition publish', + ]); + $this->assertFalse($block->access('view', $account)); + $this->assertTrue($block->access('update', $account)); + $this->assertTrue($block->access('delete', $account)); + } + + /** + * Test inline block dependencies with a non-default revision entity host. + */ + public function testInlineBlockDependencyNonDefaultActiveRevision() { + // Create the canonical revision. + $entity = EntityTestMulRevPub::create(['moderation_state' => 'published']); + $entity->save(); + + // Create and add a custom block to a new active revision. + $entity->moderation_state = 'draft'; + $block = $this->addInlineBlockToOverrideLayout($entity); + + $account = $this->createUser([ + 'create and edit custom blocks', + 'view test entity', + 'use editorial transition create_new_draft', + 'use editorial transition publish', + ]); + // The block does not exist on the canonical revision, so access will not be + // granted since the custom block will not have a resolved dependency via + // the canonical revision. Some components may choose to manually set a + // different revision as the block dependent when displaying a non-canonical + // revision of the entity, such as the content moderation latest-version + // route. @see + // \Drupal\layout_builder\EventSubscriber\BlockComponentRenderArray::onBuildRender. + $this->assertFalse($block->access('view', $account)); + // Access to update the block is resolved and granted via the 'active' + // revision of the entity. Update access on the content block itself must be + // granted so that access checks outside of the layout builder routes are + // correctly granted. + $this->assertTrue($block->access('update', $account)); + $this->assertTrue($block->access('delete', $account)); + } + + /** + * Test the inline block dependency when removed from the active revision. + */ + public function testInlineBlockDependencyRemovedInActiveRevision() { + // Create the canonical revision with an inline block. + $entity = EntityTestMulRevPub::create(['moderation_state' => 'published']); + $entity->save(); + $block = $this->addInlineBlockToOverrideLayout($entity); + + // Create an active revision that removes the inline block. + $entity->{OverridesSectionStorage::FIELD_NAME} = []; + $entity->moderation_state = 'draft'; + $entity->save(); + + $account = $this->createUser([ + 'create and edit custom blocks', + 'view test entity', + 'use editorial transition create_new_draft', + 'use editorial transition publish', + ]); + // Access to update the block will be resolved through the active revision + // and denied, since the block has been removed from the layout. + $this->assertFalse($block->access('update', $account)); + $this->assertFalse($block->access('delete', $account)); + // Access to view the block will be resolved through the canonical revision + // and granted, since the block still exists on the canonical revision. + $this->assertTrue($block->access('view', $account)); + } + + /** + * Add an inline block to an override layout of an entity. + * + * @param \Drupal\entity_test\Entity\EntityTestMulRevPub $entity + * The entity to add an inline block to. + * + * @return \Drupal\block_content\Entity\BlockContent + * The loaded block content revision attached to the layout. + */ + protected function addInlineBlockToOverrideLayout(EntityTestMulRevPub $entity) { + $block = BlockContent::create([ + 'type' => 'basic', + 'reusable' => FALSE, + ]); + $section_data = new Section('layout_onecol', [], [ + 'first-uuid' => new SectionComponent('first-uuid', 'content', [ + 'id' => sprintf('inline_block:basic'), + 'block_serialized' => serialize($block), + ]), + ]); + $entity->{OverridesSectionStorage::FIELD_NAME} = $section_data; + $entity->save(); + $inline_block_revision_id = $entity->{OverridesSectionStorage::FIELD_NAME}->getSections()[0]->getComponent('first-uuid')->getPlugin()->getConfiguration()['block_revision_id']; + return $this->container->get('entity_type.manager')->getStorage('block_content')->loadRevision($inline_block_revision_id); + } + +}