.../Core/Entity/Entity/EntityFormDisplay.php | 2 +- core/lib/Drupal/Core/Render/Renderer.php | 25 ++++++++++++---------- core/modules/language/language.module | 5 +++-- .../language/src/Element/LanguageConfiguration.php | 1 + .../src/Entity/ContentLanguageSettings.php | 14 ++++++++++++ .../src/Tests/LanguageConfigurationElementTest.php | 1 + .../node/src/Tests/NodeTypeInitialLanguageTest.php | 2 ++ .../src/Tests/Entity/EntityTranslationFormTest.php | 2 ++ .../Drupal/Tests/Core/Render/RendererTest.php | 20 +++++++++++++++++ 9 files changed, 58 insertions(+), 14 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/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/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/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/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/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']);