Loading src/Plugin/SingleContentSyncFieldProcessor/LayoutSection.php +5 −1 Changes for src/Plugin/SingleContentSyncFieldProcessor/LayoutSection.php: 5 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -232,11 +232,15 @@ class LayoutSection extends SingleContentSyncFieldProcessorPluginBase implements foreach ($section_components as $component) { if ($component->getPlugin() instanceof InlineBlock) { $configuration = $component->toArray()['configuration']; // Imported inline blocks use structured 'blocks' data and revision // references. Never pass a serialized object graph from an import // to core's InlineBlock plugin. unset($configuration['block_serialized']); if (isset($configuration['block_revision_id']) && isset($imported_blocks[$configuration['block_revision_id']])) { // Replace the old revision id with a new revision id. $configuration['block_revision_id'] = $imported_blocks[$configuration['block_revision_id']]; $component->setConfiguration($configuration); } $component->setConfiguration($configuration); } } } Loading tests/src/Kernel/Fixtures/SerializedBlockCanary.php 0 → 100644 +29 −0 Changes for tests/src/Kernel/Fixtures/SerializedBlockCanary.php: 29 added lines, 0 removed lines. Original line number Diff line number Diff line <?php declare(strict_types=1); namespace Drupal\Tests\single_content_sync\Kernel\Fixtures; /** * Test-only object used to detect an unrestricted unserialize(). * * When an instance of this class is unserialized, __wakeup() flips a static * flag. The tests serialize one of these, place it where a block object would * live, and assert whether it is (or is not) reconstructed downstream. The * class itself is inert; it stands in for a real gadget object. */ class SerializedBlockCanary { /** * Set to TRUE the moment an instance is unserialized. */ public static bool $reconstructed = FALSE; /** * Records that an instance was rebuilt from serialized bytes. */ public function __wakeup(): void { self::$reconstructed = TRUE; } } tests/src/Kernel/LayoutSectionImportTest.php 0 → 100644 +345 −0 Changes for tests/src/Kernel/LayoutSectionImportTest.php: 345 added lines, 0 removed lines. Original line number Diff line number Diff line <?php declare(strict_types=1); namespace Drupal\Tests\single_content_sync\Kernel; use Drupal\block_content\Entity\BlockContent; use Drupal\block_content\Entity\BlockContentType; use Drupal\KernelTests\KernelTestBase; use Drupal\layout_builder\Plugin\Block\InlineBlock; use Drupal\layout_builder\Section; use Drupal\layout_builder\SectionComponent; use Drupal\node\Entity\Node; use Drupal\single_content_sync\Plugin\SingleContentSyncFieldProcessor\LayoutSection; use Drupal\Tests\node\Traits\ContentTypeCreationTrait; use Drupal\Tests\single_content_sync\Kernel\Fixtures\SerializedBlockCanary; /** * Covers how the layout_section import handles inline block configuration. * * The importer rebuilds each Layout Builder component from the (fully * attacker-controlled) import payload via Section::fromArray(). Core's * InlineBlock later reads the 'block_serialized' key back with a bare * unserialize(), so that key must not survive an import, while the legitimate * inline-block reference must round-trip intact. * * @group single_content_sync * @coversDefaultClass \Drupal\single_content_sync\Plugin\SingleContentSyncFieldProcessor\LayoutSection * @runTestsInSeparateProcesses * @preserveGlobalState disabled */ class LayoutSectionImportTest extends KernelTestBase { use ContentTypeCreationTrait; /** * {@inheritdoc} */ protected static $modules = [ 'single_content_sync', 'field', 'file', 'node', 'system', 'text', 'user', 'block_content', 'layout_discovery', 'layout_builder', ]; /** * A serialized stand-in object placed where a block object would be. */ protected string $serializedPayload; /** * {@inheritdoc} */ protected function setUp(): void { parent::setUp(); $this->installEntitySchema('node'); $this->installEntitySchema('user'); $this->installEntitySchema('block_content'); $this->installSchema('layout_builder', ['inline_block_usage']); $this->installSchema('node', ['node_access']); $this->installConfig(['node', 'system']); $this->createContentType(['type' => 'article']); // The 'basic' bundle makes core's 'inline_block:basic' plugin available, // which is the plugin that owns the unserialize() sink under test. BlockContentType::create(['id' => 'basic', 'label' => 'Basic'])->save(); // Enable Layout Builder overrides on the article view display; this is // what creates the layout_builder__layout field the import writes to. \Drupal::service('entity_display.repository') ->getViewDisplay('node', 'article', 'default') ->enableLayoutBuilder() ->setOverridable() ->save(); SerializedBlockCanary::$reconstructed = FALSE; $this->serializedPayload = serialize(new SerializedBlockCanary()); } /** * A crafted block_serialized is neutralized; the core sink becomes inert. * * Both current and legacy layouts must discard the serialized data before * reaching core's save or read paths. This also works with hardened core. * * @covers ::importFieldValue * @dataProvider sectionFormats */ public function testCraftedSerializedBlockIsNeutralized(string $format): void { $uuid = '10000000-0000-1000-a000-000000000000'; $component = new SectionComponent($uuid, 'content', [ 'id' => 'inline_block:basic', 'label' => 'x', 'label_display' => '0', 'provider' => 'layout_builder', 'view_mode' => 'full', 'block_serialized' => $this->serializedPayload, ]); $section = new Section('layout_onecol', [], [$component]); // No 'blocks' behind the section: the inline block was never really // exported, it is a raw serialized payload smuggled in the section data. $value = [ 'blocks' => [], 'sections' => $this->encodeSections([$section], $format), ]; $node = Node::create(['type' => 'article', 'title' => 'Target']); $this->processor()->importFieldValue($node, 'layout_builder__layout', $value); $configuration = $this->componentConfiguration($node, $uuid); $this->assertArrayNotHasKey( 'block_serialized', $configuration, 'The imported layout must not carry a serialized block object.' ); // Exercise the presave path before checking core's read path. $node->save(); $this->container->get('entity_type.manager')->getStorage('node')->resetCache(); $node = Node::load($node->id()); $configuration = $this->componentConfiguration($node, $uuid); $this->assertEmpty($configuration['block_serialized'] ?? NULL); $this->driveCoreSink($configuration); $this->assertFalse( SerializedBlockCanary::$reconstructed, 'After import the core sink reconstructs nothing.' ); } /** * A real inline block round-trips through export/import after the strip. * * A legitimately exported inline block travels as an entry in the 'blocks' * list and is referenced from the section by block_revision_id. Dropping * block_serialized on import must leave that path intact: the block content * survives and the component still points at it. * * @covers ::exportFieldValue * @covers ::importFieldValue * @dataProvider sectionFormats */ public function testInlineBlockRoundTripSurvivesStrip(string $format): void { $block = BlockContent::create([ 'type' => 'basic', 'info' => 'ROUNDTRIP-9931', ]); $block->save(); // The persisted state of an inline block: referenced by revision id, with // no serialized blob. $uuid = '20000000-0000-1000-a000-000000000000'; $component = new SectionComponent($uuid, 'content', [ 'id' => 'inline_block:basic', 'label' => 'x', 'label_display' => '0', 'provider' => 'layout_builder', 'view_mode' => 'full', 'block_revision_id' => $block->getRevisionId(), ]); $section = new Section('layout_onecol', [], [$component]); $source = Node::create(['type' => 'article', 'title' => 'Source']); $source->set('layout_builder__layout', [['section' => $section]]); $source->save(); // Export through the module, exactly as the real feature does. A // legitimate inline block travels as an entry in the 'blocks' list and is // referenced from the section by block_revision_id; its block_serialized // slot is empty. (Only a crafted import supplies a non-empty blob there.) $exported = $this->processor()->exportFieldValue($source->get('layout_builder__layout')); $this->assertCount(1, $exported['blocks'], 'The inline block is exported as an entity.'); $exported_config = $exported['sections'][0]['components'][$uuid]['configuration']; $this->assertNotEmpty( $exported_config['block_revision_id'], 'The exported inline block is referenced by revision id.' ); $this->assertEmpty( $exported_config['block_serialized'] ?? NULL, 'A legitimate export carries no serialized block blob.' ); // Require remapping of a revision ID from another site. A malicious blob // alongside a legitimate block must not override its structured content. $old_revision_id = 999999; $exported['blocks'][0]['base_fields']['block_revision_id'] = $old_revision_id; $exported['sections'][0]['components'][$uuid]['configuration']['block_revision_id'] = $old_revision_id; $exported['sections'][0]['components'][$uuid]['configuration']['block_serialized'] = $this->serializedPayload; $sections = array_map([Section::class, 'fromArray'], $exported['sections']); $exported['sections'] = $this->encodeSections($sections, $format); // Import into a fresh node. $target = Node::create(['type' => 'article', 'title' => 'Target']); $this->processor()->importFieldValue($target, 'layout_builder__layout', $exported); $configuration = $this->componentConfiguration($target, $uuid); $this->assertArrayNotHasKey('block_serialized', $configuration); $this->assertNotEquals($old_revision_id, $configuration['block_revision_id']); $target->save(); \Drupal::entityTypeManager()->getStorage('node')->resetCache(); $target = Node::load($target->id()); // After the node is saved, core's own inline-block handling re-adds an // empty block_serialized slot; the invariant is that it is never a blob. $configuration = $this->componentConfiguration($target, $uuid); $this->assertEmpty( $configuration['block_serialized'] ?? NULL, 'The round-tripped inline block carries no serialized blob.' ); $this->assertNotEmpty( $configuration['block_revision_id'], 'The imported component still references an inline block revision.' ); // The referenced revision resolves to the round-tripped block content. $imported_block = \Drupal::entityTypeManager() ->getStorage('block_content') ->loadRevision($configuration['block_revision_id']); $this->assertNotNull($imported_block, 'The referenced block revision exists.'); $this->assertEquals('ROUNDTRIP-9931', $imported_block->get('info')->value); $this->assertFalse(SerializedBlockCanary::$reconstructed); } /** * The complete import service sanitizes layouts on creation and update. * * @covers ::importFieldValue * @dataProvider sectionFormats */ public function testContentImportRemovesSerializedBlock(string $format): void { $uuid = $this->container->get('uuid')->generate(); $component_uuid = $this->container->get('uuid')->generate(); $section = new Section('layout_onecol', [], [ new SectionComponent($component_uuid, 'content', [ 'id' => 'inline_block:basic', 'block_serialized' => $this->serializedPayload, ]), ]); $content = [ 'entity_type' => 'node', 'bundle' => 'article', 'uuid' => $uuid, 'base_fields' => [ 'title' => 'Imported layout', 'langcode' => 'en', 'created' => 123456789, 'status' => TRUE, ], 'custom_fields' => [ 'layout_builder__layout' => [ 'blocks' => [], 'sections' => $this->encodeSections([$section], $format), ], ], ]; $node_id = NULL; foreach (['Create', 'Update'] as $operation) { $content['base_fields']['title'] = $operation; $node = $this->container->get('single_content_sync.importer')->doImport($content); if ($node_id !== NULL) { $this->assertEquals($node_id, $node->id()); } $node_id = $node->id(); $this->container->get('entity_type.manager')->getStorage('node')->resetCache(); $node = Node::load($node_id); $this->assertEquals($operation, $node->label()); $this->assertEmpty($this->componentConfiguration($node, $component_uuid)['block_serialized'] ?? NULL); $this->assertFalse(SerializedBlockCanary::$reconstructed); } } /** * Provides the supported layout export formats. */ public static function sectionFormats(): array { return [ 'structured arrays' => ['array'], 'legacy serialized sections' => ['legacy'], ]; } /** * Encodes sections using the current or legacy export format. */ protected function encodeSections(array $sections, string $format) { return $format === 'legacy' ? base64_encode(implode('|', array_map('serialize', $sections))) : array_map(static function (Section $section): array { return $section->toArray(); }, $sections); } /** * Returns the layout_section field processor under test. */ protected function processor(): LayoutSection { return $this->container ->get('plugin.manager.single_content_sync_field_processor') ->createInstance('layout_section'); } /** * Returns the stored configuration of the first component of a node layout. */ protected function componentConfiguration(Node $node, string $uuid): array { $sections = $node->get('layout_builder__layout')->getSections(); $this->assertCount(1, $sections); $component = $sections[0]->getComponent($uuid); $this->assertNotNull($component); return $component->get('configuration'); } /** * Feeds a component configuration to core's InlineBlock unserialize sink. * * The getEntity() method reads serialized block data; saveBlockContent() * reads it during presave. Drive the protected method directly to check * the read path without depending on a full render pipeline. */ protected function driveCoreSink(array $configuration): void { $configuration += [ 'id' => 'inline_block:basic', 'label' => 'x', 'label_display' => '0', 'provider' => 'layout_builder', 'view_mode' => 'full', ]; /** @var \Drupal\layout_builder\Plugin\Block\InlineBlock $plugin */ $plugin = $this->container->get('plugin.manager.block') ->createInstance('inline_block:basic', $configuration); $this->assertInstanceOf(InlineBlock::class, $plugin); $method = new \ReflectionMethod($plugin, 'getEntity'); $this->assertInstanceOf(BlockContent::class, $method->invoke($plugin)); } } Loading
src/Plugin/SingleContentSyncFieldProcessor/LayoutSection.php +5 −1 Changes for src/Plugin/SingleContentSyncFieldProcessor/LayoutSection.php: 5 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -232,11 +232,15 @@ class LayoutSection extends SingleContentSyncFieldProcessorPluginBase implements foreach ($section_components as $component) { if ($component->getPlugin() instanceof InlineBlock) { $configuration = $component->toArray()['configuration']; // Imported inline blocks use structured 'blocks' data and revision // references. Never pass a serialized object graph from an import // to core's InlineBlock plugin. unset($configuration['block_serialized']); if (isset($configuration['block_revision_id']) && isset($imported_blocks[$configuration['block_revision_id']])) { // Replace the old revision id with a new revision id. $configuration['block_revision_id'] = $imported_blocks[$configuration['block_revision_id']]; $component->setConfiguration($configuration); } $component->setConfiguration($configuration); } } } Loading
tests/src/Kernel/Fixtures/SerializedBlockCanary.php 0 → 100644 +29 −0 Changes for tests/src/Kernel/Fixtures/SerializedBlockCanary.php: 29 added lines, 0 removed lines. Original line number Diff line number Diff line <?php declare(strict_types=1); namespace Drupal\Tests\single_content_sync\Kernel\Fixtures; /** * Test-only object used to detect an unrestricted unserialize(). * * When an instance of this class is unserialized, __wakeup() flips a static * flag. The tests serialize one of these, place it where a block object would * live, and assert whether it is (or is not) reconstructed downstream. The * class itself is inert; it stands in for a real gadget object. */ class SerializedBlockCanary { /** * Set to TRUE the moment an instance is unserialized. */ public static bool $reconstructed = FALSE; /** * Records that an instance was rebuilt from serialized bytes. */ public function __wakeup(): void { self::$reconstructed = TRUE; } }
tests/src/Kernel/LayoutSectionImportTest.php 0 → 100644 +345 −0 Changes for tests/src/Kernel/LayoutSectionImportTest.php: 345 added lines, 0 removed lines. Original line number Diff line number Diff line <?php declare(strict_types=1); namespace Drupal\Tests\single_content_sync\Kernel; use Drupal\block_content\Entity\BlockContent; use Drupal\block_content\Entity\BlockContentType; use Drupal\KernelTests\KernelTestBase; use Drupal\layout_builder\Plugin\Block\InlineBlock; use Drupal\layout_builder\Section; use Drupal\layout_builder\SectionComponent; use Drupal\node\Entity\Node; use Drupal\single_content_sync\Plugin\SingleContentSyncFieldProcessor\LayoutSection; use Drupal\Tests\node\Traits\ContentTypeCreationTrait; use Drupal\Tests\single_content_sync\Kernel\Fixtures\SerializedBlockCanary; /** * Covers how the layout_section import handles inline block configuration. * * The importer rebuilds each Layout Builder component from the (fully * attacker-controlled) import payload via Section::fromArray(). Core's * InlineBlock later reads the 'block_serialized' key back with a bare * unserialize(), so that key must not survive an import, while the legitimate * inline-block reference must round-trip intact. * * @group single_content_sync * @coversDefaultClass \Drupal\single_content_sync\Plugin\SingleContentSyncFieldProcessor\LayoutSection * @runTestsInSeparateProcesses * @preserveGlobalState disabled */ class LayoutSectionImportTest extends KernelTestBase { use ContentTypeCreationTrait; /** * {@inheritdoc} */ protected static $modules = [ 'single_content_sync', 'field', 'file', 'node', 'system', 'text', 'user', 'block_content', 'layout_discovery', 'layout_builder', ]; /** * A serialized stand-in object placed where a block object would be. */ protected string $serializedPayload; /** * {@inheritdoc} */ protected function setUp(): void { parent::setUp(); $this->installEntitySchema('node'); $this->installEntitySchema('user'); $this->installEntitySchema('block_content'); $this->installSchema('layout_builder', ['inline_block_usage']); $this->installSchema('node', ['node_access']); $this->installConfig(['node', 'system']); $this->createContentType(['type' => 'article']); // The 'basic' bundle makes core's 'inline_block:basic' plugin available, // which is the plugin that owns the unserialize() sink under test. BlockContentType::create(['id' => 'basic', 'label' => 'Basic'])->save(); // Enable Layout Builder overrides on the article view display; this is // what creates the layout_builder__layout field the import writes to. \Drupal::service('entity_display.repository') ->getViewDisplay('node', 'article', 'default') ->enableLayoutBuilder() ->setOverridable() ->save(); SerializedBlockCanary::$reconstructed = FALSE; $this->serializedPayload = serialize(new SerializedBlockCanary()); } /** * A crafted block_serialized is neutralized; the core sink becomes inert. * * Both current and legacy layouts must discard the serialized data before * reaching core's save or read paths. This also works with hardened core. * * @covers ::importFieldValue * @dataProvider sectionFormats */ public function testCraftedSerializedBlockIsNeutralized(string $format): void { $uuid = '10000000-0000-1000-a000-000000000000'; $component = new SectionComponent($uuid, 'content', [ 'id' => 'inline_block:basic', 'label' => 'x', 'label_display' => '0', 'provider' => 'layout_builder', 'view_mode' => 'full', 'block_serialized' => $this->serializedPayload, ]); $section = new Section('layout_onecol', [], [$component]); // No 'blocks' behind the section: the inline block was never really // exported, it is a raw serialized payload smuggled in the section data. $value = [ 'blocks' => [], 'sections' => $this->encodeSections([$section], $format), ]; $node = Node::create(['type' => 'article', 'title' => 'Target']); $this->processor()->importFieldValue($node, 'layout_builder__layout', $value); $configuration = $this->componentConfiguration($node, $uuid); $this->assertArrayNotHasKey( 'block_serialized', $configuration, 'The imported layout must not carry a serialized block object.' ); // Exercise the presave path before checking core's read path. $node->save(); $this->container->get('entity_type.manager')->getStorage('node')->resetCache(); $node = Node::load($node->id()); $configuration = $this->componentConfiguration($node, $uuid); $this->assertEmpty($configuration['block_serialized'] ?? NULL); $this->driveCoreSink($configuration); $this->assertFalse( SerializedBlockCanary::$reconstructed, 'After import the core sink reconstructs nothing.' ); } /** * A real inline block round-trips through export/import after the strip. * * A legitimately exported inline block travels as an entry in the 'blocks' * list and is referenced from the section by block_revision_id. Dropping * block_serialized on import must leave that path intact: the block content * survives and the component still points at it. * * @covers ::exportFieldValue * @covers ::importFieldValue * @dataProvider sectionFormats */ public function testInlineBlockRoundTripSurvivesStrip(string $format): void { $block = BlockContent::create([ 'type' => 'basic', 'info' => 'ROUNDTRIP-9931', ]); $block->save(); // The persisted state of an inline block: referenced by revision id, with // no serialized blob. $uuid = '20000000-0000-1000-a000-000000000000'; $component = new SectionComponent($uuid, 'content', [ 'id' => 'inline_block:basic', 'label' => 'x', 'label_display' => '0', 'provider' => 'layout_builder', 'view_mode' => 'full', 'block_revision_id' => $block->getRevisionId(), ]); $section = new Section('layout_onecol', [], [$component]); $source = Node::create(['type' => 'article', 'title' => 'Source']); $source->set('layout_builder__layout', [['section' => $section]]); $source->save(); // Export through the module, exactly as the real feature does. A // legitimate inline block travels as an entry in the 'blocks' list and is // referenced from the section by block_revision_id; its block_serialized // slot is empty. (Only a crafted import supplies a non-empty blob there.) $exported = $this->processor()->exportFieldValue($source->get('layout_builder__layout')); $this->assertCount(1, $exported['blocks'], 'The inline block is exported as an entity.'); $exported_config = $exported['sections'][0]['components'][$uuid]['configuration']; $this->assertNotEmpty( $exported_config['block_revision_id'], 'The exported inline block is referenced by revision id.' ); $this->assertEmpty( $exported_config['block_serialized'] ?? NULL, 'A legitimate export carries no serialized block blob.' ); // Require remapping of a revision ID from another site. A malicious blob // alongside a legitimate block must not override its structured content. $old_revision_id = 999999; $exported['blocks'][0]['base_fields']['block_revision_id'] = $old_revision_id; $exported['sections'][0]['components'][$uuid]['configuration']['block_revision_id'] = $old_revision_id; $exported['sections'][0]['components'][$uuid]['configuration']['block_serialized'] = $this->serializedPayload; $sections = array_map([Section::class, 'fromArray'], $exported['sections']); $exported['sections'] = $this->encodeSections($sections, $format); // Import into a fresh node. $target = Node::create(['type' => 'article', 'title' => 'Target']); $this->processor()->importFieldValue($target, 'layout_builder__layout', $exported); $configuration = $this->componentConfiguration($target, $uuid); $this->assertArrayNotHasKey('block_serialized', $configuration); $this->assertNotEquals($old_revision_id, $configuration['block_revision_id']); $target->save(); \Drupal::entityTypeManager()->getStorage('node')->resetCache(); $target = Node::load($target->id()); // After the node is saved, core's own inline-block handling re-adds an // empty block_serialized slot; the invariant is that it is never a blob. $configuration = $this->componentConfiguration($target, $uuid); $this->assertEmpty( $configuration['block_serialized'] ?? NULL, 'The round-tripped inline block carries no serialized blob.' ); $this->assertNotEmpty( $configuration['block_revision_id'], 'The imported component still references an inline block revision.' ); // The referenced revision resolves to the round-tripped block content. $imported_block = \Drupal::entityTypeManager() ->getStorage('block_content') ->loadRevision($configuration['block_revision_id']); $this->assertNotNull($imported_block, 'The referenced block revision exists.'); $this->assertEquals('ROUNDTRIP-9931', $imported_block->get('info')->value); $this->assertFalse(SerializedBlockCanary::$reconstructed); } /** * The complete import service sanitizes layouts on creation and update. * * @covers ::importFieldValue * @dataProvider sectionFormats */ public function testContentImportRemovesSerializedBlock(string $format): void { $uuid = $this->container->get('uuid')->generate(); $component_uuid = $this->container->get('uuid')->generate(); $section = new Section('layout_onecol', [], [ new SectionComponent($component_uuid, 'content', [ 'id' => 'inline_block:basic', 'block_serialized' => $this->serializedPayload, ]), ]); $content = [ 'entity_type' => 'node', 'bundle' => 'article', 'uuid' => $uuid, 'base_fields' => [ 'title' => 'Imported layout', 'langcode' => 'en', 'created' => 123456789, 'status' => TRUE, ], 'custom_fields' => [ 'layout_builder__layout' => [ 'blocks' => [], 'sections' => $this->encodeSections([$section], $format), ], ], ]; $node_id = NULL; foreach (['Create', 'Update'] as $operation) { $content['base_fields']['title'] = $operation; $node = $this->container->get('single_content_sync.importer')->doImport($content); if ($node_id !== NULL) { $this->assertEquals($node_id, $node->id()); } $node_id = $node->id(); $this->container->get('entity_type.manager')->getStorage('node')->resetCache(); $node = Node::load($node_id); $this->assertEquals($operation, $node->label()); $this->assertEmpty($this->componentConfiguration($node, $component_uuid)['block_serialized'] ?? NULL); $this->assertFalse(SerializedBlockCanary::$reconstructed); } } /** * Provides the supported layout export formats. */ public static function sectionFormats(): array { return [ 'structured arrays' => ['array'], 'legacy serialized sections' => ['legacy'], ]; } /** * Encodes sections using the current or legacy export format. */ protected function encodeSections(array $sections, string $format) { return $format === 'legacy' ? base64_encode(implode('|', array_map('serialize', $sections))) : array_map(static function (Section $section): array { return $section->toArray(); }, $sections); } /** * Returns the layout_section field processor under test. */ protected function processor(): LayoutSection { return $this->container ->get('plugin.manager.single_content_sync_field_processor') ->createInstance('layout_section'); } /** * Returns the stored configuration of the first component of a node layout. */ protected function componentConfiguration(Node $node, string $uuid): array { $sections = $node->get('layout_builder__layout')->getSections(); $this->assertCount(1, $sections); $component = $sections[0]->getComponent($uuid); $this->assertNotNull($component); return $component->get('configuration'); } /** * Feeds a component configuration to core's InlineBlock unserialize sink. * * The getEntity() method reads serialized block data; saveBlockContent() * reads it during presave. Drive the protected method directly to check * the read path without depending on a full render pipeline. */ protected function driveCoreSink(array $configuration): void { $configuration += [ 'id' => 'inline_block:basic', 'label' => 'x', 'label_display' => '0', 'provider' => 'layout_builder', 'view_mode' => 'full', ]; /** @var \Drupal\layout_builder\Plugin\Block\InlineBlock $plugin */ $plugin = $this->container->get('plugin.manager.block') ->createInstance('inline_block:basic', $configuration); $this->assertInstanceOf(InlineBlock::class, $plugin); $method = new \ReflectionMethod($plugin, 'getEntity'); $this->assertInstanceOf(BlockContent::class, $method->invoke($plugin)); } }