diff --git a/core/modules/layout_builder/layout_builder.services.yml b/core/modules/layout_builder/layout_builder.services.yml index 6e94ed74d2..11e6adfa68 100644 --- a/core/modules/layout_builder/layout_builder.services.yml +++ b/core/modules/layout_builder/layout_builder.services.yml @@ -42,7 +42,7 @@ services: arguments: ['@tempstore.shared', '@entity_type.manager'] layout_builder.render_block_component_subscriber: class: Drupal\layout_builder\EventSubscriber\BlockComponentRenderArray - arguments: ['@current_user'] + arguments: ['@current_user', '@renderer'] tags: - { name: event_subscriber } logger.channel.layout_builder: diff --git a/core/modules/layout_builder/src/EventSubscriber/BlockComponentRenderArray.php b/core/modules/layout_builder/src/EventSubscriber/BlockComponentRenderArray.php index e1a9773959..1505721200 100644 --- a/core/modules/layout_builder/src/EventSubscriber/BlockComponentRenderArray.php +++ b/core/modules/layout_builder/src/EventSubscriber/BlockComponentRenderArray.php @@ -3,10 +3,12 @@ namespace Drupal\layout_builder\EventSubscriber; use Drupal\block_content\Access\RefinableDependentAccessInterface; +use Drupal\Component\Utility\Html; use Drupal\Core\Access\AccessResult; use Drupal\Core\Block\BlockPluginInterface; use Drupal\Core\Render\Element; use Drupal\Core\Render\PreviewFallbackInterface; +use Drupal\Core\Render\RendererInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\layout_builder\Access\LayoutPreviewAccessAllowed; @@ -32,14 +34,24 @@ class BlockComponentRenderArray implements EventSubscriberInterface { */ protected $currentUser; + /** + * The core renderer service. + * + * @var \Drupal\Core\Render\RendererInterface + */ + protected $renderer; + /** * Creates a BlockComponentRenderArray object. * * @param \Drupal\Core\Session\AccountInterface $current_user * The current user. + * @param \Drupal\Core\Render\RendererInterface $renderer + * The core renderer service. */ - public function __construct(AccountInterface $current_user) { + public function __construct(AccountInterface $current_user, RendererInterface $renderer) { $this->currentUser = $current_user; + $this->renderer = $renderer; } /** @@ -108,6 +120,9 @@ public function onBuildRender(SectionComponentBuildRenderArrayEvent $event) { if ($is_content_empty && !$is_placeholder_ready) { return; } + if ($event->inPreview()) { + $this->convertFormsToDiv($content); + } $build = [ // @todo Move this to BlockBase in https://www.drupal.org/node/2931040. @@ -138,4 +153,46 @@ public function onBuildRender(SectionComponentBuildRenderArrayEvent $event) { } } + /** + * Prevent nested forms from generating form tokens. + * + * @param array $content + * The block render array. + * + * @return array + * A render array where nested forms will not generate tokens due to + * #theme_wrappers being removed. + */ + protected function detokenizeNestedForms(array $content) { + if (is_array($content)) { + if (isset($content['#type']) && $content['#type'] === 'form') { + unset($content['#theme_wrappers']); + } + foreach ($content as $key => $child_content) { + if (is_array($child_content)) { + $content[$key] = $this->detokenizeNestedForms($child_content); + } + + } + } + return $content; + } + + /** + * Convert form tags to div when displayed in the Layout Builder UI form. + * + * @param array $content + * The render array of the block. + */ + protected function convertFormsToDiv(array &$content) { + $markup = $this->renderer->render($content); + $html = Html::load((string) $markup); + + // This step is only necessary if forms are present in the markup. + if ($html->getElementsByTagName('form')->length > 0) { + $new_markup = preg_replace('//s', '', $markup); + $content['#markup'] = $new_markup; + } + } + } diff --git a/core/modules/layout_builder/tests/modules/layout_builder_views_test/config/install/views.view.test_exposed_filter.yml b/core/modules/layout_builder/tests/modules/layout_builder_views_test/config/install/views.view.test_exposed_filter.yml new file mode 100644 index 0000000000..81d92a429f --- /dev/null +++ b/core/modules/layout_builder/tests/modules/layout_builder_views_test/config/install/views.view.test_exposed_filter.yml @@ -0,0 +1,209 @@ +langcode: en +status: true +dependencies: + module: + - node + - user +id: test_exposed_filter +label: 'Test Exposed Filter' +module: views +description: '' +tag: '' +base_table: node_field_data +base_field: nid +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + access: + type: perm + options: + perm: 'access content' + cache: + type: tag + options: { } + query: + type: views_query + options: + disable_sql_rewrite: false + distinct: false + replica: false + query_comment: '' + query_tags: { } + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + pager: + type: some + options: + items_per_page: 5 + offset: 0 + style: + type: default + row: + type: fields + fields: + title: + id: title + table: node_field_data + field: title + settings: + link_to_entity: true + plugin_id: field + relationship: none + group_type: group + admin_label: '' + label: '' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: value + type: string + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + filters: + status_1: + id: status_1 + table: node_field_data + field: status + relationship: none + group_type: group + admin_label: '' + operator: '=' + value: All + group: 1 + exposed: true + expose: + operator_id: '' + label: 'Published status' + description: '' + use_operator: false + operator: status_1_op + identifier: status_1 + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator: '0' + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + entity_type: node + entity_field: status + plugin_id: boolean + sorts: + created: + id: created + table: node_field_data + field: created + order: DESC + entity_type: node + entity_field: created + plugin_id: date + relationship: none + group_type: group + admin_label: '' + exposed: false + expose: + label: '' + granularity: second + title: 'Test Exposed Filter' + header: { } + footer: { } + empty: { } + relationships: { } + arguments: { } + display_extenders: { } + use_ajax: true + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - 'user.node_grants:view' + - user.permissions + tags: { } + block_1: + display_plugin: block + id: block_1 + display_title: Block + position: 1 + display_options: + display_extenders: { } + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - 'user.node_grants:view' + - user.permissions + tags: { } diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderDisableInteractionsTest.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderDisableInteractionsTest.php index 8dcd507c8d..8b1ee432a9 100644 --- a/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderDisableInteractionsTest.php +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderDisableInteractionsTest.php @@ -30,6 +30,7 @@ class LayoutBuilderDisableInteractionsTest extends WebDriverTestBase { 'node', 'search', 'contextual', + 'layout_builder_views_test', ]; /** @@ -76,6 +77,16 @@ protected function setUp() { 'format' => 'full_html', ], ])->save(); + + BlockContent::create([ + 'type' => 'basic', + 'info' => 'Block with hardcoded form', + 'body' => [ + // Add hard-coded form. + 'value' => '
First name:

Last name:

', + 'format' => 'full_html', + ], + ])->save(); } /** @@ -104,7 +115,7 @@ public function testFormsLinksDisabled() { $this->clickLink('Manage layout'); // Add a block with a form, another with a link, and one with an iframe. - $this->addBlock('Search form', '#layout-builder .search-block-form'); + $this->addBlock('Search form', '#layout-builder .block-search'); $this->addBlock('Block with link', '#link-that-should-be-disabled'); $this->addBlock('Block with iframe', '#iframe-that-should-be-disabled'); @@ -302,4 +313,67 @@ protected function movePointerTo($selector) { $driver_session->moveto(['element' => $element->getID()]); } + /** + * Tests that blocks with forms do not prevent saving. + * + * @dataProvider providerTestBlocksWithFormsAllowSave + */ + public function testBlocksWithFormsAllowSave($block_link_text, $rendered_locator) { + $assert_session = $this->assertSession(); + $page = $this->getSession()->getPage(); + + $this->drupalLogin($this->drupalCreateUser([ + 'configure any layout', + 'administer node display', + 'administer node fields', + 'search content', + 'access contextual links', + ])); + + $field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field'; + + $this->drupalPostForm("$field_ui_prefix/display", ['layout[enabled]' => TRUE], 'Save'); + $this->drupalPostForm("$field_ui_prefix/display/default", ['layout[allow_custom]' => TRUE], 'Save'); + + $this->drupalGet('node/1/layout'); + $this->addBlock($block_link_text, $rendered_locator); + $assert_session->pageTextContains('You have unsaved changes.'); + $page->pressButton('Save layout'); + $this->drupalGet('node/1/layout'); + $assert_session->pageTextNotContains('You have unsaved changes.'); + $assert_session->waitForElement('css', $rendered_locator); + + $this->drupalGet("$field_ui_prefix/display/default/layout"); + $this->addBlock($block_link_text, $rendered_locator); + $assert_session->pageTextContains('You have unsaved changes.'); + $page->pressButton('Save layout'); + $this->drupalGet('node/1/layout'); + $assert_session->pageTextNotContains('You have unsaved changes.'); + $assert_session->waitForElement('css', $rendered_locator); + } + + /** + * Provides data for ::testBlocksWithFormsAllowSave(). + */ + public function providerTestBlocksWithFormsAllowSave() { + $data[] = [ + 'Block with hardcoded form', + '#block-with-hardcoded-form', + ]; + $data[] = [ + 'User login', + '#layout-builder .block-user', + ]; + $data[] = [ + 'Search form', + '#layout-builder .block-search', + ]; + $data[] = [ + 'Test Exposed Filter', + '#layout-builder .block-views-blocktest-exposed-filter-block-1', + ]; + + return $data; + } + } diff --git a/core/modules/layout_builder/tests/src/Unit/BlockComponentRenderArrayTest.php b/core/modules/layout_builder/tests/src/Unit/BlockComponentRenderArrayTest.php index 7b30433b02..2cb7f4d439 100644 --- a/core/modules/layout_builder/tests/src/Unit/BlockComponentRenderArrayTest.php +++ b/core/modules/layout_builder/tests/src/Unit/BlockComponentRenderArrayTest.php @@ -13,6 +13,7 @@ use Drupal\Core\Plugin\Context\ContextHandlerInterface; use Drupal\Core\Render\PreviewFallbackInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\Core\Render\RendererInterface; use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\Core\StringTranslation\TranslationInterface; use Drupal\layout_builder\Access\LayoutPreviewAccessAllowed; @@ -60,6 +61,7 @@ protected function setUp() { $this->blockManager = $this->prophesize(BlockManagerInterface::class); $this->account = $this->prophesize(AccountInterface::class); + $this->renderer = $this->prophesize(RendererInterface::class); $container = new ContainerBuilder(); $container->set('plugin.manager.block', $this->blockManager->reveal()); @@ -107,7 +109,7 @@ public function testOnBuildRender($refinable_dependent_access) { $in_preview = FALSE; $event = new SectionComponentBuildRenderArrayEvent($component, $contexts, $in_preview); - $subscriber = new BlockComponentRenderArray($this->account->reveal()); + $subscriber = new BlockComponentRenderArray($this->account->reveal(), $this->renderer->reveal()); $expected_build = [ '#theme' => 'block', @@ -177,7 +179,7 @@ public function testOnBuildRenderWithoutPreviewFallbackString($refinable_depende $in_preview = FALSE; $event = new SectionComponentBuildRenderArrayEvent($component, $contexts, $in_preview); - $subscriber = new BlockComponentRenderArray($this->account->reveal()); + $subscriber = new BlockComponentRenderArray($this->account->reveal(), $this->renderer->reveal()); $translation = $this->prophesize(TranslationInterface::class); $translation->translateString(Argument::type(TranslatableMarkup::class)) @@ -252,7 +254,7 @@ public function testOnBuildRenderDenied($refinable_dependent_access) { $in_preview = FALSE; $event = new SectionComponentBuildRenderArrayEvent($component, $contexts, $in_preview); - $subscriber = new BlockComponentRenderArray($this->account->reveal()); + $subscriber = new BlockComponentRenderArray($this->account->reveal(), $this->renderer->reveal()); $expected_build = []; $expected_cache = [ @@ -311,7 +313,7 @@ public function testOnBuildRenderInPreview($refinable_dependent_access) { $in_preview = TRUE; $event = new SectionComponentBuildRenderArrayEvent($component, $contexts, $in_preview); - $subscriber = new BlockComponentRenderArray($this->account->reveal()); + $subscriber = new BlockComponentRenderArray($this->account->reveal(), $this->renderer->reveal()); $expected_build = [ '#theme' => 'block', @@ -366,7 +368,7 @@ public function testOnBuildRenderInPreviewEmptyBuild() { $component = new SectionComponent('some-uuid', 'some-region', ['id' => 'some_block_id']); $event = new SectionComponentBuildRenderArrayEvent($component, [], TRUE); - $subscriber = new BlockComponentRenderArray($this->account->reveal()); + $subscriber = new BlockComponentRenderArray($this->account->reveal(), $this->renderer->reveal()); $translation = $this->prophesize(TranslationInterface::class); $translation->translateString(Argument::type(TranslatableMarkup::class)) ->willReturn($placeholder_string); @@ -422,7 +424,7 @@ public function testOnBuildRenderEmptyBuild() { $component = new SectionComponent('some-uuid', 'some-region', ['id' => 'some_block_id']); $event = new SectionComponentBuildRenderArrayEvent($component, [], FALSE); - $subscriber = new BlockComponentRenderArray($this->account->reveal()); + $subscriber = new BlockComponentRenderArray($this->account->reveal(), $this->renderer->reveal()); $expected_build = []; @@ -452,7 +454,7 @@ public function testOnBuildRenderNoBlock() { $in_preview = FALSE; $event = new SectionComponentBuildRenderArrayEvent($component, $contexts, $in_preview); - $subscriber = new BlockComponentRenderArray($this->account->reveal()); + $subscriber = new BlockComponentRenderArray($this->account->reveal(), $this->renderer->reveal()); $expected_build = []; $expected_cache = [ diff --git a/core/modules/layout_builder/tests/src/Unit/SectionRenderTest.php b/core/modules/layout_builder/tests/src/Unit/SectionRenderTest.php index 0989da8487..c562da1ae9 100644 --- a/core/modules/layout_builder/tests/src/Unit/SectionRenderTest.php +++ b/core/modules/layout_builder/tests/src/Unit/SectionRenderTest.php @@ -16,6 +16,7 @@ use Drupal\Core\Plugin\Context\ContextRepositoryInterface; use Drupal\Core\Plugin\ContextAwarePluginInterface; use Drupal\Core\Render\PreviewFallbackInterface; +use Drupal\Core\Render\RendererInterface; use Drupal\Core\Session\AccountInterface; use Drupal\layout_builder\EventSubscriber\BlockComponentRenderArray; use Drupal\layout_builder\Section; @@ -36,6 +37,13 @@ class SectionRenderTest extends UnitTestCase { */ protected $account; + /** + * The renderer service. + * + * @var \Drupal\Core\Render\RendererInterface + */ + protected $renderer; + /** * The block plugin manager. * @@ -78,7 +86,8 @@ protected function setUp() { $this->eventDispatcher = (new \ReflectionClass(ContainerAwareEventDispatcher::class))->newInstanceWithoutConstructor(); $this->account = $this->prophesize(AccountInterface::class); - $subscriber = new BlockComponentRenderArray($this->account->reveal()); + $this->renderer = $this->prophesize(RendererInterface::class); + $subscriber = new BlockComponentRenderArray($this->account->reveal(), $this->renderer->reveal()); $this->eventDispatcher->addSubscriber($subscriber); $layout = $this->prophesize(LayoutInterface::class);