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('', $elements, $matches); + + $action_values = $matches[2]; + + foreach ($action_values as $action_value) { + drupal_set_message('Form action: ' . $action_value); + } + return $elements; + }; + + $build['#post_render'] = [$post_render_callable]; + + return $build; + } + +} diff --git a/core/modules/block/tests/modules/block_test/src/Form/FavoriteAnimalTestForm.php b/core/modules/block/tests/modules/block_test/src/Form/FavoriteAnimalTestForm.php new file mode 100644 index 0000000000000000000000000000000000000000..2e4067508f123226542c8018c7708a4b5c522170 --- /dev/null +++ b/core/modules/block/tests/modules/block_test/src/Form/FavoriteAnimalTestForm.php @@ -0,0 +1,46 @@ + 'textfield', + '#title' => $this->t('Your favorite animal.') + ]; + + $form['submit_animal'] = [ + '#type' => 'submit', + '#value' => $this->t('Submit your chosen animal'), + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + drupal_set_message($this->t('Your favorite animal is: @favorite_animal', ['@favorite_animal' => $form['favorite_animal']['#value']])); + } + +} diff --git a/core/modules/block/tests/modules/block_test/src/Form/TestForm.php b/core/modules/block/tests/modules/block_test/src/Form/TestForm.php new file mode 100644 index 0000000000000000000000000000000000000000..c0a06944e56d06d43f27f87cd32143251d7291e9 --- /dev/null +++ b/core/modules/block/tests/modules/block_test/src/Form/TestForm.php @@ -0,0 +1,55 @@ + 'email', + '#title' => $this->t('Your .com email address.') + ]; + + $form['show'] = [ + '#type' => 'submit', + '#value' => $this->t('Submit'), + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + if (strpos($form_state->getValue('email'), '.com') === FALSE) { + $form_state->setErrorByName('email', $this->t('This is not a .com email address.')); + } + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + drupal_set_message($this->t('Your email address is @email', ['@email' => $form['email']['#value']])); + } + +} diff --git a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestFormBlock.php b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestFormBlock.php new file mode 100644 index 0000000000000000000000000000000000000000..feeb9df54566bf0daf11ac184b9d4e4b9804b72e --- /dev/null +++ b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestFormBlock.php @@ -0,0 +1,29 @@ +getForm('Drupal\block_test\Form\TestForm'); + } + +} diff --git a/core/modules/block_content/src/Tests/BlockContentTranslationUITest.php b/core/modules/block_content/src/Tests/BlockContentTranslationUITest.php index 4ce15655badfe0d00d84ec075f9a6cdfed54a5a4..964f0f9de3a54dfb4bbc4821c2b70e3608e513a8 100644 --- a/core/modules/block_content/src/Tests/BlockContentTranslationUITest.php +++ b/core/modules/block_content/src/Tests/BlockContentTranslationUITest.php @@ -36,7 +36,8 @@ class BlockContentTranslationUITest extends ContentTranslationUITestBase { protected $defaultCacheContexts = [ 'languages:language_interface', 'theme', - 'url.query_args:_wrapper_format', + 'url.path', + 'url.query_args', 'user.permissions', 'user.roles:authenticated', ]; diff --git a/core/modules/content_translation/src/Tests/ContentTestTranslationUITest.php b/core/modules/content_translation/src/Tests/ContentTestTranslationUITest.php index 43fd7798ed39ef136d32e0d78aeb1b4433d55cc0..08b1d4ab2bb89bf4b518b70d32684b48dbe45cbd 100644 --- a/core/modules/content_translation/src/Tests/ContentTestTranslationUITest.php +++ b/core/modules/content_translation/src/Tests/ContentTestTranslationUITest.php @@ -32,7 +32,8 @@ class ContentTestTranslationUITest extends ContentTranslationUITestBase { protected $defaultCacheContexts = [ 'languages:language_interface', 'theme', - 'url.query_args:_wrapper_format', + 'url.path', + 'url.query_args', 'user.permissions', 'user.roles:authenticated', ]; diff --git a/core/modules/menu_link_content/src/Tests/MenuLinkContentTranslationUITest.php b/core/modules/menu_link_content/src/Tests/MenuLinkContentTranslationUITest.php index f72cb29e5133a1b856f327b825ebfbd238402e50..14043842613e0aa31e4e6f99c2101abcd0c86f4b 100644 --- a/core/modules/menu_link_content/src/Tests/MenuLinkContentTranslationUITest.php +++ b/core/modules/menu_link_content/src/Tests/MenuLinkContentTranslationUITest.php @@ -20,7 +20,7 @@ class MenuLinkContentTranslationUITest extends ContentTranslationUITestBase { /** * {inheritdoc} */ - protected $defaultCacheContexts = ['languages:language_interface', 'theme', 'url.query_args:_wrapper_format', 'user.permissions', 'user.roles:authenticated']; + protected $defaultCacheContexts = ['languages:language_interface', 'theme', 'url.path', 'url.query_args', 'user.permissions', 'user.roles:authenticated']; /** * Modules to enable. diff --git a/core/modules/node/src/Tests/NodeBlockFunctionalTest.php b/core/modules/node/src/Tests/NodeBlockFunctionalTest.php index 6f319f5a9da97622cea66bd3ee3a229878512094..b6855bae7292616410a9b4ecee339e46fcd5a5e9 100644 --- a/core/modules/node/src/Tests/NodeBlockFunctionalTest.php +++ b/core/modules/node/src/Tests/NodeBlockFunctionalTest.php @@ -148,7 +148,7 @@ public function testRecentNodeBlock() { $this->assertCacheContexts(['languages:language_content', 'languages:language_interface', 'theme', 'url.query_args:' . MainContentViewSubscriber::WRAPPER_FORMAT, 'user', 'route']); $this->drupalGet('node/add/article'); $this->assertText($label, 'Block was displayed on the node/add/article page.'); - $this->assertCacheContexts(['languages:language_content', 'languages:language_interface', 'theme', 'url.query_args:' . MainContentViewSubscriber::WRAPPER_FORMAT, 'user', 'route']); + $this->assertCacheContexts(['languages:language_content', 'languages:language_interface', 'theme', 'url.path', 'url.query_args', 'user', 'route']); $this->drupalGet('node/' . $node1->id()); $this->assertText($label, 'Block was displayed on the node/N when node is of type article.'); $this->assertCacheContexts(['languages:language_content', 'languages:language_interface', 'theme', 'url.query_args:' . MainContentViewSubscriber::WRAPPER_FORMAT, 'user', 'route', 'timezone']); diff --git a/core/modules/shortcut/src/Tests/ShortcutTranslationUITest.php b/core/modules/shortcut/src/Tests/ShortcutTranslationUITest.php index 5fa63d8b9bf517b87451fa2eda755c0cd49ab7c6..e42262fb5a067fbe0e826de01165788b56c0b3ee 100644 --- a/core/modules/shortcut/src/Tests/ShortcutTranslationUITest.php +++ b/core/modules/shortcut/src/Tests/ShortcutTranslationUITest.php @@ -21,7 +21,7 @@ class ShortcutTranslationUITest extends ContentTranslationUITestBase { /** * {inheritdoc} */ - protected $defaultCacheContexts = ['languages:language_interface', 'theme', 'user', 'url.query_args:_wrapper_format', 'url.site']; + protected $defaultCacheContexts = ['languages:language_interface', 'theme', 'user', 'url.path', 'url.query_args', 'url.site']; /** * Modules to enable. diff --git a/core/modules/system/src/Tests/Common/RenderWebTest.php b/core/modules/system/src/Tests/Common/RenderWebTest.php index 5cfe3abfc9e3ea4183ceb0d853b3b17ace2a15a5..f6252e9999973e9ac081f1a5e971fade49c3767d 100644 --- a/core/modules/system/src/Tests/Common/RenderWebTest.php +++ b/core/modules/system/src/Tests/Common/RenderWebTest.php @@ -30,17 +30,17 @@ class RenderWebTest extends WebTestBase { * Asserts the cache context for the wrapper format is always present. */ function testWrapperFormatCacheContext() { - $this->drupalGet(''); + $this->drupalGet('common-test/type-link-active-class'); $this->assertIdentical(0, strpos($this->getRawContent(), "\nassertIdentical('text/html; charset=UTF-8', $this->drupalGetHeader('Content-Type')); - $this->assertTitle('Log in | Drupal'); + $this->assertTitle('Test active link class | Drupal'); $this->assertCacheContext('url.query_args:' . MainContentViewSubscriber::WRAPPER_FORMAT); - $this->drupalGet('', ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'json']]); + $this->drupalGet('common-test/type-link-active-class', ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'json']]); $this->assertIdentical('application/json', $this->drupalGetHeader('Content-Type')); $json = Json::decode($this->getRawContent()); $this->assertEqual(['content', 'title'], array_keys($json)); - $this->assertIdentical('Log in', $json['title']); + $this->assertIdentical('Test active link class', $json['title']); $this->assertCacheContext('url.query_args:' . MainContentViewSubscriber::WRAPPER_FORMAT); } diff --git a/core/modules/system/tests/modules/common_test/common_test.routing.yml b/core/modules/system/tests/modules/common_test/common_test.routing.yml index 92c80bedfcb14e2b428162c3eb70dd8b003f6441..8062d063e762cef36916e3a932061259d6c15b08 100644 --- a/core/modules/system/tests/modules/common_test/common_test.routing.yml +++ b/core/modules/system/tests/modules/common_test/common_test.routing.yml @@ -1,6 +1,7 @@ common_test.l_active_class: path: '/common-test/type-link-active-class' defaults: + _title: 'Test active link class' _controller: '\Drupal\common_test\Controller\CommonTestController::typeLinkActiveClass' requirements: _access: 'TRUE'