Verified Commit 187c122a authored by Dave Long's avatar Dave Long
Browse files

fix: #2940605 Can only intentionally re-render an entity with references 20 times

By: godotislate
By: ckaotik
By: kaythay
By: pfrenssen
By: ndobromirov
By: spokje
By: zorz
By: capysara
By: marcvangend
By: jonathanshaw
By: philltran
By: chewie
By: heddn
By: ameymudras
By: catch
By: rpayanm
By: smustgrave
By: kasey_mk
By: alexpott
By: kmonty
By: herved
By: ghost of drupal past
By: kiseleva.t
By: eduardo morales alberti
By: medha kumari
By: stefan.korn
By: dxvargas
By: taran2l
(cherry picked from commit 4d11c55d)
parent c01cec09
Loading
Loading
Loading
Loading
Loading
+89 −1
Original line number Diff line number Diff line
@@ -80,6 +80,13 @@ class EntityViewBuilder extends EntityHandlerBase implements EntityHandlerInterf
   */
  protected $singleFieldDisplays;

  /**
   * A collection of keys.
   *
   * It identifies rendering in progress, used to prevent recursion.
   */
  protected static array $recursionKeys = [];

  /**
   * Constructs a new EntityViewBuilder.
   *
@@ -136,7 +143,12 @@ public function view(EntityInterface $entity, $view_mode = 'full', $langcode = N
   * {@inheritdoc}
   */
  public static function trustedCallbacks() {
    return ['build', 'buildMultiple'];
    return [
      'build',
      'buildMultiple',
      'setRecursiveRenderProtection',
      'unsetRecursiveRenderProtection',
    ];
  }

  /**
@@ -190,6 +202,9 @@ 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)) {
@@ -537,4 +552,77 @@ 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 = $this->getRenderRecursionKey($build);
    if (isset(static::$recursionKeys[$recursion_key])) {
      trigger_error(sprintf('Recursive rendering attempt aborted for %s. In progress: %s', $recursion_key, print_r(static::$recursionKeys, TRUE)), E_USER_WARNING);
      $build['#printed'] = TRUE;
    }
    else {
      static::$recursionKeys[$recursion_key] = TRUE;
    }
    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 = $this->getRenderRecursionKey($build);
    unset(static::$recursionKeys[$recursion_key]);

    return $renderedEntity;
  }

  /**
   * Generates a key for an entity render array for recursion protection.
   *
   * @param array $build
   *   The entity render array.
   *
   * @return string
   *   The key to ID the build array within recursion tracking.
   */
  protected function getRenderRecursionKey(array $build): string {
    /** @var \Drupal\Core\Entity\EntityInterface $entity */
    $entity = $build['#' . $this->entityTypeId];

    $recursion_keys = [
      $entity->getEntityTypeId(),
    ];

    // If entity is new and has no ID, generate unique ID from entity object.
    // This is to prevent false positives, for example when previewing a new
    // node that is referencing a new node without either node yet being saved.
    if ($entity->id()) {
      $recursion_keys[] = 'entity_id';
      $recursion_keys[] = $entity->id();
      if ($entity instanceof RevisionableInterface) {
        $recursion_keys[] = $entity->getRevisionId();
      }
    }
    else {
      $recursion_keys[] = 'object_id';
      $recursion_keys[] = spl_object_id($entity);
    }

    if ($entity instanceof TranslatableDataInterface) {
      $recursion_keys[] = $entity->language()->getId();
    }

    $recursion_keys[] = $build['#view_mode'];

    // It seems very unlikely that the same entity displayed in the same view
    // mode would be recursively nested and meant to be displayed differently.
    return implode(':', $recursion_keys);
  }

}
+14 −35
Original line number Diff line number Diff line
@@ -29,6 +29,12 @@ class EntityReferenceEntityFormatter extends EntityReferenceFormatterBase {
   * The number of times this formatter allows rendering the same entity.
   *
   * @var int
   *
   * @deprecated in drupal:11.4.0 and is removed from drupal:13.0.0.
   * EntityViewBuilder #pre_render and #post_render callbacks prevent recursion.
   *
   * @see https://www.drupal.org/node/3316878
   * @see \Drupal\Core\Entity\EntityViewBuilder::getBuildDefaults()
   */
  const RECURSIVE_RENDER_LIMIT = 20;

@@ -56,10 +62,17 @@ class EntityReferenceEntityFormatter extends EntityReferenceFormatterBase {
  /**
   * An array of counters for the recursive rendering protection.
   *
   * @var array
   *
   * Each counter takes into account all the relevant information about the
   * field and the referenced entity that is being rendered.
   *
   * @var array
   * @deprecated in drupal:11.4.0 and is removed from drupal:13.0.0.
   *  EntityViewBuilder #pre_render and #post_render callbacks prevent
   *  recursion.
   *
   * @see https://www.drupal.org/node/3316878
   * @see \Drupal\Core\Entity\EntityViewBuilder::getBuildDefaults()
   *
   * @see \Drupal\Core\Field\Plugin\Field\FieldFormatter\EntityReferenceEntityFormatter::viewElements()
   */
@@ -160,40 +173,6 @@ public function viewElements(FieldItemListInterface $items, $langcode) {
    $elements = [];

    foreach ($this->getEntitiesToView($items, $langcode) as $delta => $entity) {
      // 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()
        // We include the referencing entity, so we can render default images
        // without hitting recursive protections.
        . $items->getEntity()->id()
        . $entity->getEntityTypeId()
        . $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 %parent_entity_type:%parent_bundle %parent_entity_id entity. Aborting rendering.', [
          '%entity_type' => $entity->getEntityTypeId(),
          '%entity_id' => $entity->id(),
          '%field_name' => $items->getName(),
          '%parent_entity_type' => $items->getFieldDefinition()->getTargetEntityTypeId(),
          '%parent_bundle' => $items->getFieldDefinition()->getTargetBundle(),
          '%parent_entity_id' => $items->getEntity()->id(),
        ]);
        return $elements;
      }

      $view_builder = $this->entityTypeManager->getViewBuilder($entity->getEntityTypeId());
      $elements[$delta] = $view_builder->view($entity, $view_mode, $entity->language()->getId());

+50 −0
Original line number Diff line number Diff line
@@ -306,4 +306,54 @@ public function testNoBundles(): void {
    $this->assertEquals($referenced_id, $referencing_node->$field_name->target_id, 'Newly created node is referenced from the referencing entity.');
  }

  /**
   * Test previewing autocreated node in a new node is not flagged as recursion.
   *
   * Entity render recursion is prevented by pushing/popping entities being
   * rendered on to a stack keyed by a string made up of the entity type ID,
   * entity ID, and the view mode. In the case of new entities, the entity ID is
   * NULL, so this test checks that unique identifiers are used in the stack
   * keys for new entities to prevent false positive recursion protection.
   */
  public function testAutoCreatedNodeNewNodePreview(): void {
    // Set the referenced node to be displayed as a rendered entity in the same
    // view mode as the referencing node.
    $display_repository = \Drupal::service('entity_display.repository');
    $display_repository->getViewDisplay('node', $this->referencingType)
      ->setComponent('test_field', [
        'type' => 'entity_reference_entity_view',
        'settings' => [
          'view_mode' => 'teaser',
        ],
      ])
      ->save();
    // Remove the links component from the referenced node's display because
    // there would be errors rendering links on the referenced entity without an
    // ID.
    $display_repository->getViewDisplay('node', $this->referencedType, 'teaser')
      ->removeComponent('links')
      ->save();

    // Assert referenced node does not exist.
    $referenced_title = $this->randomMachineName();
    $result = \Drupal::entityQuery('node')
      ->accessCheck(FALSE)
      ->condition('type', $this->referencedType)
      ->condition('title', $referenced_title)
      ->execute();
    $this->assertEmpty($result, 'Referenced node does not exist yet.');

    // Preview a new referencing node from the node add form.
    $edit = [
      'title[0][value]' => $this->randomMachineName(),
      'test_field[0][target_id]' => $referenced_title,
    ];
    $this->drupalGet("node/add/{$this->referencingType}");
    $this->submitForm($edit, 'Preview');
    $this->assertSession()->statusCodeEquals(200);
    // Referenced node title should appear if not blocked by recursion
    // protection.
    $this->assertSession()->pageTextContains($referenced_title);
  }

}
+165 −36
Original line number Diff line number Diff line
@@ -6,8 +6,9 @@

