.../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 | 8 ++-- core/modules/contact/src/MessageForm.php | 3 ++ .../src/Tests/ContactAuthenticatedUserTest.php | 14 ++++++- core/modules/editor/src/Element.php | 2 + .../modules/editor/src/Tests/EditorLoadingTest.php | 2 + .../editor/src/Tests/EditorSecurityTest.php | 2 + core/modules/filter/src/Element/TextFormat.php | 3 ++ core/modules/language/language.module | 6 ++- .../language/src/Element/LanguageConfiguration.php | 2 + .../src/Entity/ContentLanguageSettings.php | 14 +++++++ .../src/Tests/LanguageConfigurationElementTest.php | 1 + .../node/src/Controller/NodePreviewController.php | 4 +- core/modules/node/src/NodeForm.php | 3 ++ core/modules/node/src/Plugin/Search/NodeSearch.php | 5 +++ .../node/src/Tests/NodeTypeInitialLanguageTest.php | 2 + .../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 ++++++++++ 23 files changed, 132 insertions(+), 39 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 bb7034f..6f31650 100644 --- a/core/lib/Drupal/Core/Form/FormBuilder.php +++ b/core/lib/Drupal/Core/Form/FormBuilder.php @@ -769,9 +769,6 @@ public function prepareForm($form_id, &$form, FormStateInterface &$form_state) { ] ] ], - '#cache' => [ - 'max-age' => 0, - ], ); } } diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php index 9fc7053..a858746 100644 --- a/core/lib/Drupal/Core/Render/Renderer.php +++ b/core/lib/Drupal/Core/Render/Renderer.php @@ -214,6 +214,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']); @@ -228,25 +239,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 1164ba0..074df52 100644 --- a/core/modules/comment/src/Tests/CommentDefaultFormatterCacheTagsTest.php +++ b/core/modules/comment/src/Tests/CommentDefaultFormatterCacheTagsTest.php @@ -89,6 +89,7 @@ public function testCacheTags() { 'config:field.field.comment.comment.comment_body', 'config:field.field.entity_test.entity_test.comment', 'config:field.storage.comment.comment_body', + 'config:filter_format_list', 'config:user.settings', ]; sort($expected_cache_tags); @@ -135,6 +136,7 @@ public function testCacheTags() { 'config:field.field.comment.comment.comment_body', 'config:field.field.entity_test.entity_test.comment', 'config:field.storage.comment.comment_body', + 'config:filter_format_list', 'config:user.settings', ]; sort($expected_cache_tags); @@ -149,10 +151,10 @@ public function testCacheTags() { ->view($commented_entity); $renderer->renderRoot($build); - // The entity itself was cached but the top-level element is max-age 0 due - // to the bubbled up max age due to the lazy-built comment form. + // The entity itself was cached, and the the top-level element also has a + // non-zero max-age despite the lazy-built form. $this->assertIdentical(Cache::PERMANENT, $build['entity']['#cache']['max-age']); - $this->assertIdentical(0, $build['#cache']['max-age'], 'Top level render array has max-age 0'); + $this->assertIdentical(Cache::PERMANENT, $build['#cache']['max-age'], 'Comment form is cacheable.'); // The children (fields) of the entity render array are only built in case // of a cache miss. diff --git a/core/modules/contact/src/MessageForm.php b/core/modules/contact/src/MessageForm.php index 46bb5d9..ac435ae 100644 --- a/core/modules/contact/src/MessageForm.php +++ b/core/modules/contact/src/MessageForm.php @@ -118,6 +118,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; @@ -129,11 +130,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..4af65a1 100644 --- a/core/modules/editor/src/Element.php +++ b/core/modules/editor/src/Element.php @@ -7,6 +7,7 @@ namespace Drupal\editor; +use Drupal\Core\Cache\Cache; use Drupal\editor\Entity\Editor; use Drupal\filter\Entity\FilterFormat; use Drupal\Component\Plugin\PluginManagerInterface; @@ -53,6 +54,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'] = Cache::mergeTags(isset($element['#cache']['tags']) ? $element['#cache']['tags'] : [], \Drupal::entityManager()->getDefinition('editor')->getListCacheTags()); $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..08ec542 100644 --- a/core/modules/filter/src/Element/TextFormat.php +++ b/core/modules/filter/src/Element/TextFormat.php @@ -7,6 +7,7 @@ namespace Drupal\filter\Element; +use Drupal\Core\Cache\Cache; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Render\Element\RenderElement; use Drupal\Core\Render\Element; @@ -210,6 +211,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'] = Cache::mergeTags(isset($element['#cache']['tags']) ? $element['#cache']['tags'] : [], \Drupal::entityManager()->getDefinition('filter_format')->getListCacheTags()); + // 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 e92baa2..750c36a 100644 --- a/core/modules/language/language.module +++ b/core/modules/language/language.module @@ -6,6 +6,7 @@ */ use Drupal\Core\Access\AccessResult; +use Drupal\Core\Cache\Cache; use Drupal\Core\Entity\ContentEntityFormInterface; use Drupal\Core\Entity\EntityFormInterface; use Drupal\Core\Entity\EntityInterface; @@ -157,6 +158,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'] = Cache::mergeTags(isset($element['#cache']['tags']) ? $element['#cache']['tags'] : [], \Drupal::entityManager()->getDefinition('language_content_settings')->getListCacheTags()); } return $element; } @@ -407,7 +409,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); } } } @@ -434,7 +436,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..a7e0f1e 100644 --- a/core/modules/language/src/Element/LanguageConfiguration.php +++ b/core/modules/language/src/Element/LanguageConfiguration.php @@ -7,6 +7,7 @@ namespace Drupal\language\Element; +use Drupal\Core\Cache\Cache; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Render\Element\FormElement; @@ -56,6 +57,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'] = Cache::mergeTags(isset($element['#cache']['tags']) ? $element['#cache']['tags'] : [], \Drupal::entityManager()->getDefinition('language_content_settings')->getListCacheTags()); // 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 7501012..171c8d7 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; } + /** + * {@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..540adc6 100644 --- a/core/modules/node/src/Controller/NodePreviewController.php +++ b/core/modules/node/src/Controller/NodePreviewController.php @@ -25,7 +25,9 @@ public function view(EntityInterface $node_preview, $view_mode_id = 'full', $lan $build['#attached']['library'][] = 'node/drupal.node.preview'; // 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 bb6c49d..6d10f15 100644 --- a/core/modules/node/src/NodeForm.php +++ b/core/modules/node/src/NodeForm.php @@ -75,6 +75,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 0fa464e..0e2e226 100644 --- a/core/modules/node/src/Plugin/Search/NodeSearch.php +++ b/core/modules/node/src/Plugin/Search/NodeSearch.php @@ -530,6 +530,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/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 805f682..b216730 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 7bd51bc..26f6ea6 100644 --- a/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php +++ b/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php @@ -817,7 +817,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); @@ -842,19 +842,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'])); + } } } @@ -865,12 +878,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 48b394e..0a35c17 100644 --- a/core/tests/Drupal/Tests/Core/Render/RendererTest.php +++ b/core/tests/Drupal/Tests/Core/Render/RendererTest.php @@ -489,7 +489,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']);