Issue #2073753 by amateescu, Sweetchuck, amitaibu, dawehner: Fix and add tests for the recursive rendering protection of the 'Rendered entity' formatter
parent
d3ef26be7e
commit
05e023d538
|
@ -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;
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
Loading…
Reference in New Issue