.../Core/Entity/Entity/EntityFormDisplay.php | 2 +- core/lib/Drupal/Core/Form/FormBuilder.php | 3 -- core/lib/Drupal/Core/Render/Renderer.php | 25 ++++++------ .../ckeditor/src/Tests/CKEditorLoadingTest.php | 2 + .../Tests/CommentDefaultFormatterCacheTagsTest.php | 2 + core/modules/contact/src/MessageForm.php | 3 ++ .../src/Tests/ContactAuthenticatedUserTest.php | 14 ++++++- core/modules/editor/src/Element.php | 1 + .../modules/editor/src/Tests/EditorLoadingTest.php | 2 + .../editor/src/Tests/EditorSecurityTest.php | 2 + core/modules/filter/src/Element/TextFormat.php | 2 + core/modules/language/language.module | 5 ++- .../language/src/Element/LanguageConfiguration.php | 1 + .../src/Entity/ContentLanguageSettings.php | 14 +++++++ .../src/Tests/LanguageConfigurationElementTest.php | 1 + .../node/src/Controller/NodePreviewController.php | 1 + core/modules/node/src/NodeForm.php | 3 ++ core/modules/node/src/Plugin/Search/NodeSearch.php | 5 +++ .../node/src/Tests/NodeTypeInitialLanguageTest.php | 2 + .../search/src/Controller/SearchController.php | 3 ++ .../src/Tests/SearchAdvancedSearchFormTest.php | 1 + .../src/Tests/Entity/EntityTranslationFormTest.php | 2 + .../Drupal/Tests/Core/Form/FormBuilderTest.php | 45 ++++++++++++++-------- .../Drupal/Tests/Core/Render/RendererTest.php | 20 ++++++++++ 24 files changed, 126 insertions(+), 35 deletions(-) diff --git a/core/lib/Drupal/Core/Entity/Entity/EntityFormDisplay.php b/core/lib/Drupal/Core/Entity/Entity/EntityFormDisplay.php index bee0144..cd55751 100644 --- a/core/lib/Drupal/Core/Entity/Entity/EntityFormDisplay.php +++ b/core/lib/Drupal/Core/Entity/Entity/EntityFormDisplay.php @@ -171,7 +171,7 @@ public function buildForm(FieldableEntityInterface $entity, array &$form, FormSt $items = $entity->get($name); $items->filterEmptyItems(); $form[$name] = $widget->form($items, $form, $form_state); - $form[$name]['#access'] = $items->access('edit'); + $form[$name]['#access'] = $items->access('edit', NULL, TRUE); // Assign the correct weight. This duplicates the reordering done in // processForm(), but is needed for other forms calling this method diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php index 721ee92..3dac78b 100644 --- a/core/lib/Drupal/Core/Form/FormBuilder.php +++ b/core/lib/Drupal/Core/Form/FormBuilder.php @@ -734,9 +734,6 @@ public function prepareForm($form_id, &$form, FormStateInterface &$form_state) { // submitted form value appears literally, regardless of custom #tree // and #parents being set elsewhere. '#parents' => array('form_token'), - '#cache' => [ - 'max-age' => 0, - ], ); } } diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php index c2e03c9..72c4517 100644 --- a/core/lib/Drupal/Core/Render/Renderer.php +++ b/core/lib/Drupal/Core/Render/Renderer.php @@ -230,6 +230,17 @@ protected function doRender(&$elements, $is_root_call = FALSE) { return ''; } + // Do not print elements twice. + if (!empty($elements['#printed'])) { + return ''; + } + + $context = $this->getCurrentRenderContext(); + if (!isset($context)) { + throw new \LogicException("Render context is empty, because render() was called outside of a renderRoot() or renderPlain() call. Use renderPlain()/renderRoot() or #lazy_builder/#pre_render instead."); + } + $context->push(new BubbleableMetadata()); + if (!isset($elements['#access']) && isset($elements['#access_callback'])) { if (is_string($elements['#access_callback']) && strpos($elements['#access_callback'], '::') === FALSE) { $elements['#access_callback'] = $this->controllerResolver->getControllerFromDefinition($elements['#access_callback']); @@ -244,25 +255,17 @@ protected function doRender(&$elements, $is_root_call = FALSE) { if ($elements['#access'] instanceof AccessResultInterface) { $this->addCacheableDependency($elements, $elements['#access']); if (!$elements['#access']->isAllowed()) { + $context->update($elements); + $context->bubble(); return ''; } } elseif ($elements['#access'] === FALSE) { + $context->pop(); return ''; } } - // Do not print elements twice. - if (!empty($elements['#printed'])) { - return ''; - } - - $context = $this->getCurrentRenderContext(); - if (!isset($context)) { - throw new \LogicException("Render context is empty, because render() was called outside of a renderRoot() or renderPlain() call. Use renderPlain()/renderRoot() or #lazy_builder/#pre_render instead."); - } - $context->push(new BubbleableMetadata()); - // Set the bubbleable rendering metadata that has configurable defaults, if: // - this is the root call, to ensure that the final render array definitely // has these configurable defaults, even when no subtree is render cached. diff --git a/core/modules/ckeditor/src/Tests/CKEditorLoadingTest.php b/core/modules/ckeditor/src/Tests/CKEditorLoadingTest.php index 1081eb9..92e694e 100644 --- a/core/modules/ckeditor/src/Tests/CKEditorLoadingTest.php +++ b/core/modules/ckeditor/src/Tests/CKEditorLoadingTest.php @@ -7,6 +7,7 @@ namespace Drupal\ckeditor\Tests; +use Drupal\Core\Cache\Cache; use Drupal\simpletest\WebTestBase; /** @@ -126,6 +127,7 @@ function testLoading() { // configuration also results in modified CKEditor configuration, so we // don't test that here. \Drupal::service('module_installer')->install(array('ckeditor_test')); + Cache::invalidateTags(['rendered']); $this->container->get('plugin.manager.ckeditor.plugin')->clearCachedDefinitions(); $editor_settings = $editor->getSettings(); $editor_settings['toolbar']['rows'][0][0]['items'][] = 'Llama'; diff --git a/core/modules/comment/src/Tests/CommentDefaultFormatterCacheTagsTest.php b/core/modules/comment/src/Tests/CommentDefaultFormatterCacheTagsTest.php index 327a354..4346827 100644 --- a/core/modules/comment/src/Tests/CommentDefaultFormatterCacheTagsTest.php +++ b/core/modules/comment/src/Tests/CommentDefaultFormatterCacheTagsTest.php @@ -78,6 +78,7 @@ public function testCacheTags() { 'config:field.field.entity_test.entity_test.comment', 'config:field.storage.comment.comment_body', 'config:user.settings', + 'user:1', ]; sort($expected_cache_tags); $this->assertEqual($build['#cache']['tags'], $expected_cache_tags); @@ -124,6 +125,7 @@ public function testCacheTags() { 'config:field.field.entity_test.entity_test.comment', 'config:field.storage.comment.comment_body', 'config:user.settings', + 'user:1', ]; sort($expected_cache_tags); $this->assertEqual($build['#cache']['tags'], $expected_cache_tags); diff --git a/core/modules/contact/src/MessageForm.php b/core/modules/contact/src/MessageForm.php index 9cb5e54..c576a56 100644 --- a/core/modules/contact/src/MessageForm.php +++ b/core/modules/contact/src/MessageForm.php @@ -120,6 +120,7 @@ public function form(array $form, FormStateInterface $form_state) { '#title' => $this->t('Your email address'), '#required' => TRUE, ); + $form['#cache']['contexts'][] = 'user.roles:authenticated'; if ($user->isAnonymous()) { $form['#attached']['library'][] = 'core/drupal.form'; $form['#attributes']['data-user-info-from-browser'] = TRUE; @@ -131,11 +132,13 @@ public function form(array $form, FormStateInterface $form_state) { $form['name']['#value'] = $user->getUsername(); $form['name']['#required'] = FALSE; $form['name']['#plain_text'] = $user->getUsername(); + $form['name']['#cache']['contexts'][] = 'user'; $form['mail']['#type'] = 'item'; $form['mail']['#value'] = $user->getEmail(); $form['mail']['#required'] = FALSE; $form['mail']['#plain_text'] = $user->getEmail(); + $form['mail']['#cache']['contexts'][] = 'user'; } // The user contact form has a preset recipient. diff --git a/core/modules/contact/src/Tests/ContactAuthenticatedUserTest.php b/core/modules/contact/src/Tests/ContactAuthenticatedUserTest.php index 2adc4c2..923878e 100644 --- a/core/modules/contact/src/Tests/ContactAuthenticatedUserTest.php +++ b/core/modules/contact/src/Tests/ContactAuthenticatedUserTest.php @@ -21,19 +21,29 @@ class ContactAuthenticatedUserTest extends WebTestBase { * * @var array */ - public static $modules = array('contact'); + public static $modules = array('contact', 'contact_test'); /** * Tests that name and email fields are not present for authenticated users. */ function testContactSiteWideTextfieldsLoggedInTestCase() { - $this->drupalLogin($this->drupalCreateUser(array('access site-wide contact form'))); + $user1 = $this->drupalCreateUser(array('access site-wide contact form')); + $this->drupalLogin($user1); $this->drupalGet('contact'); + $this->assertResponse(200); + $this->assertCacheContext('user'); // Ensure that there is no textfield for name. $this->assertFalse($this->xpath('//input[@name=:name]', array(':name' => 'name'))); + $this->assertRaw($user1->getAccountName()); // Ensure that there is no textfield for email. $this->assertFalse($this->xpath('//input[@name=:name]', array(':name' => 'mail'))); + + // Log in as a different user and confirm that + $user2 = $this->drupalCreateUser(array('access site-wide contact form')); + $this->drupalLogin($user2); + $this->drupalGet('contact'); + $this->assertRaw($user2->getAccountName()); } } diff --git a/core/modules/editor/src/Element.php b/core/modules/editor/src/Element.php index 43b99ba..41ffedf 100644 --- a/core/modules/editor/src/Element.php +++ b/core/modules/editor/src/Element.php @@ -53,6 +53,7 @@ function preRenderTextFormat(array $element) { $format_ids = array_keys($element['format']['format']['#options']); // Early-return if no text editor is associated with any of the text formats. + $element['#cache']['tags'][] = 'config:editor_list'; $editors = Editor::loadMultiple($format_ids); foreach ($editors as $key => $editor) { $definition = $this->pluginManager->getDefinition($editor->getEditor()); diff --git a/core/modules/editor/src/Tests/EditorLoadingTest.php b/core/modules/editor/src/Tests/EditorLoadingTest.php index 002ccd3..95dda17 100644 --- a/core/modules/editor/src/Tests/EditorLoadingTest.php +++ b/core/modules/editor/src/Tests/EditorLoadingTest.php @@ -245,6 +245,7 @@ public function testSupportedElementTypes() { // Assert the unicorn editor works with textfields. $this->drupalLogin($this->privilegedUser); $this->drupalGet('node/1/edit'); + $this->assertCacheTag('config:editor_list'); list( , $editor_settings_present, $editor_js_present, $field, $format_selector) = $this->getThingsToCheck('field-text', 'input'); $this->assertTrue($editor_settings_present, "Text Editor module's JavaScript settings are on the page."); $this->assertTrue($editor_js_present, 'Text Editor JavaScript is present.'); @@ -261,6 +262,7 @@ public function testSupportedElementTypes() { ))->save(); $this->drupalGet('node/1/edit'); + $this->assertCacheTag('config:editor_list'); list( , $editor_settings_present, $editor_js_present, $field, $format_selector) = $this->getThingsToCheck('field-text', 'input'); $this->assertFalse($editor_settings_present, "Text Editor module's JavaScript settings are not on the page."); $this->assertFalse($editor_js_present, 'Text Editor JavaScript is not present.'); diff --git a/core/modules/editor/src/Tests/EditorSecurityTest.php b/core/modules/editor/src/Tests/EditorSecurityTest.php index bb39450..db15b05 100644 --- a/core/modules/editor/src/Tests/EditorSecurityTest.php +++ b/core/modules/editor/src/Tests/EditorSecurityTest.php @@ -8,6 +8,7 @@ namespace Drupal\editor\Tests; use Drupal\Component\Serialization\Json; +use Drupal\Core\Cache\Cache; use Drupal\simpletest\WebTestBase; /** @@ -428,6 +429,7 @@ function testEditorXssFilterOverride() { // Enable editor_test.module's hook_editor_xss_filter_alter() implementation // to alter the text editor XSS filter class being used. \Drupal::state()->set('editor_test_editor_xss_filter_alter_enabled', TRUE); + Cache::invalidateTags(['rendered']); // First: the Insecure text editor XSS filter. $this->drupalGet('node/2/edit'); diff --git a/core/modules/filter/src/Element/TextFormat.php b/core/modules/filter/src/Element/TextFormat.php index 9247dba..9b3000f 100644 --- a/core/modules/filter/src/Element/TextFormat.php +++ b/core/modules/filter/src/Element/TextFormat.php @@ -210,6 +210,8 @@ public static function processFormat(&$element, FormStateInterface $form_state, $user_has_access = isset($formats[$element['#format']]); $user_is_admin = $user->hasPermission('administer filters'); + $element['#cache']['tags'][] = 'config:filter_format_list'; + // If the stored format does not exist or if it is not among the allowed // formats for this textarea, administrators have to assign a new format. if ((!$format_exists || !$format_allowed) && $user_is_admin) { diff --git a/core/modules/language/language.module b/core/modules/language/language.module index 30fedc6..53d2f69 100644 --- a/core/modules/language/language.module +++ b/core/modules/language/language.module @@ -163,6 +163,7 @@ function language_process_language_select($element) { foreach (\Drupal::languageManager()->getLanguages($element['#languages']) as $langcode => $language) { $element['#options'][$langcode] = $language->isLocked() ? t('- @name -', array('@name' => $language->getName())) : $language->getName(); } + $element['#cache']['tags'][] = 'config:language_content_settings_list'; } return $element; } @@ -413,7 +414,7 @@ function language_form_alter(&$form, FormStateInterface $form_state) { $langcode_key = $entity_type->getKey('langcode'); if (isset($form[$langcode_key])) { $language_configuration = ContentLanguageSettings::loadByEntityTypeBundle($entity->getEntityTypeId(), $entity->bundle()); - $form[$langcode_key]['#access'] = $language_configuration->isLanguageAlterable(); + $form[$langcode_key]['#access'] = AccessResult::allowedIf($language_configuration->isLanguageAlterable())->addCacheableDependency($language_configuration); } } } @@ -440,7 +441,7 @@ function language_entity_field_access($operation, FieldDefinitionInterface $fiel // Grant access depending on whether the entity language can be altered. $entity = $items->getEntity(); $config = ContentLanguageSettings::loadByEntityTypeBundle($entity->getEntityTypeId(), $entity->bundle()); - return AccessResult::forbiddenIf(!$config->isLanguageAlterable()); + return AccessResult::forbiddenIf(!$config->isLanguageAlterable())->addCacheableDependency($config); } } return AccessResult::neutral(); diff --git a/core/modules/language/src/Element/LanguageConfiguration.php b/core/modules/language/src/Element/LanguageConfiguration.php index 2fad091..6dc2833 100644 --- a/core/modules/language/src/Element/LanguageConfiguration.php +++ b/core/modules/language/src/Element/LanguageConfiguration.php @@ -56,6 +56,7 @@ public static function processLanguageConfiguration(&$element, FormStateInterfac '#title' => t('Show language selector on create and edit pages'), '#default_value' => ($default_config != NULL) ? $default_config->isLanguageAlterable() : FALSE, ); + $element['#cache']['tags'][] = 'config:language_content_settings_list'; // Add the entity type and bundle information to the form if they are set. // They will be used, in the submit handler, to generate the names of the diff --git a/core/modules/language/src/Entity/ContentLanguageSettings.php b/core/modules/language/src/Entity/ContentLanguageSettings.php index 7504998..8a44120 100644 --- a/core/modules/language/src/Entity/ContentLanguageSettings.php +++ b/core/modules/language/src/Entity/ContentLanguageSettings.php @@ -203,4 +203,18 @@ public function calculateDependencies() { return $this->dependencies; } + /** + * {@inheritdoc} + */ + public function getCacheTagsToInvalidate() { + // Because ::loadByEntityTypeBundle() auto-creates config entities on demand + // without saving them, we cannot rely on individual cache tags anywhere, + // because they're cache tags for ephemeral config entities. Therefore, the + // only cache tag we can rely on for this config entity, is its list cache + // tag. + // @todo Remove this once ::loadByEntityTypeBundle() no longer creates + // ephemeral (unsaved) ContentLanguageSettings config entities. + return $this->getEntityType()->getListCacheTags(); + } + } diff --git a/core/modules/language/src/Tests/LanguageConfigurationElementTest.php b/core/modules/language/src/Tests/LanguageConfigurationElementTest.php index 9591618..95d523a 100644 --- a/core/modules/language/src/Tests/LanguageConfigurationElementTest.php +++ b/core/modules/language/src/Tests/LanguageConfigurationElementTest.php @@ -37,6 +37,7 @@ protected function setUp() { */ public function testLanguageConfigurationElement() { $this->drupalGet('language-tests/language_configuration_element'); + $this->assertCacheTag('config:language_content_settings_list'); $edit['lang_configuration[langcode]'] = 'current_interface'; $edit['lang_configuration[language_alterable]'] = FALSE; $this->drupalPostForm(NULL, $edit, 'Save'); diff --git a/core/modules/node/src/Controller/NodePreviewController.php b/core/modules/node/src/Controller/NodePreviewController.php index b1009be..e8ad426 100644 --- a/core/modules/node/src/Controller/NodePreviewController.php +++ b/core/modules/node/src/Controller/NodePreviewController.php @@ -26,6 +26,7 @@ public function view(EntityInterface $node_preview, $view_mode_id = 'full', $lan // Don't render cache previews. unset($build['#cache']); + $build['#cache']['max-age'] = 0; foreach ($node_preview->uriRelationships() as $rel) { // Set the node path as the canonical URL to prevent duplicate content. diff --git a/core/modules/node/src/NodeForm.php b/core/modules/node/src/NodeForm.php index 36cfc74..d1713c5 100644 --- a/core/modules/node/src/NodeForm.php +++ b/core/modules/node/src/NodeForm.php @@ -76,6 +76,9 @@ public function form(array $form, FormStateInterface $form_state) { $uuid = $this->entity->uuid(); $store = $this->tempStoreFactory->get('node_preview'); + // Because of the temp store integration, this is not cacheable. + $form['#cache']['max-age'] = 0; + // If the user is creating a new node, the UUID is passed in the request. if ($request_uuid = \Drupal::request()->query->get('uuid')) { $uuid = $request_uuid; diff --git a/core/modules/node/src/Plugin/Search/NodeSearch.php b/core/modules/node/src/Plugin/Search/NodeSearch.php index ecd6e86..8956dd9 100644 --- a/core/modules/node/src/Plugin/Search/NodeSearch.php +++ b/core/modules/node/src/Plugin/Search/NodeSearch.php @@ -518,6 +518,11 @@ public function searchFormAlter(array &$form, FormStateInterface $form_state) { '#attributes' => array('class' => array('search-advanced')), '#access' => $this->account && $this->account->hasPermission('use advanced search'), '#open' => $used_advanced, + '#cache' => [ + 'contexts' => [ + 'url.query_args', + ], + ], ); $form['advanced']['keywords-fieldset'] = array( '#type' => 'fieldset', diff --git a/core/modules/node/src/Tests/NodeTypeInitialLanguageTest.php b/core/modules/node/src/Tests/NodeTypeInitialLanguageTest.php index f7df02f..0614937 100644 --- a/core/modules/node/src/Tests/NodeTypeInitialLanguageTest.php +++ b/core/modules/node/src/Tests/NodeTypeInitialLanguageTest.php @@ -47,6 +47,7 @@ function testNodeTypeInitialLanguageDefaults() { $this->assert(empty($language_field), 'Language field is not visible on manage fields tab.'); $this->drupalGet('node/add/article'); + $this->assertCacheTag('config:language_content_settings_list'); $this->assertNoField('langcode', 'Language is not selectable on node add/edit page by default.'); // Adds a new language and set it as default. @@ -66,6 +67,7 @@ function testNodeTypeInitialLanguageDefaults() { ); $this->drupalPostForm('admin/structure/types/manage/article', $edit, t('Save content type')); $this->drupalGet('node/add/article'); + $this->assertCacheTag('config:language_content_settings_list'); $this->assertField('langcode[0][value]', 'Language is selectable on node add/edit page when language not hidden.'); $this->assertOptionSelected('edit-langcode-0-value', 'hu', 'The initial language is the site default on the node add page after the site default language is changed.'); diff --git a/core/modules/search/src/Controller/SearchController.php b/core/modules/search/src/Controller/SearchController.php index 4d839d1..9cb364e 100644 --- a/core/modules/search/src/Controller/SearchController.php +++ b/core/modules/search/src/Controller/SearchController.php @@ -73,6 +73,9 @@ public function view(Request $request, SearchPageInterface $entity) { $build = array(); $plugin = $entity->getPlugin(); + // @todo Remove once https://www.drupal.org/node/2464409 lands. + $build['#cache']['max-age'] = 0; + // Build the form first, because it may redirect during the submit, // and we don't want to build the results based on last time's request. $build['#cache']['contexts'][] = 'url.query_args:keys'; diff --git a/core/modules/search/src/Tests/SearchAdvancedSearchFormTest.php b/core/modules/search/src/Tests/SearchAdvancedSearchFormTest.php index 3650a93..278718c 100644 --- a/core/modules/search/src/Tests/SearchAdvancedSearchFormTest.php +++ b/core/modules/search/src/Tests/SearchAdvancedSearchFormTest.php @@ -59,6 +59,7 @@ function testNodeType() { // Search for the title of the node with a POST query. $edit = array('or' => $this->node->label()); $this->drupalPostForm('search/node', $edit, t('Advanced search')); + $this->assertCacheContext('url.query_args'); $this->assertText($this->node->label(), 'Basic page node is found with POST query.'); // Search by node type. diff --git a/core/modules/system/src/Tests/Entity/EntityTranslationFormTest.php b/core/modules/system/src/Tests/Entity/EntityTranslationFormTest.php index 4ff9f22..0de7285 100644 --- a/core/modules/system/src/Tests/Entity/EntityTranslationFormTest.php +++ b/core/modules/system/src/Tests/Entity/EntityTranslationFormTest.php @@ -59,6 +59,7 @@ function testEntityFormLanguage() { $edit['title[0][value]'] = $this->randomMachineName(8); $edit['body[0][value]'] = $this->randomMachineName(16); $this->drupalGet('node/add/page'); + $this->assertCacheTag('config:language_content_settings_list'); $form_langcode = \Drupal::state()->get('entity_test.form_langcode'); $this->drupalPostForm(NULL, $edit, t('Save')); @@ -68,6 +69,7 @@ function testEntityFormLanguage() { // Edit the node and test the form language. $this->drupalGet($this->langcodes[0] . '/node/' . $node->id() . '/edit'); + $this->assertCacheTag('config:language_content_settings_list'); $form_langcode = \Drupal::state()->get('entity_test.form_langcode'); $this->assertTrue($node->language()->getId() == $form_langcode, 'Form language is the same as the entity language.'); diff --git a/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php b/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php index e5f2963..f37a4c7 100644 --- a/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php +++ b/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php @@ -820,7 +820,7 @@ public function providerTestInvalidToken() { * * @dataProvider providerTestFormTokenCacheability */ - public function testFormTokenCacheability($token, $is_authenticated, $expected_form_cacheability, $expected_token_cacheability, $method) { + public function testFormTokenCacheability($token, $is_authenticated, $method) { $user = $this->prophesize(AccountProxyInterface::class); $user->isAuthenticated() ->willReturn($is_authenticated); @@ -845,19 +845,32 @@ public function testFormTokenCacheability($token, $is_authenticated, $expected_f $form_state = new FormState(); $built_form = $this->formBuilder->buildForm($form_arg, $form_state); - if (!isset($expected_form_cacheability) || ($method == 'get' && !is_string($token))) { + + // FormBuilder does not even consider to set a form token when: + // - #token = FALSE (opting out explicitly) + // - #method = GET and #token is not set to a string (GET forms don't get a + // form token by default, and this form did not explicitly opt in) + if ($token === FALSE || ($method == 'get' && !is_string($token))) { $this->assertFalse(isset($built_form['#cache'])); - } - else { - $this->assertTrue(isset($built_form['#cache'])); - $this->assertEquals($expected_form_cacheability, $built_form['#cache']); - } - if (!isset($expected_token_cacheability)) { $this->assertFalse(isset($built_form['form_token'])); } + // Otherwise, a form token is set, but only if the user is logged in. It is + // impossible (and unnecessary) to set a form token if the user is not + // logged in, because there is no session, and hence no CSRF token. else { - $this->assertTrue(isset($built_form['form_token'])); - $this->assertEquals($expected_token_cacheability, $built_form['form_token']['#cache']); + // For forms that are eligible for form tokens, a cache context must be + // set that indicates the form token only exists for logged in users. + $this->assertTrue(isset($built_form['#cache'])); + $this->assertEquals(['contexts' => ['user.roles:authenticated']], $built_form['#cache']); + // Finally, verify that a form token is generated when appropriate, with + // the expected cacheability metadata (or lack thereof). + if (!$is_authenticated) { + $this->assertFalse(isset($built_form['form_token'])); + } + else { + $this->assertTrue(isset($built_form['form_token'])); + $this->assertFalse(isset($built_form['form_token']['#cache'])); + } } } @@ -868,12 +881,12 @@ public function testFormTokenCacheability($token, $is_authenticated, $expected_f */ function providerTestFormTokenCacheability() { return [ - 'token:none,authenticated:true' => [NULL, TRUE, ['contexts' => ['user.roles:authenticated']], ['max-age' => 0], 'post'], - 'token:none,authenticated:false' => [NULL, FALSE, ['contexts' => ['user.roles:authenticated']], NULL, 'post'], - 'token:false,authenticated:false' => [FALSE, FALSE, NULL, NULL, 'post'], - 'token:false,authenticated:true' => [FALSE, TRUE, NULL, NULL, 'post'], - 'token:none,authenticated:false,method:get' => [NULL, FALSE, ['contexts' => ['user.roles:authenticated']], NULL, 'get'], - 'token:test_form_id,authenticated:false,method:get' => ['test_form_id', TRUE, ['contexts' => ['user.roles:authenticated']], ['max-age' => 0], 'get'], + 'token:none,authenticated:true' => [NULL, TRUE, 'post'], + 'token:none,authenticated:false' => [NULL, FALSE, 'post'], + 'token:false,authenticated:false' => [FALSE, FALSE, 'post'], + 'token:false,authenticated:true' => [FALSE, TRUE, 'post'], + 'token:none,authenticated:false,method:get' => [NULL, FALSE, 'get'], + 'token:test_form_id,authenticated:true,method:get' => ['test_form_id', TRUE, 'get'], ]; } diff --git a/core/tests/Drupal/Tests/Core/Render/RendererTest.php b/core/tests/Drupal/Tests/Core/Render/RendererTest.php index 4d0dec5..0308df1 100644 --- a/core/tests/Drupal/Tests/Core/Render/RendererTest.php +++ b/core/tests/Drupal/Tests/Core/Render/RendererTest.php @@ -502,7 +502,27 @@ public function testRenderAccessCacheablityDependencyInheritance() { $build = [ '#access' => AccessResult::allowed()->addCacheContexts(['user']), ]; + $this->renderer->renderPlain($build); + $build = [ + '#access' => AccessResult::forbidden()->addCacheContexts(['user']), + ]; + $this->renderer->renderPlain($build); + + // Simulate the theme system/Twig: a recursive call to Renderer::render(), + // just like the theme system or a Twig template would have done. + $this->themeManager->expects($this->any()) + ->method('render') + ->willReturnCallback(function ($hook, $vars) { + return $this->renderer->render($vars['child']); + }); + + $build = [ + '#theme' => 'something', + 'child' => [ + '#access' => AccessResult::forbidden()->addCacheContexts(['user']), + ], + ]; $this->renderer->renderPlain($build); $this->assertEquals(['languages:language_interface', 'theme', 'user'], $build['#cache']['contexts']);