diff --git a/core/modules/block_content/src/BlockContentAccessControlHandler.php b/core/modules/block_content/src/BlockContentAccessControlHandler.php index 5ea6cfce1f..289f51b0bc 100644 --- a/core/modules/block_content/src/BlockContentAccessControlHandler.php +++ b/core/modules/block_content/src/BlockContentAccessControlHandler.php @@ -72,7 +72,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 423d89f2f8..a87761fa88 100644 --- a/core/modules/block_content/src/Event/BlockContentGetDependencyEvent.php +++ b/core/modules/block_content/src/Event/BlockContentGetDependencyEvent.php @@ -27,14 +27,24 @@ class BlockContentGetDependencyEvent extends Event { */ protected $accessDependency; + /** + * The access operation to load the block content dependency for. + * + * @var string + */ + protected $operation; + /** * BlockContentGetDependencyEvent constructor. * * @param \Drupal\block_content\BlockContentInterface $blockContent * The block content entity. + * @param string $operation + * The access operation to load the block content dependency for. */ - public function __construct(BlockContentInterface $blockContent) { + public function __construct(BlockContentInterface $blockContent, $operation) { $this->blockContent = $blockContent; + $this->operation = $operation; } /** @@ -47,6 +57,16 @@ public function getBlockContentEntity() { return $this->blockContent; } + /** + * Get the access operation for this dependency event. + * + * @return string + * The access operation. + */ + public function getOperation() { + 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 d1ff4ad3dc..c4d78780e8 100644 --- a/core/modules/layout_builder/src/EventSubscriber/SetInlineBlockDependency.php +++ b/core/modules/layout_builder/src/EventSubscriber/SetInlineBlockDependency.php @@ -5,9 +5,12 @@ use Drupal\block_content\BlockContentEvents; use Drupal\block_content\BlockContentInterface; use Drupal\block_content\Event\BlockContentGetDependencyEvent; +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\CurrentRouteMatch; +use Drupal\layout_builder\Access\LayoutPreviewAccessAllowed; use Drupal\layout_builder\InlineBlockUsageInterface; use Drupal\layout_builder\LayoutEntityHelperTrait; use Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface; @@ -35,13 +38,14 @@ 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 $entityRepository; /** * The database connection. @@ -57,10 +61,17 @@ class SetInlineBlockDependency implements EventSubscriberInterface { */ protected $usage; + /** + * The current route match service. + * + * @var \Drupal\Core\Routing\CurrentRouteMatch + */ + protected $currentRouteMatch; + /** * Constructs SetInlineBlockDependency object. * - * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository * The entity type manager. * @param \Drupal\Core\Database\Connection $database * The database connection. @@ -69,11 +80,12 @@ class SetInlineBlockDependency implements EventSubscriberInterface { * @param \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface $section_storage_manager * The section storage manager. */ - public function __construct(EntityTypeManagerInterface $entity_type_manager, Connection $database, InlineBlockUsageInterface $usage, SectionStorageManagerInterface $section_storage_manager) { - $this->entityTypeManager = $entity_type_manager; + public function __construct(EntityRepositoryInterface $entity_repository, Connection $database, InlineBlockUsageInterface $usage, SectionStorageManagerInterface $section_storage_manager, CurrentRouteMatch $current_route_match) { + $this->entityRepository = $entity_repository; $this->database = $database; $this->usage = $usage; $this->sectionStorageManager = $section_storage_manager; + $this->currentRouteMatch = $current_route_match; } /** @@ -92,7 +104,18 @@ public static function getSubscribedEvents() { * The event. */ public function onGetDependency(BlockContentGetDependencyEvent $event) { - if ($dependency = $this->getInlineBlockDependency($event->getBlockContentEntity())) { + // Ajax form submission will trigger here, it's ajax route, such as UpdateBlockForm. + // If the action from ajax, the section component should in preview mode, + // the view operation should LayoutPreviewAccessAllowed. + // @see \Drupal\layout_builder\Form\ConfigureBlockFormBase::successfulAjaxSubmit(). + // @see \Drupal\layout_builder\EventSubscriber\BlockComponentRenderArray::onBuildRender(). + if ($this->isAjax() && $event->getOperation() === 'view' && ($section_storage = $this->currentRouteMatch->getParameter('section_storage'))) { + // Determines if a block content revision is used in section storage. + if (in_array($event->getBlockContentEntity()->getRevisionId(), $this->getInlineBlockRevisionIdsInSections($section_storage->getSections()))) { + $event->setAccessDependency(new LayoutPreviewAccessAllowed()); + } + } + if ($dependency = $this->getInlineBlockDependency($event->getBlockContentEntity(), $event->getOperation())) { $event->setAccessDependency($dependency); } } @@ -115,6 +138,8 @@ 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. @@ -122,15 +147,27 @@ public function onGetDependency(BlockContentGetDependencyEvent $event) { * @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, $operation) { + $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; diff --git a/core/modules/layout_builder/src/LayoutBuilderServiceProvider.php b/core/modules/layout_builder/src/LayoutBuilderServiceProvider.php index f0b078c48e..24efff92ea 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 0000000000..2388ade19b --- /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->assertTrue($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); + } + +}