use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\Core\Entity\Entity\EntityViewMode;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Field\Plugin\Field\FieldFormatter\EntityReferenceEntityFormatter;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\entity_test\Entity\EntityTestLabel;
use Drupal\field\Entity\FieldConfig;
@@ -50,6 +51,11 @@ class EntityReferenceFormatterTest extends EntityKernelTestBase {
   */
  protected $fieldName = 'field_test';

  /**
   * The non cacheable view mode.
   */
  protected string $nonCacheableViewMode = 'no_cache';

  /**
   * The entity to be referenced in this test.
   *
@@ -236,21 +242,57 @@ public function testEntityFormatter(): void {
    $this->assertSame('default | ' . $this->unsavedReferencedEntity->label() . $expected_rendered_name_field_2 . $expected_rendered_body_field_2, (string) $build[1]['#markup'], sprintf('The markup returned by the %s formatter is correct for an item with a unsaved entity.', $formatter));
  }

  /**
   * Tests that iterative rendering is allowed by recursive render protection.
   */
  public function testEntityFormatterIterativeRendering(): void {
    /** @var \Drupal\Core\Render\RendererInterface $renderer */
    $renderer = $this->container->get('renderer');
    $view_builder = $this->entityTypeManager->getViewBuilder($this->entityType);

    $this->createNonCacheableViewMode();

    $storage = \Drupal::entityTypeManager()->getStorage($this->entityType);
    $entity = $storage->create(['name' => $this->randomMachineName()]);
    $entity->save();
    $referencing_entity = $storage->create([
      'name' => $this->randomMachineName(),
      $this->fieldName => $entity->id(),
    ]);
    $referencing_entity->save();

    // Set count to higher than the previous render protection limit of 20.
    $count = 21;
    // Render the same entity multiple times to check that iterative rendering
    // is allowed as long as the entity is not being recursively rendered.
    $build = $view_builder->viewMultiple(array_fill(0, $count, $referencing_entity), $this->nonCacheableViewMode);
    $output = (string) $renderer->renderRoot($build);
    // The title of entity_test entities is printed twice by default, so we have
    // to multiply our count by 2.
    $this->assertSame($count * 2, substr_count($output, $entity->label()));
  }

  /**
   * Tests the recursive rendering protection of the entity formatter.
   */
  public function testEntityFormatterRecursiveRendering(): void {
    // Intercept our specific warning and suppress it.
    set_error_handler(function (int $errno, string $errstr): bool {
      return $errno === E_USER_WARNING && str_starts_with($errstr, 'Recursive rendering attempt aborted');
    });

    /** @var \Drupal\Core\Render\RendererInterface $renderer */
    $renderer = $this->container->get('renderer');
    $formatter = 'entity_reference_entity_view';
    $view_builder = $this->entityTypeManager->getViewBuilder($this->entityType);

    $this->createNonCacheableViewMode();

    // Set the default view mode to use the 'entity_reference_entity_view'
    // formatter.
    \Drupal::service('entity_display.repository')
      ->getViewDisplay($this->entityType, $this->bundle)
      ->setComponent($this->fieldName, [
        'type' => $formatter,
        'type' => 'entity_reference_entity_view',
      ])
      ->save();

@@ -261,60 +303,82 @@ public function testEntityFormatterRecursiveRendering(): void {
    // Create a self-reference.
    $referencing_entity_1->{$this->fieldName}->entity = $referencing_entity_1;
    $referencing_entity_1->save();
    $referencing_entity_1_label = $referencing_entity_1->label();

    // Check that the recursive rendering stops after it reaches the specified
    // limit.
    $build = $view_builder->view($referencing_entity_1, 'default');
    // Using a different view mode is not recursion.
    $build = $view_builder->view($referencing_entity_1, 'teaser');
    $output = (string) $renderer->renderRoot($build);
    // 2 occurrences of the entity title per entity.
    $expected_occurrences = 4;

    // 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->label());
    $actual_occurrences = substr_count($output, $referencing_entity_1_label);
    $this->assertEquals($expected_occurrences, $actual_occurrences);

    // Repeat the process with another entity in order to check that the
    // 'recursive_render_id' counter is generated properly.
    // Self-references should not be rendered.
    // entity_1 -> entity_1
    $build = $view_builder->view($referencing_entity_1, $this->nonCacheableViewMode);
    $output = (string) $renderer->renderRoot($build);
    $expected_occurrences = 2;
    $actual_occurrences = substr_count($output, $referencing_entity_1_label);
    $this->assertEquals($expected_occurrences, $actual_occurrences);
    $build = $view_builder->view($referencing_entity_1, $this->nonCacheableViewMode);

    // Repetition is not wrongly detected as recursion.
    // entity_1 -> entity_1
    $output = (string) $renderer->renderRoot($build);
    $actual_occurrences = substr_count($output, $referencing_entity_1_label);
    $this->assertEquals($expected_occurrences, $actual_occurrences);

    // Referencing from another entity works fine.
    // entity_2 -> entity_1
    $referencing_entity_2 = $storage->create(['name' => $this->randomMachineName()]);
    $referencing_entity_2->save();
    $referencing_entity_2->{$this->fieldName}->entity = $referencing_entity_2;
    $referencing_entity_2_label = $referencing_entity_2->label();
    $referencing_entity_2->{$this->fieldName}->entity = $referencing_entity_1;
    $referencing_entity_2->save();

    $build = $view_builder->view($referencing_entity_2, 'default');
    $build = $view_builder->view($referencing_entity_2, $this->nonCacheableViewMode);
    $output = (string) $renderer->renderRoot($build);

    $actual_occurrences = substr_count($output, $referencing_entity_2->label());
    $actual_occurrences = substr_count($output, $referencing_entity_1_label);
    $this->assertEquals($expected_occurrences, $actual_occurrences);

    // Now render both entities at the same time and check again.
    $build = $view_builder->viewMultiple([$referencing_entity_1, $referencing_entity_2], 'default');
    // Referencing from multiple is fine.
    // entity_1 -> entity_1
    // entity_2 -> entity_1
    $build = $view_builder->viewMultiple([$referencing_entity_1, $referencing_entity_2], $this->nonCacheableViewMode);
    $output = (string) $renderer->renderRoot($build);

    $actual_occurrences = substr_count($output, $referencing_entity_1->label());
    // entity_1 should be seen once as a parent and once as a child of entity_2.
    $expected_occurrences = 4;
    $actual_occurrences = substr_count($output, $referencing_entity_1_label);
    $this->assertEquals($expected_occurrences, $actual_occurrences);

    $actual_occurrences = substr_count($output, $referencing_entity_2->label());
    // entity_2 is seen only once, as a parent.
    $expected_occurrences = 2;
    $actual_occurrences = substr_count($output, $referencing_entity_2_label);
    $this->assertEquals($expected_occurrences, $actual_occurrences);
    // Indirect recursion is not ok.
    // entity_2 -> entity_1 -> entity_2
    $referencing_entity_1->{$this->fieldName}->entity = $referencing_entity_2;
    $referencing_entity_1->save();
    $build = $view_builder->view($referencing_entity_2, $this->nonCacheableViewMode);
    $output = (string) $renderer->renderRoot($build);
    // Each entity should be seen once.
    $expected_occurrences = 2;
    $actual_occurrences = substr_count($output, $referencing_entity_1_label);
    $this->assertEquals($expected_occurrences, $actual_occurrences);
    $expected_occurrences = 2;
    $actual_occurrences = substr_count($output, $referencing_entity_2_label);
    $this->assertEquals($expected_occurrences, $actual_occurrences);

    restore_error_handler();
  }

  /**
   * Renders the same entity referenced from different places.
   */
  public function testEntityReferenceRecursiveProtectionWithManyRenderedEntities(): void {
    $formatter = 'entity_reference_entity_view';
    $view_builder = $this->entityTypeManager->getViewBuilder($this->entityType);

    // Set the default view mode to use the 'entity_reference_entity_view'
    // formatter.
    \Drupal::service('entity_display.repository')
      ->getViewDisplay($this->entityType, $this->bundle)
      ->setComponent($this->fieldName, [
        'type' => $formatter,
      ])
      ->save();
    $this->createNonCacheableViewMode();

    $storage = $this->entityTypeManager->getStorage($this->entityType);
    /** @var \Drupal\Core\Entity\ContentEntityInterface $referenced_entity */
@@ -330,7 +394,7 @@ public function testEntityReferenceRecursiveProtectionWithManyRenderedEntities()
      return $referencing_entity;
    }, $range);

    $build = $view_builder->viewMultiple($referencing_entities, 'default');
    $build = $view_builder->viewMultiple($referencing_entities, $this->nonCacheableViewMode);
    $output = $this->render($build);

    // The title of entity_test entities is printed twice by default, so we have
@@ -339,10 +403,46 @@ public function testEntityReferenceRecursiveProtectionWithManyRenderedEntities()
    // the entity title because we're rendering the full entity, not just the
    // reference field.
    $expected_occurrences = 30 * 2 + 2;
    $actual_occurrences = substr_count($output, $referenced_entity->get('name')->value);
    $actual_occurrences = substr_count($output, $referenced_entity->label());
    $this->assertEquals($expected_occurrences, $actual_occurrences);
  }

  /**
   * Tests multiple renderings of an entity that references another.
   */
  public function testEntityReferenceRecursionProtectionWithRepeatedReferencingEntity(): void {
    /** @var \Drupal\Core\Render\RendererInterface $renderer */
    $renderer = $this->container->get('renderer');
    $view_builder = $this->entityTypeManager->getViewBuilder($this->entityType);

    $this->createNonCacheableViewMode();

    $storage = \Drupal::entityTypeManager()->getStorage($this->entityType);
    $entity = $storage->create(['name' => $this->randomMachineName()]);
    $entity->save();
    $referencing_entity = $storage->create([
      'name' => $this->randomMachineName(),
      $this->fieldName => $entity->id(),
    ]);
    $referencing_entity->save();

    // Large-scale repetition within a single render root is not recursion.
    $count = 30;
    $build = $view_builder->viewMultiple(array_fill(0, $count, $referencing_entity), $this->nonCacheableViewMode);
    $output = (string) $renderer->renderRoot($build);
    // The title of entity_test entities is printed twice by default, so we have
    // to multiply our count by 2.
    $this->assertSame($count * 2, substr_count($output, $entity->label()));

    // Large-scale repetition across render roots is not recursion.
    for ($i = 0; $i < $count; $i++) {
      $build = $view_builder->view($referencing_entity, $this->nonCacheableViewMode);
      $output = (string) $renderer->renderRoot($build);
      // The title of entity_test entities is printed twice by default.
      $this->assertSame(2, substr_count($output, $entity->label()));
    }
  }

  /**
   * Tests the label formatter.
   */
