diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php index b3410649545b19726604108c4e834e5c0aefa571..adf4c86344db1922abe8bf5c62268e4f6c42a594 100644 --- a/core/lib/Drupal/Core/Form/FormBuilder.php +++ b/core/lib/Drupal/Core/Form/FormBuilder.php @@ -637,6 +637,20 @@ public function processForm($form_id, &$form, FormStateInterface &$form_state) { } } + /** + * #lazy_builder callback; renders a form action URL. + * + * @return array + * A renderable array representing the form action. + */ + public function renderPlaceholderFormAction() { + return [ + '#type' => 'markup', + '#markup' => $this->buildFormAction(), + '#cache' => ['contexts' => ['url.path', 'url.query_args']], + ]; + } + /** * {@inheritdoc} */ @@ -647,7 +661,17 @@ public function prepareForm($form_id, &$form, FormStateInterface &$form_state) { // Only update the action if it is not already set. if (!isset($form['#action'])) { - $form['#action'] = $this->buildFormAction(); + // Instead of setting an actual action URL, we set the placeholder, which + // will be replaced at the very last moment. This ensures forms with + // dynamically generated action URLs don't have poor cacheability. + // Use the proper API to generate the placeholder, when we have one. See + // https://www.drupal.org/node/2562341. + $placeholder = 'form_action_' . hash('crc32b', __METHOD__); + + $form['#attached']['placeholders'][$placeholder] = [ + '#lazy_builder' => ['form_builder:renderPlaceholderFormAction', []], + ]; + $form['#action'] = $placeholder; } // Fix the form method, if it is 'get' in $form_state, but not in $form. diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php index bb8f1ff770dc7c1909312636e255a481a5580892..48b3bcde7a28130a55c23f6b3d7a07abf4707fa1 100644 --- a/core/lib/Drupal/Core/Render/Renderer.php +++ b/core/lib/Drupal/Core/Render/Renderer.php @@ -179,7 +179,7 @@ protected function renderPlaceholder($placeholder, array $elements) { // Replace the placeholder with its rendered markup, and merge its // bubbleable metadata with the main elements'. - $elements['#markup'] = str_replace($placeholder, $markup, $elements['#markup']); + $elements['#markup'] = SafeString::create(str_replace($placeholder, $markup, $elements['#markup'])); $elements = $this->mergeBubbleableMetadata($elements, $placeholder_elements); // Remove the placeholder that we've just rendered. diff --git a/core/modules/block/src/Tests/BlockFormInBlockTest.php b/core/modules/block/src/Tests/BlockFormInBlockTest.php new file mode 100644 index 0000000000000000000000000000000000000000..32eddfaece63b1d47e554c548458299c6af2c709 --- /dev/null +++ b/core/modules/block/src/Tests/BlockFormInBlockTest.php @@ -0,0 +1,76 @@ +drupalPlaceBlock('test_form_in_block'); + } + + /** + * Test to see if form in block's redirect isn't cached. + */ + function testCachePerPage() { + $form_values = ['email' => 'test@example.com']; + + // Go to "test-page" and test if the block is enabled. + $this->drupalGet('test-page'); + $this->assertResponse(200); + $this->assertText('Your .com email address.', 'form found'); + + // Make sure that we're currently still on /test-page after submitting the + // form. + $this->drupalPostForm(NULL, $form_values, t('Submit')); + $this->assertUrl('test-page'); + $this->assertText(t('Your email address is @email', ['@email' => 'test@example.com'])); + + // Go to a different page and see if the block is enabled there as well. + $this->drupalGet('test-render-title'); + $this->assertResponse(200); + $this->assertText('Your .com email address.', 'form found'); + + // Make sure that submitting the form didn't redirect us to the first page + // we submitted the form from after submitting the form from + // /test-render-title. + $this->drupalPostForm(NULL, $form_values, t('Submit')); + $this->assertUrl('test-render-title'); + $this->assertText(t('Your email address is @email', ['@email' => 'test@example.com'])); + } + + /** + * Test the actual placeholders + */ + public function testPlaceholders() { + $this->drupalGet('test-multiple-forms'); + + $placeholder = 'form_action_' . hash('crc32b', 'Drupal\Core\Form\FormBuilder::prepareForm'); + $this->assertText('Form action: ' . $placeholder, 'placeholder found.'); + } + +} diff --git a/core/modules/block/src/Tests/Views/DisplayBlockTest.php b/core/modules/block/src/Tests/Views/DisplayBlockTest.php index 0beb5390b993ccc3e72bd1e650038661cc84c918..9b85831c73ed29d5adfe21010d13a15c6a71225f 100644 --- a/core/modules/block/src/Tests/Views/DisplayBlockTest.php +++ b/core/modules/block/src/Tests/Views/DisplayBlockTest.php @@ -290,7 +290,7 @@ public function testBlockEmptyRendering() { // Ensure that the view cachability metadata is propagated even, for an // empty block. $this->assertCacheTags(array_merge($block->getCacheTags(), ['block_view', 'config:block_list', 'config:system.site', 'config:views.view.test_view_block' ,'rendered'])); - $this->assertCacheContexts(['url.query_args:_wrapper_format', 'user.roles:authenticated']); + $this->assertCacheContexts(['url.path', 'url.query_args', 'user.roles:authenticated']); // Add a header displayed on empty result. $display = &$view->getDisplay('block_1'); @@ -308,7 +308,7 @@ public function testBlockEmptyRendering() { $this->drupalGet(''); $this->assertEqual(1, count($this->xpath('//div[contains(@class, "block-views-blocktest-view-block-block-1")]'))); $this->assertCacheTags(array_merge($block->getCacheTags(), ['block_view', 'config:block_list', 'config:system.site', 'config:views.view.test_view_block' ,'rendered'])); - $this->assertCacheContexts(['url.query_args:_wrapper_format', 'user.roles:authenticated']); + $this->assertCacheContexts(['url.path', 'url.query_args', 'user.roles:authenticated']); // Hide the header on empty results. $display = &$view->getDisplay('block_1'); @@ -326,7 +326,7 @@ public function testBlockEmptyRendering() { $this->drupalGet(''); $this->assertEqual(0, count($this->xpath('//div[contains(@class, "block-views-blocktest-view-block-block-1")]'))); $this->assertCacheTags(array_merge($block->getCacheTags(), ['block_view', 'config:block_list', 'config:system.site', 'config:views.view.test_view_block' ,'rendered'])); - $this->assertCacheContexts(['url.query_args:_wrapper_format', 'user.roles:authenticated']); + $this->assertCacheContexts(['url.path', 'url.query_args', 'user.roles:authenticated']); // Add an empty text. $display = &$view->getDisplay('block_1'); @@ -343,7 +343,7 @@ public function testBlockEmptyRendering() { $this->drupalGet(''); $this->assertEqual(1, count($this->xpath('//div[contains(@class, "block-views-blocktest-view-block-block-1")]'))); $this->assertCacheTags(array_merge($block->getCacheTags(), ['block_view', 'config:block_list', 'config:system.site', 'config:views.view.test_view_block' ,'rendered'])); - $this->assertCacheContexts(['url.query_args:_wrapper_format', 'user.roles:authenticated']); + $this->assertCacheContexts(['url.path', 'url.query_args', 'user.roles:authenticated']); } /** diff --git a/core/modules/block/tests/modules/block_test/block_test.routing.yml b/core/modules/block/tests/modules/block_test/block_test.routing.yml new file mode 100644 index 0000000000000000000000000000000000000000..58af6fc12127d162ae58b5252fe73c762cb78f22 --- /dev/null +++ b/core/modules/block/tests/modules/block_test/block_test.routing.yml @@ -0,0 +1,7 @@ +block_test.test_multipleforms: + path: '/test-multiple-forms' + defaults: + _controller: '\Drupal\block_test\Controller\TestMultipleFormController::testMultipleForms' + _title: 'Multiple forms' + requirements: + _access: 'TRUE' diff --git a/core/modules/block/tests/modules/block_test/src/Controller/TestMultipleFormController.php b/core/modules/block/tests/modules/block_test/src/Controller/TestMultipleFormController.php new file mode 100644 index 0000000000000000000000000000000000000000..d7862903f31bde0ecfc88786d2a10e59f98ffe4b --- /dev/null +++ b/core/modules/block/tests/modules/block_test/src/Controller/TestMultipleFormController.php @@ -0,0 +1,43 @@ + $this->formBuilder()->buildForm('\Drupal\block_test\Form\TestForm', $form_state), + 'form2' => $this->formBuilder()->buildForm('\Drupal\block_test\Form\FavoriteAnimalTestForm', $form_state), + ]; + + // Output all attached placeholders trough drupal_set_message(), so we can + // see if there's only one in the tests. + $post_render_callable = function ($elements) { + $matches = []; + preg_match_all('