From 05e023d538f3b4dafa5f21594d6df584ab527d8c Mon Sep 17 00:00:00 2001 From: Alex Pott Date: Tue, 26 Apr 2016 11:22:39 +0100 Subject: [PATCH] Issue #2073753 by amateescu, Sweetchuck, amitaibu, dawehner: Fix and add tests for the recursive rendering protection of the 'Rendered entity' formatter --- .../EntityReferenceEntityFormatter.php | 58 ++++++++++++++--- .../EntityReferenceFormatterTest.php | 63 +++++++++++++++++++ 2 files changed, 111 insertions(+), 10 deletions(-) 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 ee19523789e..6f3ce021a63 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/EntityReferenceEntityFormatter.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/EntityReferenceEntityFormatter.php @@ -25,6 +25,13 @@ use Symfony\Component\DependencyInjection\ContainerInterface; */ class EntityReferenceEntityFormatter extends EntityReferenceFormatterBase implements ContainerFactoryPluginInterface { + /** + * The number of times this formatter allows rendering the same entity. + * + * @var int + */ + const RECURSIVE_RENDER_LIMIT = 20; + /** * The logger factory. * @@ -47,7 +54,19 @@ class EntityReferenceEntityFormatter extends EntityReferenceFormatterBase implem protected $entityDisplayRepository; /** - * Constructs a StringFormatter instance. + * An array of counters for the recursive rendering protection. + * + * Each counter takes into account all the relevant information about the + * field and the referenced entity that is being rendered. + * + * @see \Drupal\Core\Field\Plugin\Field\FieldFormatter\EntityReferenceEntityFormatter::viewElements() + * + * @var array + */ + protected static $recursiveRenderDepth = []; + + /** + * Constructs a EntityReferenceEntityFormatter instance. * * @param string $plugin_id * The plugin_id for the formatter. @@ -141,15 +160,35 @@ class EntityReferenceEntityFormatter extends EntityReferenceFormatterBase implem $elements = array(); foreach ($this->getEntitiesToView($items, $langcode) as $delta => $entity) { - // Protect ourselves from recursive rendering. - static $depth = 0; - $depth++; - if ($depth > 20) { - $this->loggerFactory->get('entity')->error('Recursive rendering detected when rendering entity @entity_type @entity_id. Aborting rendering.', array('@entity_type' => $entity->getEntityTypeId(), '@entity_id' => $entity->id())); - return $elements; - } - if ($entity->id()) { + // Due to render caching and delayed calls, the viewElements() method + // will be called later in the rendering process through a '#pre_render' + // callback, so we need to generate a counter that takes into account + // all the relevant information about this field and the referenced + // entity that is being rendered. + $recursive_render_id = $items->getFieldDefinition()->getTargetEntityTypeId() + . $items->getFieldDefinition()->getTargetBundle() + . $items->getName() + . $entity->id(); + + if (isset(static::$recursiveRenderDepth[$recursive_render_id])) { + static::$recursiveRenderDepth[$recursive_render_id]++; + } + else { + static::$recursiveRenderDepth[$recursive_render_id] = 1; + } + + // Protect ourselves from recursive rendering. + if (static::$recursiveRenderDepth[$recursive_render_id] > static::RECURSIVE_RENDER_LIMIT) { + $this->loggerFactory->get('entity')->error('Recursive rendering detected when rendering entity %entity_type: %entity_id, using the %field_name field on the %bundle_name bundle. Aborting rendering.', [ + '%entity_type' => $entity->getEntityTypeId(), + '%entity_id' => $entity->id(), + '%field_name' => $items->getName(), + '%bundle_name' => $items->getFieldDefinition()->getTargetBundle(), + ]); + return $elements; + } + $view_builder = $this->entityTypeManager->getViewBuilder($entity->getEntityTypeId()); $elements[$delta] = $view_builder->view($entity, $view_mode, $entity->language()->getId()); @@ -164,7 +203,6 @@ class EntityReferenceEntityFormatter extends EntityReferenceFormatterBase implem // This is an "auto_create" item. $elements[$delta] = array('#markup' => $entity->label()); } - $depth = 0; } return $elements; diff --git a/core/modules/field/tests/src/Kernel/EntityReference/EntityReferenceFormatterTest.php b/core/modules/field/tests/src/Kernel/EntityReference/EntityReferenceFormatterTest.php index a5bab760214..4894a118df5 100644 --- a/core/modules/field/tests/src/Kernel/EntityReference/EntityReferenceFormatterTest.php +++ b/core/modules/field/tests/src/Kernel/EntityReference/EntityReferenceFormatterTest.php @@ -5,6 +5,7 @@ namespace Drupal\Tests\field\Kernel\EntityReference; 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\field\Tests\EntityReference\EntityReferenceTestTrait; @@ -204,6 +205,68 @@ class EntityReferenceFormatterTest extends EntityKernelTestBase { $this->assertEqual($build[1]['#markup'], $this->unsavedReferencedEntity->label(), sprintf('The markup returned by the %s formatter is correct for an item with a unsaved entity.', $formatter)); } + /** + * Tests the recursive rendering protection of the entity formatter. + */ + public function testEntityFormatterRecursiveRendering() { + /** @var \Drupal\Core\Render\RendererInterface $renderer */ + $renderer = $this->container->get('renderer'); + $formatter = 'entity_reference_entity_view'; + $view_builder = $this->entityManager->getViewBuilder($this->entityType); + + // Set the default view mode to use the 'entity_reference_entity_view' + // formatter. + entity_get_display($this->entityType, $this->bundle, 'default') + ->setComponent($this->fieldName, [ + 'type' => $formatter, + ]) + ->save(); + + $referencing_entity_1 = entity_create($this->entityType, ['name' => $this->randomMachineName()]); + $referencing_entity_1->save(); + + // Create a self-reference. + $referencing_entity_1->{$this->fieldName}->entity = $referencing_entity_1; + $referencing_entity_1->save(); + + // Check that the recursive rendering stops after it reaches the specified + // limit. + $build = $view_builder->view($referencing_entity_1, 'default'); + $output = $renderer->renderRoot($build); + + // The title of entity_test entities is printed twice by default, so we have + // to multiply the formatter's recursive rendering protection limit by 2. + // Additionally, we have to take into account 2 additional occurrences of + // the entity title because we're rendering the full entity, not just the + // reference field. + $expected_occurrences = EntityReferenceEntityFormatter::RECURSIVE_RENDER_LIMIT * 2 + 2; + $actual_occurrences = substr_count($output, $referencing_entity_1->name->value); + $this->assertEqual($actual_occurrences, $expected_occurrences); + + // Repeat the process with another entity in order to check that the + // 'recursive_render_id' counter is generated properly. + $referencing_entity_2 = entity_create($this->entityType, ['name' => $this->randomMachineName()]); + $referencing_entity_2->save(); + $referencing_entity_2->{$this->fieldName}->entity = $referencing_entity_2; + $referencing_entity_2->save(); + + $build = $view_builder->view($referencing_entity_2, 'default'); + $output = $renderer->renderRoot($build); + + $actual_occurrences = substr_count($output, $referencing_entity_2->name->value); + $this->assertEqual($actual_occurrences, $expected_occurrences); + + // Now render both entities at the same time and check again. + $build = $view_builder->viewMultiple([$referencing_entity_1, $referencing_entity_2], 'default'); + $output = $renderer->renderRoot($build); + + $actual_occurrences = substr_count($output, $referencing_entity_1->name->value); + $this->assertEqual($actual_occurrences, $expected_occurrences); + + $actual_occurrences = substr_count($output, $referencing_entity_2->name->value); + $this->assertEqual($actual_occurrences, $expected_occurrences); + } + /** * Tests the label formatter. */