@@ -498,4 +598,33 @@ protected function buildRenderArray(array $referenced_entities, $formatter, $for
    return $items->view(['type' => $formatter, 'settings' => $formatter_options]);
  }

  /**
   * Creates a non cacheable view mode.
   */
  protected function createNonCacheableViewMode(): void {
    EntityViewMode::create([
      'id' => $this->entityType . '.' . $this->nonCacheableViewMode,
      'label' => 'No cache view mode',
      'targetEntityType' => $this->entityType,
      'cache' => FALSE,
    ])
      ->save();

    // Set the new view mode to use the 'entity_reference_entity_view'
    // formatter displaying entities in non cacheable view mode as well.
    EntityViewDisplay::create([
      'targetEntityType' => $this->entityType,
      'bundle' => $this->bundle,
      'mode' => $this->nonCacheableViewMode,
      'status' => TRUE,
    ])
      ->setComponent($this->fieldName, [
        'type' => 'entity_reference_entity_view',
        'settings' => [
          'view_mode' => $this->nonCacheableViewMode,
        ],
      ])
      ->save();
  }

}
+0 −32
Original line number Diff line number Diff line
@@ -8,7 +8,6 @@
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\EntityViewModeInterface;
use Drupal\Core\Field\Plugin\Field\FieldFormatter\EntityReferenceEntityFormatter;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
@@ -88,18 +87,6 @@ class MediaEmbed extends FilterBase implements ContainerFactoryPluginInterface,
   */
  protected $loggerFactory;

  /**
   * 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.
   *
   * @var array
   *
   * @see \Drupal\Core\Field\Plugin\Field\FieldFormatter\EntityReferenceEntityFormatter::$recursiveRenderDepth
   */
  protected static $recursiveRenderDepth = [];

  /**
   * Constructs a MediaEmbed object.
   *
@@ -212,25 +199,6 @@ public static function validateOptions(array &$element, FormStateInterface $form
   *   A render array.
   */
  protected function renderMedia(MediaInterface $media, $view_mode, $langcode) {
    // Due to render caching and delayed calls, filtering happens later
    // in the rendering process through a '#pre_render' callback, so we
    // need to generate a counter for the media entity that is being embedded.
    // @see \Drupal\filter\Element\ProcessedText::preRenderText()
    $recursive_render_id = $media->uuid();
    if (isset(static::$recursiveRenderDepth[$recursive_render_id])) {
      static::$recursiveRenderDepth[$recursive_render_id]++;
    }
    else {
      static::$recursiveRenderDepth[$recursive_render_id] = 1;
    }
    // Protect ourselves from recursive rendering: return an empty render array.
    if (static::$recursiveRenderDepth[$recursive_render_id] > EntityReferenceEntityFormatter::RECURSIVE_RENDER_LIMIT) {
      $this->loggerFactory->get('media')->error('During rendering of embedded media: recursive rendering detected for %entity_id. Aborting rendering.', [
        '%entity_id' => $media->id(),
      ]);
      return [];
    }

    $build = $this->entityTypeManager
      ->getViewBuilder('media')
      ->view($media, $view_mode, $langcode);
Loading