diff --git a/core/modules/layout_builder/config/schema/layout_builder.schema.yml b/core/modules/layout_builder/config/schema/layout_builder.schema.yml index 682caa78c4..287215c8e9 100644 --- a/core/modules/layout_builder/config/schema/layout_builder.schema.yml +++ b/core/modules/layout_builder/config/schema/layout_builder.schema.yml @@ -44,3 +44,20 @@ layout_builder.component: additional: type: ignore label: 'Additional data' + +inline_block_contents: + type: block_settings + label: 'Inline content block' + mapping: + view_mode: + type: string + lable: 'View mode' + block_revision_id: + type: integer + label: 'Block revision ID' + block_serialized: + type: string + label: 'Serialized block' + +block.settings.inline_block_content:*: + type: inline_block_contents diff --git a/core/modules/layout_builder/layout_builder.install b/core/modules/layout_builder/layout_builder.install index acb1e4fdf3..01ac7140c8 100644 --- a/core/modules/layout_builder/layout_builder.install +++ b/core/modules/layout_builder/layout_builder.install @@ -6,6 +6,8 @@ */ use Drupal\Core\Cache\Cache; +use Drupal\Core\Database\Database; +use Drupal\Core\Entity\EntityTypeInterface; use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay; use Drupal\layout_builder\Section; @@ -38,3 +40,75 @@ function layout_builder_install() { // prepare for future changes. Cache::invalidateTags(['rendered']); } + +/** + * Implements hook_schema(). + */ +function layout_builder_schema() { + $schema['inline_block_content_usage'] = [ + 'description' => 'Track where a block_content entity is used.', + 'fields' => [ + 'block_content_id' => [ + 'description' => 'The block_content entity ID.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ], + 'layout_entity_type' => [ + 'description' => 'The entity type of the parent entity.', + 'type' => 'varchar_ascii', + 'length' => EntityTypeInterface::ID_MAX_LENGTH, + 'not null' => FALSE, + 'default' => '', + ], + 'layout_entity_id' => [ + 'description' => 'The ID of the parent entity.', + 'type' => 'varchar_ascii', + 'length' => 128, + 'not null' => FALSE, + 'default' => 0, + ], + ], + 'primary key' => ['block_content_id'], + 'indexes' => [ + 'type_id' => ['layout_entity_type', 'layout_entity_id'], + ], + ]; + return $schema; +} + +/** + * Create the 'inline_block_content_usage' table. + */ +function layout_builder_update_8001() { + $inline_block_content_usage = [ + 'description' => 'Track where a block_content entity is used.', + 'fields' => [ + 'block_content_id' => [ + 'description' => 'The block_content entity ID.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ], + 'layout_entity_type' => [ + 'description' => 'The entity type of the parent entity.', + 'type' => 'varchar_ascii', + 'length' => EntityTypeInterface::ID_MAX_LENGTH, + 'not null' => FALSE, + 'default' => '', + ], + 'layout_entity_id' => [ + 'description' => 'The ID of the parent entity.', + 'type' => 'varchar_ascii', + 'length' => 128, + 'not null' => FALSE, + 'default' => 0, + ], + ], + 'primary key' => ['block_content_id'], + 'indexes' => [ + 'type_id' => ['layout_entity_type', 'layout_entity_id'], + ], + ]; + Database::getConnection()->schema()->createTable('inline_block_content_usage', $inline_block_content_usage); +} diff --git a/core/modules/layout_builder/layout_builder.module b/core/modules/layout_builder/layout_builder.module index 76ec53433d..79f8257781 100644 --- a/core/modules/layout_builder/layout_builder.module +++ b/core/modules/layout_builder/layout_builder.module @@ -5,6 +5,7 @@ * Provides hook implementations for Layout Builder. */ +use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Url; @@ -12,10 +13,12 @@ use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay; use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplayStorage; use Drupal\layout_builder\Form\LayoutBuilderEntityViewDisplayForm; -use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\Display\EntityViewDisplayInterface; use Drupal\layout_builder\Entity\LayoutEntityDisplayInterface; use Drupal\layout_builder\Plugin\Block\ExtraFieldBlock; +use Drupal\layout_builder\InlineBlockEntityOperations; +use Drupal\Core\Session\AccountInterface; +use Drupal\Core\Access\AccessResult; /** * Implements hook_help(). @@ -134,3 +137,70 @@ function layout_builder_module_implements_alter(&$implementations, $hook) { $implementations['layout_builder'] = $group; } } + + +/** + * Implements hook_entity_presave(). + */ +function layout_builder_entity_presave(EntityInterface $entity) { + if (\Drupal::moduleHandler()->moduleExists('block_content')) { + /** @var \Drupal\layout_builder\InlineBlockEntityOperations $entity_operations */ + $entity_operations = \Drupal::classResolver(InlineBlockEntityOperations::class); + $entity_operations->handlePreSave($entity); + } +} + +/** + * Implements hook_entity_delete(). + */ +function layout_builder_entity_delete(EntityInterface $entity) { + if (\Drupal::moduleHandler()->moduleExists('block_content')) { + /** @var \Drupal\layout_builder\InlineBlockEntityOperations $entity_operations */ + $entity_operations = \Drupal::classResolver(InlineBlockEntityOperations::class); + $entity_operations->handleEntityDelete($entity); + } +} + +/** + * Implements hook_cron(). + */ +function layout_builder_cron() { + if (\Drupal::moduleHandler()->moduleExists('block_content')) { + /** @var \Drupal\layout_builder\InlineBlockEntityOperations $entity_operations */ + $entity_operations = \Drupal::classResolver(InlineBlockEntityOperations::class); + $entity_operations->removeUnused(); + } +} + +/** + * Implements hook_plugin_filter_TYPE_alter(). + */ +function layout_builder_plugin_filter_block_alter(array &$definitions, array $extra, $consumer) { + // @todo Determine the 'inline_block_content' blocks should be allowed outside + // of layout_builder https://www.drupal.org/node/2979142. + if ($consumer !== 'layout_builder') { + foreach ($definitions as $id => $definition) { + if ($definition['id'] === 'inline_block_content') { + unset($definitions[$id]); + } + } + } +} + + +/** + * Implements hook_ENTITY_TYPE_access(). + */ +function layout_builder_block_content_access(EntityInterface $entity, $operation, AccountInterface $account) { + /** @var \Drupal\block_content\BlockContentInterface $entity */ + if ($operation === 'view' || $entity->isReusable() || empty(\Drupal::service('inline_block_content.usage')->getUsage($entity->id()))) { + // If the operation is 'view' or this is reusable block or if this is + // non-reusable that isn't used by this module then don't alter the access. + return AccessResult::neutral(); + } + + if ($account->hasPermission('configure any layout')) { + return AccessResult::allowed(); + } + return AccessResult::forbidden(); +} diff --git a/core/modules/layout_builder/layout_builder.services.yml b/core/modules/layout_builder/layout_builder.services.yml index 4fe50929bd..1f3a70e44e 100644 --- a/core/modules/layout_builder/layout_builder.services.yml +++ b/core/modules/layout_builder/layout_builder.services.yml @@ -39,3 +39,6 @@ services: logger.channel.layout_builder: parent: logger.channel_base arguments: ['layout_builder'] + inline_block_content.usage: + class: Drupal\layout_builder\InlineBlockContentUsage + arguments: ['@database'] diff --git a/core/modules/layout_builder/src/Access/LayoutPreviewAccessAllowed.php b/core/modules/layout_builder/src/Access/LayoutPreviewAccessAllowed.php new file mode 100644 index 0000000000..532ac6aac8 --- /dev/null +++ b/core/modules/layout_builder/src/Access/LayoutPreviewAccessAllowed.php @@ -0,0 +1,25 @@ +getContexts(); + if (isset($contexts['layout_builder.entity'])) { + if ($entity = $contexts['layout_builder.entity']->getContextValue()) { + if ($event->inPreview() && !empty($entity->in_preview)) { + // If previewing in Layout Builder allow access. + $block->setAccessDependency(new LayoutPreviewAccessAllowed()); + } + else { + $block->setAccessDependency($entity); + } + } + } + } + // Only check access if the component is not being previewed. if ($event->inPreview()) { $access = AccessResult::allowed()->setCacheMaxAge(0); diff --git a/core/modules/layout_builder/src/EventSubscriber/SetInlineBlockDependency.php b/core/modules/layout_builder/src/EventSubscriber/SetInlineBlockDependency.php new file mode 100644 index 0000000000..9cd07fafa0 --- /dev/null +++ b/core/modules/layout_builder/src/EventSubscriber/SetInlineBlockDependency.php @@ -0,0 +1,190 @@ +entityTypeManager = $entity_type_manager; + $this->database = $database; + $this->usage = $usage; + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + return [ + BlockContentEvents::BLOCK_CONTENT_GET_DEPENDENCY => 'onGetDependency', + ]; + } + + /** + * Handles the BlockContentEvents::INLINE_BLOCK_GET_DEPENDENCY event. + * + * @param \Drupal\block_content\Event\BlockContentGetDependencyEvent $event + * The event. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + */ + public function onGetDependency(BlockContentGetDependencyEvent $event) { + if ($dependency = $this->getInlineBlockDependency($event->getBlockContentEntity())) { + $event->setAccessDependency($dependency); + } + } + + /** + * Get the access dependency of an inline block. + * + * If the content block is used in a layout for a non-revisionable entity the + * entity will be returned. + * + * If the content block is used in a layout for a revisionable entity the + * first revision that uses the block will be returned. + * + * @param \Drupal\block_content\BlockContentInterface $block_content + * The block content entity. + * + * @return \Drupal\Core\Entity\EntityInterface|null + * Returns the layout dependency. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + */ + protected function getInlineBlockDependency(BlockContentInterface $block_content) { + $layout_entity_info = $this->usage->getUsage($block_content->id()); + if (empty($layout_entity_info)) { + // 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; + } + /** @var \Drupal\layout_builder\InlineBlockContentUsage $usage */ + $layout_entity_storage = $this->entityTypeManager->getStorage($layout_entity_info->layout_entity_type); + $layout_entity = $layout_entity_storage->load($layout_entity_info->layout_entity_id); + if ($this->isLayoutCompatibleEntity($layout_entity)) { + if (!$layout_entity->getEntityType()->isRevisionable()) { + // Check to see if this revision of the block was used in this entity. + // Although the layout builder does not create new block revisions when + // the layout entity does not support revisions another module may + // have created new revisions for this block. + if ($this->isBlockRevisionUsedInEntity($layout_entity, $block_content)) { + return $layout_entity; + } + } + else { + foreach ($this->getEntityRevisionIds($layout_entity) as $revision_id) { + $revision = $layout_entity_storage->loadRevision($revision_id); + if ($this->isBlockRevisionUsedInEntity($revision, $block_content)) { + return $revision; + } + } + } + + } + return NULL; + } + + /** + * Determines if a block content revision is used in an entity. + * + * @param \Drupal\Core\Entity\EntityInterface $layout_entity + * The layout entity. + * @param \Drupal\block_content\BlockContentInterface $block_content + * The block content revision. + * + * @return bool + * TRUE if the block content revision is used as an inline block in the + * layout entity. + */ + protected function isBlockRevisionUsedInEntity(EntityInterface $layout_entity, BlockContentInterface $block_content) { + $sections_blocks_revision_ids = $this->getInlineBlockRevisionIdsInSections($this->getEntitySections($layout_entity)); + return in_array($block_content->getRevisionId(), $sections_blocks_revision_ids); + } + + /** + * Gets the revision IDs for an entity. + * + * @todo Move this logic to \Drupal\Core\Entity\Sql\SqlContentEntityStorage in + * https://www.drupal.org/project/drupal/issues/2986027. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity. + * + * @return int[] + * The revision IDs. + * + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + */ + protected function getEntityRevisionIds(EntityInterface $entity) { + $entity_type = $this->entityTypeManager->getDefinition($entity->getEntityTypeId()); + if ($revision_table = $entity_type->getRevisionTable()) { + $query = $this->database->select($revision_table); + $query->condition($entity_type->getKey('id'), $entity->id()); + $query->fields($revision_table, [$entity_type->getKey('revision')]); + $query->orderBy($entity_type->getKey('revision'), 'DESC'); + return $query->execute()->fetchCol(); + } + return []; + } + +} diff --git a/core/modules/layout_builder/src/Form/RevertOverridesForm.php b/core/modules/layout_builder/src/Form/RevertOverridesForm.php index b6d07d9089..837140ecf8 100644 --- a/core/modules/layout_builder/src/Form/RevertOverridesForm.php +++ b/core/modules/layout_builder/src/Form/RevertOverridesForm.php @@ -103,6 +103,10 @@ public function buildForm(array $form, FormStateInterface $form_state, SectionSt * {@inheritdoc} */ public function submitForm(array &$form, FormStateInterface $form_state) { + // Ensure the section storage is loaded from the database. + // @todo Remove after https://www.drupal.org/node/2970801. + $this->sectionStorage = \Drupal::service('plugin.manager.layout_builder.section_storage')->loadFromStorageId($this->sectionStorage->getStorageType(), $this->sectionStorage->getStorageId()); + // Remove all sections. while ($this->sectionStorage->count()) { $this->sectionStorage->removeSection(0); diff --git a/core/modules/layout_builder/src/InlineBlockContentUsage.php b/core/modules/layout_builder/src/InlineBlockContentUsage.php new file mode 100644 index 0000000000..389c182977 --- /dev/null +++ b/core/modules/layout_builder/src/InlineBlockContentUsage.php @@ -0,0 +1,111 @@ +database = $database; + } + + /** + * Adds a usage record. + * + * @param int $block_content_id + * The block content id. + * @param \Drupal\Core\Entity\EntityInterface $entity + * The layout entity. + * + * @throws \Exception + */ + public function addUsage($block_content_id, EntityInterface $entity) { + $this->database->merge('inline_block_content_usage') + ->keys([ + 'block_content_id' => $block_content_id, + 'layout_entity_id' => $entity->id(), + 'layout_entity_type' => $entity->getEntityType(), + ])->execute(); + } + + /** + * Gets unused inline block IDs. + * + * @param int $limit + * The maximum number of block content entity IDs to return. + * + * @return int[] + * The entity IDs. + */ + public function getUnused($limit = 100) { + $query = $this->database->select('inline_block_content_usage', 't'); + $query->fields('t', ['block_content_id']); + $query->isNull('layout_entity_id'); + $query->isNull('layout_entity_type'); + return $query->range(0, $limit)->execute()->fetchCol(); + } + + /** + * Remove usage record by layout entity. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The layout entity. + */ + public function removeByLayoutEntity(EntityInterface $entity) { + $query = $this->database->update('inline_block_content_usage') + ->fields([ + 'layout_entity_type' => NULL, + 'layout_entity_id' => NULL, + ]); + $query->condition('layout_entity_type', $entity->getEntityTypeId()); + $query->condition('layout_entity_id', $entity->id()); + $query->execute(); + } + + /** + * Delete the inline blocks' the usage records. + * + * @param int[] $block_content_ids + * The block content entity IDs. + */ + public function deleteUsage(array $block_content_ids) { + $query = $this->database->delete('inline_block_content_usage')->condition('block_content_id', $block_content_ids, 'IN'); + $query->execute(); + } + + /** + * Gets usage record for inline block by ID. + * + * @param int $block_content_id + * The block content entity ID. + * + * @return object + * The usage record with properties layout_entity_id and layout_entity_type. + */ + public function getUsage($block_content_id) { + $query = $this->database->select('inline_block_content_usage'); + $query->condition('block_content_id', $block_content_id); + $query->fields('inline_block_content_usage', ['layout_entity_id', 'layout_entity_type']); + $query->range(0, 1); + return $query->execute()->fetchObject(); + } + +} diff --git a/core/modules/layout_builder/src/InlineBlockEntityOperations.php b/core/modules/layout_builder/src/InlineBlockEntityOperations.php new file mode 100644 index 0000000000..c7af6507c7 --- /dev/null +++ b/core/modules/layout_builder/src/InlineBlockEntityOperations.php @@ -0,0 +1,293 @@ +entityTypeManager = $entityTypeManager; + $this->blockContentStorage = $entityTypeManager->getStorage('block_content'); + $this->usage = $usage; + $this->database = $database; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.manager'), + $container->get('inline_block_content.usage'), + $container->get('database') + ); + } + + /** + * Remove all unused inline blocks on save. + * + * Entities that were used in prevision revisions will be removed if not + * saving a new revision. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The parent entity. + * + * @throws \Drupal\Core\Entity\EntityStorageException + */ + protected function removeUnusedForEntityOnSave(EntityInterface $entity) { + // If the entity is new or '$entity->original' is not set then there will + // not be any unused inline blocks to remove. + // If this is a revisionable entity then do not remove inline blocks. They + // could be referenced in previous revisions even if this is not a new + // revision. + if ($entity->isNew() || !isset($entity->original) || $entity instanceof RevisionableInterface) { + return; + } + $sections = $this->getEntitySections($entity); + // If this is a layout override and there are no sections then it is a new + // override. + if ($this->isEntityUsingFieldOverride($entity) && empty($sections)) { + return; + } + + // Delete and remove the usage for inline blocks that were removed. + if ($removed_block_ids = $this->getRemovedBlockIds($entity)) { + $this->deleteBlocksAndUsage($removed_block_ids); + } + } + + /** + * Gets the IDs of the inline blocks that were removed. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The layout entity. + * + * @return int[] + * The block content IDs that were removed. + */ + protected function getRemovedBlockIds(EntityInterface $entity) { + $original_sections = $this->getEntitySections($entity->original); + $current_sections = $this->getEntitySections($entity); + // Avoid un-needed conversion from revision IDs to block content IDs by + // first determining if there are any revisions in the original that are not + // also in the current sections. + $current_block_content_revision_ids = $this->getInlineBlockRevisionIdsInSections($current_sections); + $original_block_content_revision_ids = $this->getInlineBlockRevisionIdsInSections($original_sections); + if ($unused_original_revision_ids = array_diff($original_block_content_revision_ids, $current_block_content_revision_ids)) { + // If there are any revisions in the original that aren't in the current + // there may some blocks that need to be removed. + $current_block_content_ids = $this->getBlockIdsForRevisionIds($current_block_content_revision_ids); + $unused_original_block_content_ids = $this->getBlockIdsForRevisionIds($unused_original_revision_ids); + return array_diff($unused_original_block_content_ids, $current_block_content_ids); + } + return []; + } + + /** + * Handles entity tracking on deleting a parent entity. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The parent entity. + */ + public function handleEntityDelete(EntityInterface $entity) { + if ($this->isLayoutCompatibleEntity($entity)) { + $this->usage->removeByLayoutEntity($entity); + } + } + + /** + * Handles saving a parent entity. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The parent entity. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + * @throws \Drupal\Core\Entity\EntityStorageException + * @throws \Exception + */ + public function handlePreSave(EntityInterface $entity) { + if (!$this->isLayoutCompatibleEntity($entity)) { + return; + } + $duplicate_blocks = FALSE; + + if ($sections = $this->getEntitySections($entity)) { + if ($this->isEntityUsingFieldOverride($entity)) { + if (!$entity->isNew() && isset($entity->original)) { + if (empty($this->getEntitySections($entity->original))) { + // If there were no sections in the original entity then this is a + // new override from a default and the blocks need to be duplicated. + $duplicate_blocks = TRUE; + } + } + } + $new_revision = FALSE; + if ($entity instanceof RevisionableInterface) { + // If the parent entity will have a new revision create a new revision + // of the block. + // @todo Currently revisions are never created for the parent entity. + // This will be fixed in https://www.drupal.org/node/2937199. + // To work around this always make a revision when the parent entity is + // an instance of RevisionableInterface. After the issue is fixed only + // create a new revision if '$entity->isNewRevision()'. + $new_revision = TRUE; + } + + foreach ($this->getInlineBlockComponents($sections) as $component) { + $this->saveInlineBlockComponent($entity, $component, $new_revision, $duplicate_blocks); + } + } + $this->removeUnusedForEntityOnSave($entity); + } + + /** + * Gets a block ID for an inline block plugin. + * + * @param \Drupal\layout_builder\Plugin\Block\InlineBlockContentBlock $block_plugin + * The inline block plugin. + * + * @return int + * The block content ID or null none available. + */ + protected function getPluginBlockId(InlineBlockContentBlock $block_plugin) { + $configuration = $block_plugin->getConfiguration(); + if (!empty($configuration['block_revision_id'])) { + return array_pop($this->getBlockIdsForRevisionIds([$configuration['block_revision_id']])); + } + return NULL; + } + + /** + * Delete the inline blocks and the usage records. + * + * @param int[] $block_content_ids + * The block content entity IDs. + * + * @throws \Drupal\Core\Entity\EntityStorageException + */ + protected function deleteBlocksAndUsage(array $block_content_ids) { + foreach ($block_content_ids as $block_content_id) { + if ($block = $this->blockContentStorage->load($block_content_id)) { + $block->delete(); + } + } + $this->usage->deleteUsage($block_content_ids); + } + + /** + * Removes unused inline blocks. + * + * @param int $limit + * The maximum number of inline blocks to remove. + * + * @throws \Drupal\Core\Entity\EntityStorageException + */ + public function removeUnused($limit = 100) { + $this->deleteBlocksAndUsage($this->usage->getUnused($limit)); + } + + /** + * Gets blocks IDs for an array of revision IDs. + * + * @param int[] $revision_ids + * The revision IDs. + * + * @return int[] + * The block IDs. + */ + protected function getBlockIdsForRevisionIds(array $revision_ids) { + if ($revision_ids) { + $query = $this->blockContentStorage->getQuery(); + $query->condition('revision_id', $revision_ids, 'IN'); + $block_ids = $query->execute(); + return $block_ids; + } + return []; + } + + /** + * Saves an inline block component. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity with the layout. + * @param \Drupal\layout_builder\SectionComponent $component + * The section component with an inline block. + * @param bool $new_revision + * Whether a new revision of the block should be created. + * @param bool $duplicate_blocks + * Whether the blocks should be duplicated. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + * @throws \Drupal\Core\Entity\EntityStorageException + * @throws \Exception + */ + protected function saveInlineBlockComponent(EntityInterface $entity, SectionComponent $component, $new_revision, $duplicate_blocks) { + /** @var \Drupal\layout_builder\Plugin\Block\InlineBlockContentBlock $plugin */ + $plugin = $component->getPlugin(); + $pre_save_configuration = $plugin->getConfiguration(); + $plugin->saveBlockContent($new_revision, $duplicate_blocks); + $post_save_configuration = $plugin->getConfiguration(); + if ($duplicate_blocks || (empty($pre_save_configuration['block_revision_id']) && !empty($post_save_configuration['block_revision_id']))) { + $this->usage->addUsage($this->getPluginBlockId($plugin), $entity); + } + $component->setConfiguration($post_save_configuration); + } + +} diff --git a/core/modules/layout_builder/src/LayoutBuilderServiceProvider.php b/core/modules/layout_builder/src/LayoutBuilderServiceProvider.php new file mode 100644 index 0000000000..28e1e1f128 --- /dev/null +++ b/core/modules/layout_builder/src/LayoutBuilderServiceProvider.php @@ -0,0 +1,38 @@ +getParameter('container.modules'); + if (isset($modules['block_content'])) { + $definition = new Definition(SetInlineBlockDependency::class); + $definition->setArguments([ + new Reference('entity_type.manager'), + new Reference('database'), + new Reference('inline_block_content.usage'), + ]); + $definition->addTag('event_subscriber'); + $container->setDefinition('layout_builder.get_block_dependency_subscriber', $definition); + } + } + +} diff --git a/core/modules/layout_builder/src/LayoutEntityHelperTrait.php b/core/modules/layout_builder/src/LayoutEntityHelperTrait.php new file mode 100644 index 0000000000..e9921d8aff --- /dev/null +++ b/core/modules/layout_builder/src/LayoutEntityHelperTrait.php @@ -0,0 +1,107 @@ +getEntityTypeId() === 'entity_view_display' && $entity instanceof LayoutBuilderEntityViewDisplay) || + ($this->isEntityUsingFieldOverride($entity)); + } + + /** + * Gets revision IDs for layout sections. + * + * @param \Drupal\layout_builder\Section[] $sections + * The layout sections. + * + * @return int[] + * The revision IDs. + */ + protected function getInlineBlockRevisionIdsInSections(array $sections) { + $revision_ids = []; + foreach ($this->getInlineBlockComponents($sections) as $component) { + $configuration = $component->getPlugin()->getConfiguration(); + if (!empty($configuration['block_revision_id'])) { + $revision_ids[] = $configuration['block_revision_id']; + } + } + return $revision_ids; + } + + /** + * Gets the sections for an entity if any. + * + * @todo Replace this method with calls to the SectionStorageManagerInterface + * method for getting sections from an entity in + * https://www.drupal.org/node/2986403. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity. + * + * @return \Drupal\layout_builder\Section[]|null + * The entity layout sections if available. + */ + protected function getEntitySections(EntityInterface $entity) { + if ($entity->getEntityTypeId() === 'entity_view_display' && $entity instanceof LayoutBuilderEntityViewDisplay) { + return $entity->getSections(); + } + elseif ($this->isEntityUsingFieldOverride($entity)) { + return $entity->get('layout_builder__layout')->getSections(); + } + return NULL; + } + + /** + * Gets components that have Inline Block plugins. + * + * @param \Drupal\layout_builder\Section[] $sections + * The layout sections. + * + * @return \Drupal\layout_builder\SectionComponent[] + * The components that contain Inline Block plugins. + */ + protected function getInlineBlockComponents(array $sections) { + $inline_block_components = []; + foreach ($sections as $section) { + foreach ($section->getComponents() as $component) { + $plugin = $component->getPlugin(); + if ($plugin instanceof InlineBlockContentBlock) { + $inline_block_components[] = $component; + } + } + } + return $inline_block_components; + } + + /** + * Determines if an entity is using a field for the layout override. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity. + * + * @return bool + * TRUE if the entity is using a field for a layout override. + */ + protected function isEntityUsingFieldOverride(EntityInterface $entity) { + return $entity instanceof FieldableEntityInterface && $entity->hasField('layout_builder__layout'); + } + +} diff --git a/core/modules/layout_builder/src/Plugin/Block/InlineBlockContentBlock.php b/core/modules/layout_builder/src/Plugin/Block/InlineBlockContentBlock.php new file mode 100644 index 0000000000..80fd0a25c0 --- /dev/null +++ b/core/modules/layout_builder/src/Plugin/Block/InlineBlockContentBlock.php @@ -0,0 +1,287 @@ +entityTypeManager = $entity_type_manager; + $this->entityDisplayRepository = $entity_display_repository; + if (!empty($this->configuration['block_revision_id']) || !empty($this->configuration['block_serialized'])) { + $this->isNew = FALSE; + } + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_type.manager'), + $container->get('entity_display.repository') + ); + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return [ + 'view_mode' => 'full', + 'block_revision_id' => NULL, + 'block_serialized' => NULL, + ]; + } + + /** + * {@inheritdoc} + */ + public function blockForm($form, FormStateInterface $form_state) { + $block = $this->getEntity(); + + // Add the entity form display in a process callback so that #parents can + // be successfully propagated to field widgets. + $form['block_form'] = [ + '#type' => 'container', + '#process' => [[static::class, 'processBlockForm']], + '#block' => $block, + ]; + + $options = $this->entityDisplayRepository->getViewModeOptionsByBundle('block_content', $block->bundle()); + + $form['view_mode'] = [ + '#type' => 'select', + '#options' => $options, + '#title' => $this->t('View mode'), + '#description' => $this->t('The view mode in which to render the block.'), + '#default_value' => $this->configuration['view_mode'], + '#access' => count($options) > 1, + ]; + return $form; + } + + /** + * Process callback to insert a Custom Block form. + * + * @param array $element + * The containing element. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + * + * @return array + * The containing element, with the Custom Block form inserted. + */ + public static function processBlockForm(array $element, FormStateInterface $form_state) { + /** @var \Drupal\block_content\BlockContentInterface $block */ + $block = $element['#block']; + EntityFormDisplay::collectRenderDisplay($block, 'edit')->buildForm($block, $element, $form_state); + $element['revision_log']['#access'] = FALSE; + $element['info']['#access'] = FALSE; + return $element; + } + + /** + * {@inheritdoc} + */ + public function blockValidate($form, FormStateInterface $form_state) { + $block_form = $form['block_form']; + /** @var \Drupal\block_content\BlockContentInterface $block */ + $block = $block_form['#block']; + $form_display = EntityFormDisplay::collectRenderDisplay($block, 'edit'); + $complete_form_state = $form_state instanceof SubformStateInterface ? $form_state->getCompleteFormState() : $form_state; + $form_display->extractFormValues($block, $block_form, $complete_form_state); + $form_display->validateFormValues($block, $block_form, $complete_form_state); + // @todo Remove when https://www.drupal.org/project/drupal/issues/2948549 is closed. + $form_state->setTemporaryValue('block_form_parents', $block_form['#parents']); + } + + /** + * {@inheritdoc} + */ + public function blockSubmit($form, FormStateInterface $form_state) { + $this->configuration['view_mode'] = $form_state->getValue('view_mode'); + + // @todo Remove when https://www.drupal.org/project/drupal/issues/2948549 is closed. + $block_form = NestedArray::getValue($form, $form_state->getTemporaryValue('block_form_parents')); + /** @var \Drupal\block_content\BlockContentInterface $block */ + $block = $block_form['#block']; + $form_display = EntityFormDisplay::collectRenderDisplay($block, 'edit'); + $complete_form_state = $form_state instanceof SubformStateInterface ? $form_state->getCompleteFormState() : $form_state; + $form_display->extractFormValues($block, $block_form, $complete_form_state); + $block->setInfo($this->configuration['label']); + $this->configuration['block_serialized'] = serialize($block); + } + + /** + * {@inheritdoc} + */ + protected function blockAccess(AccountInterface $account) { + if ($entity = $this->getEntity()) { + return $entity->access('view', $account, TRUE); + } + return AccessResult::forbidden(); + } + + /** + * {@inheritdoc} + */ + public function build() { + $block = $this->getEntity(); + return $this->entityTypeManager->getViewBuilder($block->getEntityTypeId())->view($block, $this->configuration['view_mode']); + } + + /** + * Loads or creates the block content entity of the block. + * + * @return \Drupal\block_content\BlockContentInterface + * The block content entity. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + */ + protected function getEntity() { + if (!isset($this->blockContent)) { + if (!empty($this->configuration['block_serialized'])) { + $this->blockContent = unserialize($this->configuration['block_serialized']); + } + elseif (!empty($this->configuration['block_revision_id'])) { + $entity = $this->entityTypeManager->getStorage('block_content')->loadRevision($this->configuration['block_revision_id']); + $this->blockContent = $entity; + } + else { + $this->blockContent = $this->entityTypeManager->getStorage('block_content')->create([ + 'type' => $this->getDerivativeId(), + 'reusable' => FALSE, + ]); + } + if ($this->blockContent instanceof RefinableDependentAccessInterface && $dependee = $this->getAccessDependency()) { + $this->blockContent->setAccessDependency($dependee); + } + } + return $this->blockContent; + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $form = parent::buildConfigurationForm($form, $form_state); + if ($this->isNew) { + // If the Content Block is new then don't provide a default label. + unset($form['label']['#default_value']); + } + $form['label']['#description'] = $this->t('The title of the block as shown to the user.'); + return $form; + } + + /** + * Saves the block_content entity for this plugin. + * + * @param bool $new_revision + * Whether to create new revision. + * @param bool $duplicate_block + * Whether to duplicate the "block_content" entity. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + * @throws \Drupal\Core\Entity\EntityStorageException + */ + public function saveBlockContent($new_revision = FALSE, $duplicate_block = FALSE) { + /** @var \Drupal\block_content\BlockContentInterface $block */ + $block = NULL; + if (!empty($this->configuration['block_serialized'])) { + $block = unserialize($this->configuration['block_serialized']); + } + if ($duplicate_block) { + if (empty($block) && !empty($this->configuration['block_revision_id'])) { + $block = $this->entityTypeManager->getStorage('block_content')->loadRevision($this->configuration['block_revision_id']); + } + if ($block) { + $block = $block->createDuplicate(); + } + } + + if ($block) { + if ($new_revision) { + $block->setNewRevision(); + } + $block->save(); + $this->configuration['block_revision_id'] = $block->getRevisionId(); + $this->configuration['block_serialized'] = NULL; + } + } + +} diff --git a/core/modules/layout_builder/src/Plugin/Derivative/InlineBlockContentDeriver.php b/core/modules/layout_builder/src/Plugin/Derivative/InlineBlockContentDeriver.php new file mode 100644 index 0000000000..86e449b996 --- /dev/null +++ b/core/modules/layout_builder/src/Plugin/Derivative/InlineBlockContentDeriver.php @@ -0,0 +1,57 @@ +entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, $base_plugin_id) { + return new static( + $container->get('entity_type.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function getDerivativeDefinitions($base_plugin_definition) { + $this->derivatives = []; + if ($this->entityTypeManager->hasDefinition('block_content_type')) { + $block_content_types = $this->entityTypeManager->getStorage('block_content_type')->loadMultiple(); + foreach ($block_content_types as $id => $type) { + $this->derivatives[$id] = $base_plugin_definition; + $this->derivatives[$id]['admin_label'] = $type->label(); + $this->derivatives[$id]['config_dependencies'][$type->getConfigDependencyKey()][] = $type->getConfigDependencyName(); + } + } + return parent::getDerivativeDefinitions($base_plugin_definition); + } + +} diff --git a/core/modules/layout_builder/tests/modules/no_transitions_css/css/css_fix.theme.css b/core/modules/layout_builder/tests/modules/no_transitions_css/css/css_fix.theme.css new file mode 100644 index 0000000000..ffe0614396 --- /dev/null +++ b/core/modules/layout_builder/tests/modules/no_transitions_css/css/css_fix.theme.css @@ -0,0 +1,23 @@ +/** + * Remove all transitions for testing. + */ +* { + /* CSS transitions. */ + -o-transition-property: none !important; + -moz-transition-property: none !important; + -ms-transition-property: none !important; + -webkit-transition-property: none !important; + transition-property: none !important; + /* CSS transforms. */ + -o-transform: none !important; + -moz-transform: none !important; + -ms-transform: none !important; + -webkit-transform: none !important; + transform: none !important; + /* CSS animations. */ + -webkit-animation: none !important; + -moz-animation: none !important; + -o-animation: none !important; + -ms-animation: none !important; + animation: none !important; +} diff --git a/core/modules/layout_builder/tests/modules/no_transitions_css/no_transitions_css.info.yml b/core/modules/layout_builder/tests/modules/no_transitions_css/no_transitions_css.info.yml new file mode 100644 index 0000000000..80082db42e --- /dev/null +++ b/core/modules/layout_builder/tests/modules/no_transitions_css/no_transitions_css.info.yml @@ -0,0 +1,6 @@ +name: 'CSS Test fix' +type: module +description: 'Provides CSS fixes for tests.' +package: Testing +version: VERSION +core: 8.x diff --git a/core/modules/layout_builder/tests/modules/no_transitions_css/no_transitions_css.libraries.yml b/core/modules/layout_builder/tests/modules/no_transitions_css/no_transitions_css.libraries.yml new file mode 100644 index 0000000000..0fdaffd849 --- /dev/null +++ b/core/modules/layout_builder/tests/modules/no_transitions_css/no_transitions_css.libraries.yml @@ -0,0 +1,5 @@ +drupal.css_fix: + version: VERSION + css: + theme: + css/css_fix.theme.css: {} diff --git a/core/modules/layout_builder/tests/modules/no_transitions_css/no_transitions_css.module b/core/modules/layout_builder/tests/modules/no_transitions_css/no_transitions_css.module new file mode 100644 index 0000000000..3375399395 --- /dev/null +++ b/core/modules/layout_builder/tests/modules/no_transitions_css/no_transitions_css.module @@ -0,0 +1,16 @@ +linkExists('Save Layout'); $this->clickLink('Save Layout'); + $assert_session->pageTextContains('The layout has been saved.'); $assert_session->addressEquals("$field_ui_prefix/display/default"); // The node uses the defaults, no overrides available. diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockContentBlockTest.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockContentBlockTest.php new file mode 100644 index 0000000000..e17a658b5d --- /dev/null +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockContentBlockTest.php @@ -0,0 +1,422 @@ +assertSession(); + $page = $this->getSession()->getPage(); + + $this->drupalLogin($this->drupalCreateUser([ + 'access contextual links', + 'configure any layout', + 'administer node display', + 'administer node fields', + ])); + + $field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field'; + $this->drupalGet($field_ui_prefix . '/display/default'); + $this->clickLink('Manage layout'); + $assert_session->addressEquals("$field_ui_prefix/display-layout/default"); + // Add a basic block with the body field set. + $this->addInlineBlockToLayout('Block title', 'The DEFAULT block body'); + $this->assertSaveLayout(); + + $this->drupalGet('node/1'); + $assert_session->pageTextContains('The DEFAULT block body'); + $this->drupalGet('node/2'); + $assert_session->pageTextContains('The DEFAULT block body'); + + // Enable overrides. + $this->drupalPostForm("$field_ui_prefix/display/default", ['layout[allow_custom]' => TRUE], 'Save'); + $this->drupalGet('node/1/layout'); + + // Confirm the block can be edited. + $this->drupalGet('node/1/layout'); + $this->configureInlineBlock('The DEFAULT block body', 'The NEW block body!'); + $this->assertSaveLayout(); + $this->drupalGet('node/1'); + $assert_session->pageTextContains('The NEW block body'); + $assert_session->pageTextNotContains('The DEFAULT block body'); + $this->drupalGet('node/2'); + // Node 2 should use default layout. + $assert_session->pageTextContains('The DEFAULT block body'); + $assert_session->pageTextNotContains('The NEW block body'); + + // Add a basic block with the body field set. + $this->drupalGet('node/1/layout'); + $this->addInlineBlockToLayout('2nd Block title', 'The 2nd block body'); + $this->assertSaveLayout(); + $this->drupalGet('node/1'); + $assert_session->pageTextContains('The NEW block body!'); + $assert_session->pageTextContains('The 2nd block body'); + $this->drupalGet('node/2'); + // Node 2 should use default layout. + $assert_session->pageTextContains('The DEFAULT block body'); + $assert_session->pageTextNotContains('The NEW block body'); + $assert_session->pageTextNotContains('The 2nd block body'); + + // Confirm the block can be edited. + $this->drupalGet('node/1/layout'); + /* @var \Behat\Mink\Element\NodeElement $inline_block_2 */ + $inline_block_2 = $page->findAll('css', static::INLINE_BLOCK_LOCATOR)[1]; + $uuid = $inline_block_2->getAttribute('data-layout-block-uuid'); + $block_css_locator = static::INLINE_BLOCK_LOCATOR . "[data-layout-block-uuid=\"$uuid\"]"; + $this->configureInlineBlock('The 2nd block body', 'The 2nd NEW block body!', $block_css_locator); + $this->assertSaveLayout(); + $this->drupalGet('node/1'); + $assert_session->pageTextContains('The NEW block body!'); + $assert_session->pageTextContains('The 2nd NEW block body!'); + $this->drupalGet('node/2'); + // Node 2 should use default layout. + $assert_session->pageTextContains('The DEFAULT block body'); + $assert_session->pageTextNotContains('The NEW block body!'); + $assert_session->pageTextNotContains('The 2nd NEW block body!'); + + // The default layout entity block should be changed. + $this->drupalGet("$field_ui_prefix/display-layout/default"); + $assert_session->pageTextContains('The DEFAULT block body'); + // Confirm default layout still only has 1 entity block. + $assert_session->elementsCount('css', static::INLINE_BLOCK_LOCATOR, 1); + } + + /** + * Tests adding a new entity block and then not saving the layout. + * + * @dataProvider layoutNoSaveProvider + */ + public function testNoLayoutSave($operation, $no_save_link_text, $confirm_button_text) { + + $this->drupalLogin($this->drupalCreateUser([ + 'access contextual links', + 'configure any layout', + 'administer node display', + ])); + $assert_session = $this->assertSession(); + $page = $this->getSession()->getPage(); + $this->assertEmpty($this->blockStorage->loadMultiple(), 'No entity blocks exist'); + $field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field'; + // Enable overrides. + $this->drupalPostForm("$field_ui_prefix/display/default", ['layout[allow_custom]' => TRUE], 'Save'); + + $this->drupalGet('node/1/layout'); + $this->addInlineBlockToLayout('Block title', 'The block body'); + $this->clickLink($no_save_link_text); + if ($confirm_button_text) { + $page->pressButton($confirm_button_text); + } + $this->drupalGet('node/1'); + $this->assertEmpty($this->blockStorage->loadMultiple(), 'No entity blocks were created when layout is canceled.'); + $assert_session->pageTextNotContains('The block body'); + + $this->drupalGet('node/1/layout'); + + $this->addInlineBlockToLayout('Block title', 'The block body'); + $this->assertSaveLayout(); + $this->drupalGet('node/1'); + $assert_session->pageTextContains('The block body'); + $blocks = $this->blockStorage->loadMultiple(); + $this->assertEquals(count($blocks), 1); + /* @var \Drupal\Core\Entity\ContentEntityBase $block */ + $block = array_pop($blocks); + $revision_id = $block->getRevisionId(); + + // Confirm the block can be edited. + $this->drupalGet('node/1/layout'); + $this->configureInlineBlock('The block body', 'The block updated body'); + + $this->clickLink($no_save_link_text); + if ($confirm_button_text) { + $page->pressButton($confirm_button_text); + } + $this->drupalGet('node/1'); + + $blocks = $this->blockStorage->loadMultiple(); + // When reverting or canceling the update block should not be on the page. + $assert_session->pageTextNotContains('The block updated body'); + if ($operation === 'cancel') { + // When canceling the original block body should appear. + $assert_session->pageTextContains('The block body'); + + $this->assertEquals(count($blocks), 1); + $block = array_pop($blocks); + $this->assertEquals($block->getRevisionId(), $revision_id); + $this->assertEquals($block->get('body')->getValue()[0]['value'], 'The block body'); + } + else { + // The block should not be visible. + // Blocks are currently only deleted when the parent entity is deleted. + $assert_session->pageTextNotContains('The block body'); + } + } + + /** + * Provides test data for ::testNoLayoutSave(). + */ + public function layoutNoSaveProvider() { + return [ + 'cancel' => [ + 'cancel', + 'Cancel Layout', + NULL, + ], + 'revert' => [ + 'revert', + 'Revert to defaults', + 'Revert', + ], + ]; + } + + /** + * Tests entity blocks revisioning. + */ + public function testInlineBlocksRevisioning() { + $assert_session = $this->assertSession(); + $page = $this->getSession()->getPage(); + + $this->drupalLogin($this->drupalCreateUser([ + 'access contextual links', + 'configure any layout', + 'administer node display', + 'administer node fields', + 'administer nodes', + 'bypass node access', + ])); + + // Enable override. + $field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field'; + $this->drupalPostForm("$field_ui_prefix/display/default", ['layout[allow_custom]' => TRUE], 'Save'); + $this->drupalGet('node/1/layout'); + + // Add an inline block. + $this->addInlineBlockToLayout('Block title', 'The DEFAULT block body'); + $this->assertSaveLayout(); + $this->drupalGet('node/1'); + + $assert_session->pageTextContains('The DEFAULT block body'); + + /** @var \Drupal\node\NodeStorageInterface $node_storage */ + $node_storage = $this->container->get('entity_type.manager')->getStorage('node'); + $original_revision_id = $node_storage->getLatestRevisionId(1); + + // Create a new revision. + $this->drupalGet('node/1/edit'); + $page->findField('title[0][value]')->setValue('Node updated'); + $page->pressButton('Save'); + + $this->drupalGet('node/1'); + $assert_session->pageTextContains('The DEFAULT block body'); + + $assert_session->linkExists('Revisions'); + + // Update the block. + $this->drupalGet('node/1/layout'); + $this->configureInlineBlock('The DEFAULT block body', 'The NEW block body'); + $this->assertSaveLayout(); + $this->drupalGet('node/1'); + $assert_session->pageTextContains('The NEW block body'); + $assert_session->pageTextNotContains('The DEFAULT block body'); + + $revision_url = "node/1/revisions/$original_revision_id"; + + // Ensure viewing the previous revision shows the previous block revision. + $this->drupalGet("$revision_url/view"); + $assert_session->pageTextContains('The DEFAULT block body'); + $assert_session->pageTextNotContains('The NEW block body'); + + // Revert to first revision. + $revision_url = "$revision_url/revert"; + $this->drupalGet($revision_url); + $page->pressButton('Revert'); + + $this->drupalGet('node/1'); + $assert_session->pageTextContains('The DEFAULT block body'); + $assert_session->pageTextNotContains('The NEW block body'); + } + + /** + * Tests that entity blocks deleted correctly. + * + * @throws \Behat\Mink\Exception\ExpectationException + * @throws \Behat\Mink\Exception\ResponseTextException + */ + public function testDeletion() { + /** @var \Drupal\Core\Cron $cron */ + $cron = \Drupal::service('cron'); + /** @var \Drupal\layout_builder\InlineBlockContentUsage $usage */ + $usage = \Drupal::service('inline_block_content.usage'); + $this->drupalLogin($this->drupalCreateUser([ + 'administer content types', + 'access contextual links', + 'configure any layout', + 'administer node display', + 'administer node fields', + 'administer nodes', + 'bypass node access', + ])); + $assert_session = $this->assertSession(); + $page = $this->getSession()->getPage(); + + // Add a block to default layout. + $field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field'; + $this->drupalGet($field_ui_prefix . '/display/default'); + $this->clickLink('Manage layout'); + $assert_session->addressEquals("$field_ui_prefix/display-layout/default"); + $this->addInlineBlockToLayout('Block title', 'The DEFAULT block body'); + $this->assertSaveLayout(); + + $this->assertCount(1, $this->blockStorage->loadMultiple()); + $default_block_id = $this->getLatestBlockEntityId(); + + // Ensure the block shows up on node pages. + $this->drupalGet('node/1'); + $assert_session->pageTextContains('The DEFAULT block body'); + $this->drupalGet('node/2'); + $assert_session->pageTextContains('The DEFAULT block body'); + + // Enable overrides. + $this->drupalPostForm("$field_ui_prefix/display/default", ['layout[allow_custom]' => TRUE], 'Save'); + + // Ensure we have 2 copies of the block in node overrides. + $this->drupalGet('node/1/layout'); + $this->assertSaveLayout(); + $node_1_block_id = $this->getLatestBlockEntityId(); + + $this->drupalGet('node/2/layout'); + $this->assertSaveLayout(); + $node_2_block_id = $this->getLatestBlockEntityId(); + $this->assertCount(3, $this->blockStorage->loadMultiple()); + + $this->drupalGet($field_ui_prefix . '/display/default'); + $this->clickLink('Manage layout'); + $assert_session->addressEquals("$field_ui_prefix/display-layout/default"); + + $this->assertNotEmpty($this->blockStorage->load($default_block_id)); + $this->assertNotEmpty($usage->getUsage($default_block_id)); + // Remove block from default. + $this->removeInlineBlockFromLayout(); + $this->assertSaveLayout(); + // Ensure the block in the default was deleted. + $this->blockStorage->resetCache([$default_block_id]); + $this->assertEmpty($this->blockStorage->load($default_block_id)); + // Ensure other blocks still exist. + $this->assertCount(2, $this->blockStorage->loadMultiple()); + $this->assertEmpty($usage->getUsage($default_block_id)); + + $this->drupalGet('node/1/layout'); + $assert_session->pageTextContains('The DEFAULT block body'); + + $this->removeInlineBlockFromLayout(); + $this->assertSaveLayout(); + $cron->run(); + // Ensure entity block is not deleted because it is needed in revision. + $this->assertNotEmpty($this->blockStorage->load($node_1_block_id)); + $this->assertCount(2, $this->blockStorage->loadMultiple()); + + $this->assertNotEmpty($usage->getUsage($node_1_block_id)); + // Ensure entity block is deleted when node is deleted. + $this->drupalGet('node/1/delete'); + $page->pressButton('Delete'); + $this->assertEmpty(Node::load(1)); + $cron->run(); + $this->assertEmpty($this->blockStorage->load($node_1_block_id)); + $this->assertEmpty($usage->getUsage($node_1_block_id)); + $this->assertCount(1, $this->blockStorage->loadMultiple()); + + // Add another block to the default. + $this->drupalGet($field_ui_prefix . '/display/default'); + $this->clickLink('Manage layout'); + $assert_session->addressEquals("$field_ui_prefix/display-layout/default"); + $this->addInlineBlockToLayout('Title 2', 'Body 2'); + $this->assertSaveLayout(); + $cron->run(); + $default_block2_id = $this->getLatestBlockEntityId(); + $this->assertCount(2, $this->blockStorage->loadMultiple()); + + // Delete the other node so bundle can be deleted. + $this->assertNotEmpty($usage->getUsage($node_2_block_id)); + $this->drupalGet('node/2/delete'); + $page->pressButton('Delete'); + $this->assertEmpty(Node::load(2)); + $cron->run(); + // Ensure entity block was deleted. + $this->assertEmpty($this->blockStorage->load($node_2_block_id)); + $this->assertEmpty($usage->getUsage($node_2_block_id)); + $this->assertCount(1, $this->blockStorage->loadMultiple()); + + // Delete the bundle which has the default layout. + $this->assertNotEmpty($usage->getUsage($default_block2_id)); + $this->drupalGet("$field_ui_prefix/delete"); + $page->pressButton('Delete'); + $cron->run(); + + // Ensure the entity block in default is deleted when bundle is deleted. + $this->assertEmpty($this->blockStorage->load($default_block2_id)); + $this->assertEmpty($usage->getUsage($default_block2_id)); + $this->assertCount(0, $this->blockStorage->loadMultiple()); + } + + /** + * Tests access to the block edit form of inline blocks. + * + * This module does not provide links to these forms but in case the paths are + * accessed directly they should accessible by users with the + * 'configure any layout' permission. + * + * @see layout_builder_block_content_access() + * + * @throws \Behat\Mink\Exception\ElementNotFoundException + * @throws \Behat\Mink\Exception\ExpectationException + * @throws \Behat\Mink\Exception\ResponseTextException + */ + public function testAccess() { + $this->drupalLogin($this->drupalCreateUser([ + 'access contextual links', + 'configure any layout', + 'administer node display', + 'administer node fields', + ])); + $assert_session = $this->assertSession(); + $page = $this->getSession()->getPage(); + + // Add a block to default layout. + $field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field'; + $this->drupalPostForm("$field_ui_prefix/display/default", ['layout[allow_custom]' => TRUE], 'Save'); + + // Ensure we have 2 copies of the block in node overrides. + $this->drupalGet('node/1/layout'); + $this->addInlineBlockToLayout('Block title', 'Block body'); + $this->assertSaveLayout(); + $node_1_block_id = $this->getLatestBlockEntityId(); + + $this->drupalGet("block/$node_1_block_id"); + $assert_session->pageTextNotContains('You are not authorized to access this page'); + + $this->drupalLogout(); + $this->drupalLogin($this->drupalCreateUser([ + 'administer nodes', + ])); + + $this->drupalGet("block/$node_1_block_id"); + $assert_session->pageTextContains('You are not authorized to access this page'); + + $this->drupalLogin($this->drupalCreateUser([ + 'configure any layout', + ])); + $this->drupalGet("block/$node_1_block_id"); + $assert_session->pageTextNotContains('You are not authorized to access this page'); + } + +} diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockPrivateFilesTest.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockPrivateFilesTest.php new file mode 100644 index 0000000000..eb7344138b --- /dev/null +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockPrivateFilesTest.php @@ -0,0 +1,260 @@ + 'txt', + 'uri_scheme' => 'private', + ]; + $this->createFileField('field_file', 'block_content', 'basic', $field_settings); + $this->fileSystem = $this->container->get('file_system'); + } + + /** + * Test access to private files added via inline blocks in the layout builder. + * + * @throws \Behat\Mink\Exception\ElementNotFoundException + * @throws \Behat\Mink\Exception\ExpectationException + * @throws \Behat\Mink\Exception\ResponseTextException + * @throws \Drupal\Core\Entity\EntityStorageException + */ + public function testPrivateFiles() { + $assert_session = $this->assertSession(); + $this->drupalLogin($this->drupalCreateUser([ + 'access contextual links', + 'configure any layout', + 'administer node display', + 'administer node fields', + ])); + + $field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field'; + + // Enable overrides. + $this->drupalPostForm("$field_ui_prefix/display/default", ['layout[allow_custom]' => TRUE], 'Save'); + $this->drupalLogout(); + + // Log in as user you can only configure layouts and access content. + $this->drupalLogin($this->drupalCreateUser([ + 'access contextual links', + 'configure any layout', + 'access content', + ])); + $this->drupalGet('node/1/layout'); + $file = $this->createPrivateFile('drupal.txt'); + + $file_real_path = $this->fileSystem->realpath($file->getFileUri()); + $this->assertFileExists($file_real_path); + $this->addInlineFileBlockToLayout('The file', $file); + $this->assertSaveLayout(); + + $this->drupalGet('node/1'); + $private_href1 = $this->assertFileAccessibleOnNode($file); + + $this->drupalGet('node/1/layout'); + $this->removeInlineBlockFromLayout(); + $this->assertSaveLayout(); + + $this->drupalGet('node/1'); + $assert_session->pageTextNotContains($file->label()); + // Try to access file directly after it has been removed. + $this->drupalGet($private_href1); + $assert_session->pageTextContains('You are not authorized to access this page'); + $assert_session->pageTextNotContains($this->getFileSecret($file)); + $this->assertFileExists($file_real_path); + + $file2 = $this->createPrivateFile('2ndFile.txt'); + + $this->drupalGet('node/1/layout'); + $this->addInlineFileBlockToLayout('Number2', $file2); + $this->assertSaveLayout(); + + $this->drupalGet('node/1'); + $private_href2 = $this->assertFileAccessibleOnNode($file2); + + $node = Node::load(1); + $node->setTitle('Update node'); + $node->setNewRevision(); + $node->save(); + + $file3 = $this->createPrivateFile('3rdFile.txt'); + $this->drupalGet('node/1/layout'); + $this->replaceFileInBlock($file3); + $this->assertSaveLayout(); + + $this->drupalGet('node/1'); + $private_href3 = $this->assertFileAccessibleOnNode($file3); + + $this->drupalGet($private_href2); + $assert_session->pageTextContains('You are not authorized to access this page'); + + $node->setUnpublished(); + $node->save(); + $this->drupalGet('node/1'); + $assert_session->pageTextContains('You are not authorized to access this page'); + $this->drupalGet($private_href3); + $assert_session->pageTextNotContains($this->getFileSecret($file3)); + $assert_session->pageTextContains('You are not authorized to access this page'); + } + + /** + * Replaces the file in the block with another one. + * + * @param \Drupal\file\FileInterface $file + * The file entity. + * + * @throws \Behat\Mink\Exception\ElementNotFoundException + * @throws \Behat\Mink\Exception\ExpectationException + */ + protected function replaceFileInBlock(FileInterface $file) { + $assert_session = $this->assertSession(); + $page = $this->getSession()->getPage(); + $this->clickContextualLink(static::INLINE_BLOCK_LOCATOR, 'Configure'); + $assert_session->assertWaitOnAjaxRequest(); + $page->pressButton('Remove'); + $assert_session->assertWaitOnAjaxRequest(); + $this->attachFileToBlockForm($file); + $page->pressButton('Update'); + $this->assertDialogClosedAndTextVisible($file->label(), static::INLINE_BLOCK_LOCATOR); + } + + /** + * Adds an entity block with a file. + * + * @param string $title + * The title field value. + * @param \Drupal\file\Entity\File $file + * The file entity. + * + * @throws \Behat\Mink\Exception\ElementNotFoundException + * @throws \Behat\Mink\Exception\ExpectationException + */ + protected function addInlineFileBlockToLayout($title, File $file) { + $assert_session = $this->assertSession(); + $page = $this->getSession()->getPage(); + $page->clickLink('Add Block'); + $assert_session->assertWaitOnAjaxRequest(); + $this->assertNotEmpty($assert_session->waitForElementVisible('css', '.block-categories details:contains(Create new block)')); + $this->clickLink('Basic block'); + $assert_session->assertWaitOnAjaxRequest(); + $assert_session->fieldValueEquals('Title', ''); + $page->findField('Title')->setValue($title); + $this->attachFileToBlockForm($file); + $page->pressButton('Add Block'); + $this->assertDialogClosedAndTextVisible($file->label(), static::INLINE_BLOCK_LOCATOR); + } + + /** + * Creates a private file. + * + * @param string $file_name + * The file name. + * + * @return \Drupal\Core\Entity\EntityInterface|\Drupal\file\Entity\File + * The file entity. + * + * @throws \Drupal\Core\Entity\EntityStorageException + */ + protected function createPrivateFile($file_name) { + // Create a new file entity. + $file = File::create([ + 'uid' => 1, + 'filename' => $file_name, + 'uri' => "private://$file_name", + 'filemime' => 'text/plain', + 'status' => FILE_STATUS_PERMANENT, + ]); + file_put_contents($file->getFileUri(), $this->getFileSecret($file)); + $file->save(); + return $file; + } + + /** + * Asserts a file is accessible on the page. + * + * @param \Drupal\file\FileInterface $file + * The file entity. + * + * @return string + * The file href. + * + * @throws \Behat\Mink\Exception\ExpectationException + */ + protected function assertFileAccessibleOnNode(FileInterface $file) { + $assert_session = $this->assertSession(); + $page = $this->getSession()->getPage(); + $assert_session->linkExists($file->label()); + $private_href = $page->findLink($file->label())->getAttribute('href'); + $page->clickLink($file->label()); + $assert_session->pageTextContains($this->getFileSecret($file)); + + // Access file directly. + $this->drupalGet($private_href); + $assert_session->pageTextContains($this->getFileSecret($file)); + return $private_href; + } + + /** + * Gets the text secret for a file. + * + * @param \Drupal\file\FileInterface $file + * The file entity. + * + * @return string + * The text secret. + */ + protected function getFileSecret(FileInterface $file) { + return "The secret in {$file->label()}"; + } + + /** + * Attaches a file to the block edit form. + * + * @param \Drupal\file\FileInterface $file + * The file to be attached. + * + * @throws \Behat\Mink\Exception\ElementNotFoundException + */ + protected function attachFileToBlockForm(FileInterface $file) { + $assert_session = $this->assertSession(); + $page = $this->getSession()->getPage(); + $page->attachFileToField("files[settings_block_form_field_file_0]", $this->fileSystem->realpath($file->getFileUri())); + $assert_session->assertWaitOnAjaxRequest(); + $this->assertNotEmpty($assert_session->waitForLink($file->label())); + } + +} diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockTestBase.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockTestBase.php new file mode 100644 index 0000000000..d4792b76e1 --- /dev/null +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockTestBase.php @@ -0,0 +1,225 @@ +drupalPlaceBlock('local_tasks_block'); + + $this->createContentType(['type' => 'bundle_with_section_field', 'new_revision' => TRUE]); + $this->createNode([ + 'type' => 'bundle_with_section_field', + 'title' => 'The node title', + 'body' => [ + [ + 'value' => 'The node body', + ], + ], + ]); + $this->createNode([ + 'type' => 'bundle_with_section_field', + 'title' => 'The node2 title', + 'body' => [ + [ + 'value' => 'The node2 body', + ], + ], + ]); + $bundle = BlockContentType::create([ + 'id' => 'basic', + 'label' => 'Basic block', + 'revision' => 1, + ]); + $bundle->save(); + block_content_add_body_field($bundle->id()); + + $this->blockStorage = $this->container->get('entity_type.manager')->getStorage('block_content'); + } + + /** + * Saves a layout and asserts the message is correct. + * + * @throws \Behat\Mink\Exception\ExpectationException + * @throws \Behat\Mink\Exception\ResponseTextException + */ + protected function assertSaveLayout() { + $assert_session = $this->assertSession(); + $assert_session->linkExists('Save Layout'); + // Go to the Save Layout page. Currently there are random test failures if + // 'clickLink()' is used. + // @todo Convert tests that extend this class to NightWatch tests in + // https://www.drupal.org/node/2984161 + $link = $this->getSession()->getPage()->findLink('Save Layout'); + $this->drupalGet($link->getAttribute('href')); + $this->assertNotEmpty($assert_session->waitForElement('css', '.messages--status')); + + if (stristr($this->getUrl(), 'admin/structure') === FALSE) { + $assert_session->pageTextContains('The layout override has been saved.'); + } + else { + $assert_session->pageTextContains('The layout has been saved.'); + } + } + + /** + * Gets the latest block entity id. + */ + protected function getLatestBlockEntityId() { + $block_ids = \Drupal::entityQuery('block_content')->sort('id', 'DESC')->range(0, 1)->execute(); + $block_id = array_pop($block_ids); + $this->assertNotEmpty($this->blockStorage->load($block_id)); + return $block_id; + } + + /** + * Removes an entity block from the layout but does not save the layout. + */ + protected function removeInlineBlockFromLayout() { + $assert_session = $this->assertSession(); + $page = $this->getSession()->getPage(); + $block_text = $page->find('css', static::INLINE_BLOCK_LOCATOR)->getText(); + $this->assertNotEmpty($block_text); + $assert_session->pageTextContains($block_text); + $this->clickContextualLink(static::INLINE_BLOCK_LOCATOR, 'Remove block'); + $assert_session->waitForElement('css', "#drupal-off-canvas input[value='Remove']"); + $assert_session->assertWaitOnAjaxRequest(); + $page->find('css', '#drupal-off-canvas')->pressButton('Remove'); + $this->waitForNoElement('#drupal-off-canvas'); + $this->waitForNoElement(static::INLINE_BLOCK_LOCATOR); + $assert_session->assertWaitOnAjaxRequest(); + $assert_session->pageTextNotContains($block_text); + } + + /** + * Adds an entity block to the layout. + * + * @param string $title + * The title field value. + * @param string $body + * The body field value. + * + * @throws \Behat\Mink\Exception\ElementNotFoundException + * @throws \Behat\Mink\Exception\ExpectationException + */ + protected function addInlineBlockToLayout($title, $body) { + $assert_session = $this->assertSession(); + $page = $this->getSession()->getPage(); + $page->clickLink('Add Block'); + $assert_session->assertWaitOnAjaxRequest(); + $this->assertNotEmpty($assert_session->waitForElementVisible('css', '.block-categories details:contains(Create new block)')); + $this->clickLink('Basic block'); + $assert_session->assertWaitOnAjaxRequest(); + $textarea = $assert_session->waitForElement('css', '[name="settings[block_form][body][0][value]"]'); + $this->assertNotEmpty($textarea); + $assert_session->fieldValueEquals('Title', ''); + $page->findField('Title')->setValue($title); + $textarea->setValue($body); + $page->pressButton('Add Block'); + $this->assertDialogClosedAndTextVisible($body, static::INLINE_BLOCK_LOCATOR); + } + + /** + * Configures an inline block in the Layout Builder. + * + * @param string $old_body + * The old body field value. + * @param string $new_body + * The new body field value. + * @param string $block_css_locator + * The CSS locator to use to select the contextual link. + */ + protected function configureInlineBlock($old_body, $new_body, $block_css_locator = NULL) { + $block_css_locator = $block_css_locator ?: static::INLINE_BLOCK_LOCATOR; + $assert_session = $this->assertSession(); + $page = $this->getSession()->getPage(); + $this->clickContextualLink($block_css_locator, 'Configure'); + $textarea = $assert_session->waitForElementVisible('css', '[name="settings[block_form][body][0][value]"]'); + $this->assertNotEmpty($textarea); + $this->assertSame($old_body, $textarea->getValue()); + $textarea->setValue($new_body); + $page->pressButton('Update'); + $this->waitForNoElement('#drupal-off-canvas'); + $assert_session->assertWaitOnAjaxRequest(); + $this->assertDialogClosedAndTextVisible($new_body); + } + + /** + * Waits for an element to be removed from the page. + * + * @param string $selector + * CSS selector. + * @param int $timeout + * (optional) Timeout in milliseconds, defaults to 10000. + * + * @todo Remove in https://www.drupal.org/node/2892440. + */ + protected function waitForNoElement($selector, $timeout = 10000) { + $condition = "(typeof jQuery !== 'undefined' && jQuery('$selector').length === 0)"; + $this->assertJsCondition($condition, $timeout); + } + + /** + * Asserts that the dialog closes and the new text appears on the main canvas. + * + * @param string $text + * The text. + * @param string|null $css_locator + * The css locator to use inside the main canvas if any. + * + * @throws \Behat\Mink\Exception\ExpectationException + */ + protected function assertDialogClosedAndTextVisible($text, $css_locator = NULL) { + $assert_session = $this->assertSession(); + $this->waitForNoElement('#drupal-off-canvas'); + $assert_session->assertWaitOnAjaxRequest(); + $assert_session->elementNotExists('css', '#drupal-off-canvas'); + if ($css_locator) { + $this->assertNotEmpty($assert_session->waitForElementVisible('css', ".dialog-off-canvas-main-canvas $css_locator:contains('$text')")); + } + else { + $this->assertNotEmpty($assert_session->waitForElementVisible('css', ".dialog-off-canvas-main-canvas:contains('$text')")); + } + } + +} diff --git a/core/modules/layout_builder/tests/src/Unit/BlockComponentRenderArrayTest.php b/core/modules/layout_builder/tests/src/Unit/BlockComponentRenderArrayTest.php index 6571a8ff1a..eb7f50f745 100644 --- a/core/modules/layout_builder/tests/src/Unit/BlockComponentRenderArrayTest.php +++ b/core/modules/layout_builder/tests/src/Unit/BlockComponentRenderArrayTest.php @@ -2,12 +2,17 @@ namespace Drupal\Tests\layout_builder\Unit; +use Drupal\block_content\Access\RefinableDependentAccessInterface; +use Drupal\Component\Plugin\Context\ContextInterface; use Drupal\Core\Access\AccessResult; use Drupal\Core\Block\BlockManagerInterface; use Drupal\Core\Block\BlockPluginInterface; use Drupal\Core\Cache\Cache; use Drupal\Core\DependencyInjection\ContainerBuilder; +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Plugin\Context\ContextHandlerInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\layout_builder\Access\LayoutPreviewAccessAllowed; use Drupal\layout_builder\Event\SectionComponentBuildRenderArrayEvent; use Drupal\layout_builder\EventSubscriber\BlockComponentRenderArray; use Drupal\layout_builder\SectionComponent; @@ -33,6 +38,16 @@ class BlockComponentRenderArrayTest extends UnitTestCase { */ protected $blockManager; + /** + * Dataprovider for test functions that should test block types. + */ + public function providerBlockTypes() { + return [ + [TRUE], + [FALSE], + ]; + } + /** * {@inheritdoc} */ @@ -44,14 +59,32 @@ protected function setUp() { $container = new ContainerBuilder(); $container->set('plugin.manager.block', $this->blockManager->reveal()); + $container->set('context.handler', $this->prophesize(ContextHandlerInterface::class)); \Drupal::setContainer($container); } /** * @covers ::onBuildRender + * + * @dataProvider providerBlockTypes */ - public function testOnBuildRender() { - $block = $this->prophesize(BlockPluginInterface::class); + public function testOnBuildRender($refinable_dependent_access) { + $contexts = []; + if ($refinable_dependent_access) { + $block = $this->prophesize(TestBlockPluginWithRefinableDependentAccessInterface::class); + + + $layout_entity = $this->prophesize(EntityInterface::class); + $layout_entity = $layout_entity->reveal(); + $context = $this->prophesize(ContextInterface::class); + $context->getContextValue()->willReturn($layout_entity); + $contexts['layout_builder.entity'] = $context->reveal(); + + $block->setAccessDependency($layout_entity)->shouldBeCalled(); + } + else { + $block = $this->prophesize(BlockPluginInterface::class); + } $access_result = AccessResult::allowed(); $block->access($this->account->reveal(), TRUE)->willReturn($access_result)->shouldBeCalled(); $block->getCacheContexts()->willReturn([]); @@ -67,7 +100,6 @@ public function testOnBuildRender() { $this->blockManager->createInstance('some_block_id', ['id' => 'some_block_id'])->willReturn($block->reveal()); $component = new SectionComponent('some-uuid', 'some-region', ['id' => 'some_block_id']); - $contexts = []; $in_preview = FALSE; $event = new SectionComponentBuildRenderArrayEvent($component, $contexts, $in_preview); @@ -100,9 +132,26 @@ public function testOnBuildRender() { /** * @covers ::onBuildRender + * + * @dataProvider providerBlockTypes */ - public function testOnBuildRenderDenied() { - $block = $this->prophesize(BlockPluginInterface::class); + public function testOnBuildRenderDenied($refinable_dependent_access) { + $contexts = []; + if ($refinable_dependent_access) { + $block = $this->prophesize(TestBlockPluginWithRefinableDependentAccessInterface::class); + + $layout_entity = $this->prophesize(EntityInterface::class); + $layout_entity = $layout_entity->reveal(); + $context = $this->prophesize(ContextInterface::class); + $context->getContextValue()->willReturn($layout_entity); + $contexts['layout_builder.entity'] = $context->reveal(); + + $block->setAccessDependency($layout_entity)->shouldBeCalled(); + } + else { + $block = $this->prophesize(BlockPluginInterface::class); + } + $access_result = AccessResult::forbidden(); $block->access($this->account->reveal(), TRUE)->willReturn($access_result)->shouldBeCalled(); $block->getCacheContexts()->shouldNotBeCalled(); @@ -118,7 +167,6 @@ public function testOnBuildRenderDenied() { $this->blockManager->createInstance('some_block_id', ['id' => 'some_block_id'])->willReturn($block->reveal()); $component = new SectionComponent('some-uuid', 'some-region', ['id' => 'some_block_id']); - $contexts = []; $in_preview = FALSE; $event = new SectionComponentBuildRenderArrayEvent($component, $contexts, $in_preview); @@ -142,9 +190,26 @@ public function testOnBuildRenderDenied() { /** * @covers ::onBuildRender + * + * @dataProvider providerBlockTypes */ - public function testOnBuildRenderInPreview() { - $block = $this->prophesize(BlockPluginInterface::class); + public function testOnBuildRenderInPreview($refinable_dependent_access) { + $contexts = []; + if ($refinable_dependent_access) { + $block = $this->prophesize(TestBlockPluginWithRefinableDependentAccessInterface::class); + $block->setAccessDependency(new LayoutPreviewAccessAllowed())->shouldBeCalled(); + + $layout_entity = $this->prophesize(EntityInterface::class); + $layout_entity = $layout_entity->reveal(); + $layout_entity->in_preview = TRUE; + $context = $this->prophesize(ContextInterface::class); + $context->getContextValue()->willReturn($layout_entity); + $contexts['layout_builder.entity'] = $context->reveal(); + } + else { + $block = $this->prophesize(BlockPluginInterface::class); + } + $block->access($this->account->reveal(), TRUE)->shouldNotBeCalled(); $block->getCacheContexts()->willReturn([]); $block->getCacheTags()->willReturn(['test']); @@ -159,7 +224,6 @@ public function testOnBuildRenderInPreview() { $this->blockManager->createInstance('some_block_id', ['id' => 'some_block_id'])->willReturn($block->reveal()); $component = new SectionComponent('some-uuid', 'some-region', ['id' => 'some_block_id']); - $contexts = []; $in_preview = TRUE; $event = new SectionComponentBuildRenderArrayEvent($component, $contexts, $in_preview); @@ -220,3 +284,10 @@ public function testOnBuildRenderNoBlock() { } } + +/** + * Test interface for dependent access block plugins. + */ +interface TestBlockPluginWithRefinableDependentAccessInterface extends BlockPluginInterface, RefinableDependentAccessInterface { + +}