diff --git a/core/lib/Drupal/Core/Entity/EntityViewBuilder.php b/core/lib/Drupal/Core/Entity/EntityViewBuilder.php index 0d3fcf421ce3216504889f99cd020dd5ff73ee9b..8892926ccb741802490f0ab6f55299b18a272ed5 100644 --- a/core/lib/Drupal/Core/Entity/EntityViewBuilder.php +++ b/core/lib/Drupal/Core/Entity/EntityViewBuilder.php @@ -9,6 +9,7 @@ use Drupal\Core\Field\FieldItemInterface; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Language\LanguageManagerInterface; +use Drupal\Core\Logger\LoggerChannelTrait; use Drupal\Core\Render\Element; use Drupal\Core\Security\TrustedCallbackInterface; use Drupal\Core\Theme\Registry; @@ -22,6 +23,8 @@ */ class EntityViewBuilder extends EntityHandlerBase implements EntityHandlerInterface, EntityViewBuilderInterface, TrustedCallbackInterface { + use LoggerChannelTrait; + /** * The type of entities for which this view builder is instantiated. * @@ -80,6 +83,15 @@ class EntityViewBuilder extends EntityHandlerBase implements EntityHandlerInterf */ protected $singleFieldDisplays; + /** + * A collection of keys. + * + * It identifies rendering in progress, used to prevent recursion. + * + * @var array + */ + protected array $recursionKeys = []; + /** * Constructs a new EntityViewBuilder. * @@ -107,13 +119,15 @@ public function __construct(EntityTypeInterface $entity_type, EntityRepositoryIn * {@inheritdoc} */ public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { - return new static( + $instance = new static( $entity_type, $container->get('entity.repository'), $container->get('language_manager'), $container->get('theme.registry'), $container->get('entity_display.repository') ); + $instance->setLoggerFactory($container->get('logger.factory')); + return $instance; } /** @@ -136,7 +150,12 @@ public function view(EntityInterface $entity, $view_mode = 'full', $langcode = N * {@inheritdoc} */ public static function trustedCallbacks() { - return ['build', 'buildMultiple']; + return [ + 'build', + 'buildMultiple', + 'setRecursiveRenderProtection', + 'unsetRecursiveRenderProtection', + ]; } /** @@ -188,24 +207,25 @@ protected function getBuildDefaults(EntityInterface $entity, $view_mode) { 'max-age' => $entity->getCacheMaxAge(), ], ]; + // Add callbacks to protect from recursive rendering. + $build['#pre_render'] = [[$this, 'setRecursiveRenderProtection']]; + $build['#post_render'] = [[$this, 'unsetRecursiveRenderProtection']]; // Add the default #theme key if a template exists for it. if ($this->themeRegistry->getRuntime()->has($this->entityTypeId)) { $build['#theme'] = $this->entityTypeId; } - $keys = [ - 'entity_view', - $this->entityTypeId, - $entity->id(), - $view_mode, - ]; - // Cache the rendered output if permitted by the view mode and global entity // type configuration. if ($this->isViewModeCacheable($view_mode) && !$entity->isNew() && $entity->isDefaultRevision() && $this->entityType->isRenderCacheable()) { $build['#cache'] += [ - 'keys' => $keys, + 'keys' => [ + 'entity_view', + $this->entityTypeId, + $entity->id(), + $view_mode, + ], 'bin' => $this->cacheBin, ]; @@ -214,15 +234,6 @@ protected function getBuildDefaults(EntityInterface $entity, $view_mode) { } } - // Add keys for the renderer to use to identify recursive rendering. - $build['#recursion_keys'] = $keys; - if ($entity instanceof RevisionableInterface) { - $build['#recursion_keys'][] = $entity->getRevisionId(); - } - if ($entity instanceof TranslatableDataInterface && count($entity->getTranslationLanguages()) > 1) { - $build['#recursion_keys'][] = $entity->language()->getId(); - } - return $build; } @@ -545,4 +556,38 @@ protected function getSingleFieldDisplay($entity, $field_name, $display_options) return $display; } + /** + * Entity render array #pre_render callback. + */ + public function setRecursiveRenderProtection(array $build): array { + // Checks whether entity render array with matching cache keys is being + // recursively rendered. If not already being rendered, add an entry to track + // that it is. + $recursion_key = implode(':', $build['#cache']['keys'] ?? []); + if (isset($this->recursionKeys[$recursion_key])) { + $this->getLogger('entity') + ->error('Recursive rendering attempt aborted for %key. In progress: %guards', [ + '%key' => $recursion_key, + '%guards' => print_r($this->recursionKeys, TRUE), + ]); + $build['#printed'] = TRUE; + } + else { + $this->recursionKeys[$recursion_key] = $recursion_key; + } + return $build; + } + + /** + * Entity render array #post_render callback. + */ + public function unsetRecursiveRenderProtection(string $renderedEntity, array $build): string { + // Removes rendered entity matching cache keys from recursive render + // tracking, once the entity has been rendered. + $recursion_key = implode(':', $build['#cache']['keys'] ?? []); + unset($this->recursionKeys[$recursion_key]); + + return $renderedEntity; + } + } diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/EntityReferenceEntityFormatter.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/EntityReferenceEntityFormatter.php index 9e0025eb8da677efab8b3a84469edde1a23c0d2a..63d3a68be05601d4a5e392059337cfb867a18497 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/EntityReferenceEntityFormatter.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/EntityReferenceEntityFormatter.php @@ -29,10 +29,11 @@ class EntityReferenceEntityFormatter extends EntityReferenceFormatterBase { * * @var int * - * @deprecated in drupal:9.4.0 and is removed from drupal:10.0.0. - * Use #recursion_keys in render arrays to prevent recursion. + * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. + * EntityViewBuilder #pre_render and #post_render callbacks prevent recursion. * * @see https://www.drupal.org/node/2940605 + * @see \Drupal\Core\Entity\EntityViewBuilder::getBuildDefaults() */ const RECURSIVE_RENDER_LIMIT = 20; diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php index 4dfd3d4c4510a66aef97b8e2ccd33ce9e225ad7c..fec1cf215c5a32ae3bf26c41779b047b58b422ab 100644 --- a/core/lib/Drupal/Core/Render/Renderer.php +++ b/core/lib/Drupal/Core/Render/Renderer.php @@ -96,15 +96,6 @@ class Renderer implements RendererInterface { */ protected static $contextCollection; - /** - * A collection of keys. - * - * It identifies rendering in progress, used to prevent recursion. - * - * @var array - */ - protected $recursionKeys = []; - /** * Constructs a new Renderer. * @@ -246,7 +237,6 @@ public function render(&$elements, $is_root_call = FALSE) { catch (\Exception $e) { // Mark the ::rootRender() call finished due to this exception & re-throw. $this->isRenderingRoot = FALSE; - $this->recursionKeys = []; throw $e; } } @@ -437,24 +427,7 @@ protected function doRender(&$elements, $is_root_call = FALSE) { $elements['#cache']['max-age'] = $elements['#cache']['max-age'] ?? Cache::PERMANENT; $elements['#attached'] = $elements['#attached'] ?? []; - // Guard against recursive rendering now that all other early return - // possibilities are exhausted and it's time to render children. - $recursion_key = implode(':', $elements['#recursion_keys'] ?? []); - if ($recursion_key) { - if (isset($this->recursionKeys[$recursion_key])) { - \Drupal::logger('render') - ->error('Recursive rendering attempt aborted for %key. In progress: %guards', [ - '%key' => $recursion_key, - '%guards' => print_r($this->recursionKeys, TRUE), - ]); - $elements['#printed'] = TRUE; - } - else { - $this->recursionKeys[$recursion_key] = $recursion_key; - } - } - - // Allow #pre_render or #recursion_keys to abort rendering. + // Allow #pre_render to abort rendering. if (!empty($elements['#printed'])) { // The #printed element contains all the bubbleable rendering metadata for // the subtree. @@ -626,7 +599,6 @@ protected function doRender(&$elements, $is_root_call = FALSE) { // Rendering is finished, all necessary info collected! $context->bubble(); - unset($this->recursionKeys[$recursion_key]); $elements['#printed'] = TRUE; return $elements['#markup']; } diff --git a/core/modules/comment/src/CommentViewBuilder.php b/core/modules/comment/src/CommentViewBuilder.php index 0e4e024dd574300e854de0bb3303d74f0eff55d8..b0a7af4f800edb22129d3256ae76a1085bd8fb20 100644 --- a/core/modules/comment/src/CommentViewBuilder.php +++ b/core/modules/comment/src/CommentViewBuilder.php @@ -61,7 +61,7 @@ public function __construct(EntityTypeInterface $entity_type, EntityRepositoryIn * {@inheritdoc} */ public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { - return new static( + $instance = new static( $entity_type, $container->get('entity.repository'), $container->get('language_manager'), @@ -70,6 +70,8 @@ public static function createInstance(ContainerInterface $container, EntityTypeI $container->get('entity_display.repository'), $container->get('entity_type.manager') ); + $instance->setLoggerFactory($container->get('logger.factory')); + return $instance; } /** diff --git a/core/modules/field/tests/src/Kernel/EntityReference/EntityReferenceFormatterTest.php b/core/modules/field/tests/src/Kernel/EntityReference/EntityReferenceFormatterTest.php index 73b452d5f21a2d566530e9e9defc15d8373230f9..ea84fda9d84ce51db40453cc97a4c795498a9a4a 100644 --- a/core/modules/field/tests/src/Kernel/EntityReference/EntityReferenceFormatterTest.php +++ b/core/modules/field/tests/src/Kernel/EntityReference/EntityReferenceFormatterTest.php @@ -6,7 +6,6 @@ use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Field\FieldStorageDefinitionInterface; -use Drupal\Core\Field\Plugin\Field\FieldFormatter\EntityReferenceEntityFormatter; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; use Drupal\filter\Entity\FilterFormat; @@ -262,7 +261,7 @@ public function testEntityFormatterRecursiveRenderingFailing() { ]); $referencing_entity->save(); - $count = EntityReferenceEntityFormatter::RECURSIVE_RENDER_LIMIT + 1; + $count = 21; $build = $view_builder->viewMultiple(array_fill(0, $count, $referencing_entity), 'default'); $output = $renderer->renderRoot($build); // The title of entity_test entities is printed twice by default, so we have