.../modules/media/src/Plugin/Filter/MediaEmbed.php | 36 ++++++++++++++++++++-- .../tests/src/Kernel/MediaEmbedFilterTest.php | 22 +++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/core/modules/media/src/Plugin/Filter/MediaEmbed.php b/core/modules/media/src/Plugin/Filter/MediaEmbed.php index db8cff0579..bca5f70483 100644 --- a/core/modules/media/src/Plugin/Filter/MediaEmbed.php +++ b/core/modules/media/src/Plugin/Filter/MediaEmbed.php @@ -5,6 +5,7 @@ use Drupal\Component\Utility\Html; use Drupal\Core\Entity\EntityRepositoryInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Field\Plugin\Field\FieldFormatter\EntityReferenceEntityFormatter; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Render\BubbleableMetadata; use Drupal\Core\Render\RenderContext; @@ -51,6 +52,18 @@ class MediaEmbed extends FilterBase implements ContainerFactoryPluginInterface { */ protected $renderer; + /** + * 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. * @@ -102,6 +115,27 @@ public static function create(ContainerInterface $container, array $configuratio * 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 that takes into account all the + // relevant information about this field and the referenced entity + // that is being rendered. + // @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) { + \Drupal::logger('media')->error('Recursive rendering detected when rendering embedded media: %entity_id. Aborting rendering.', [ + '%entity_id' => $media->id(), + ]); + return []; + } + $build = $this->entityTypeManager ->getViewBuilder('media') ->view($media, $view_mode, $langcode); @@ -181,8 +215,6 @@ public function process($text, $langcode) { \Drupal::logger('media')->error('The view mode "@view-mode-id" does not exist.', ['@view-mode-id' => $view_mode_id]); } - // @todo recursive embedding protection - $build = $media && $view_mode ? $this->renderMedia($media, $view_mode_id, $langcode) : $this->renderMissingMedia(); diff --git a/core/modules/media/tests/src/Kernel/MediaEmbedFilterTest.php b/core/modules/media/tests/src/Kernel/MediaEmbedFilterTest.php index a48188b5fd..9d2ee79b65 100644 --- a/core/modules/media/tests/src/Kernel/MediaEmbedFilterTest.php +++ b/core/modules/media/tests/src/Kernel/MediaEmbedFilterTest.php @@ -290,6 +290,28 @@ public function testOnlyDrupalMediaTagProcessed() { $this->assertSame($content, $filter_result->getProcessedText()); } + /** + * Tests recursive rendering protection. + */ + public function testRecursionProtection() { + $text = $this->createEmbedCode([ + 'data-entity-type' => 'media', + 'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID, + 'data-view-mode' => 'full', + ]); + + // Render and verify the presence of the embedded entity 20 times. + for ($i = 0; $i < 20; $i++) { + $this->applyFilter($text); + $this->assertCount(1, $this->cssSelect('div[data-media-embed-test-view-mode="full"]')); + } + + // Render a 21st time, this is exceeding the recursion limit. The entity + // embed markup will be stripped. + $this->applyFilter($text); + $this->assertEmpty($this->getRawContent()); + } + /** * @covers \Drupal\filter\Plugin\Filter\FilterAlign * @covers \Drupal\filter\Plugin\Filter\FilterCaption