You are here

class OEmbedIframeController in Drupal 10

Same name and namespace in other branches
  1. 8 core/modules/media/src/Controller/OEmbedIframeController.php \Drupal\media\Controller\OEmbedIframeController
  2. 9 core/modules/media/src/Controller/OEmbedIframeController.php \Drupal\media\Controller\OEmbedIframeController

Controller which renders an oEmbed resource in a bare page (without blocks).

This controller is meant to render untrusted third-party HTML returned by an oEmbed provider in an iframe, so as to mitigate the potential dangers of of displaying third-party markup (i.e., XSS). The HTML returned by this controller should not be trusted, and should *never* be displayed outside of an iframe.

@internal This is an internal part of the media system in Drupal core and may be subject to change in minor releases. This class should not be instantiated or extended by external code.

Hierarchy

Expanded class hierarchy of OEmbedIframeController

1 file declares its use of OEmbedIframeController
OEmbedIframeControllerTest.php in core/modules/media/tests/src/Kernel/OEmbedIframeControllerTest.php

File

core/modules/media/src/Controller/OEmbedIframeController.php, line 37

Namespace

Drupal\media\Controller
View source
class OEmbedIframeController implements ContainerInjectionInterface {

  /**
   * The oEmbed resource fetcher service.
   *
   * @var \Drupal\media\OEmbed\ResourceFetcherInterface
   */
  protected $resourceFetcher;

  /**
   * The oEmbed URL resolver service.
   *
   * @var \Drupal\media\OEmbed\UrlResolverInterface
   */
  protected $urlResolver;

  /**
   * The renderer service.
   *
   * @var \Drupal\Core\Render\RendererInterface
   */
  protected $renderer;

  /**
   * The logger channel.
   *
   * @var \Psr\Log\LoggerInterface
   */
  protected $logger;

  /**
   * The iFrame URL helper service.
   *
   * @var \Drupal\media\IFrameUrlHelper
   */
  protected $iFrameUrlHelper;

  /**
   * Constructs an OEmbedIframeController instance.
   *
   * @param \Drupal\media\OEmbed\ResourceFetcherInterface $resource_fetcher
   *   The oEmbed resource fetcher service.
   * @param \Drupal\media\OEmbed\UrlResolverInterface $url_resolver
   *   The oEmbed URL resolver service.
   * @param \Drupal\Core\Render\RendererInterface $renderer
   *   The renderer service.
   * @param \Psr\Log\LoggerInterface $logger
   *   The logger channel.
   * @param \Drupal\media\IFrameUrlHelper $iframe_url_helper
   *   The iFrame URL helper service.
   */
  public function __construct(ResourceFetcherInterface $resource_fetcher, UrlResolverInterface $url_resolver, RendererInterface $renderer, LoggerInterface $logger, IFrameUrlHelper $iframe_url_helper) {
    $this->resourceFetcher = $resource_fetcher;
    $this->urlResolver = $url_resolver;
    $this->renderer = $renderer;
    $this->logger = $logger;
    $this->iFrameUrlHelper = $iframe_url_helper;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static($container
      ->get('media.oembed.resource_fetcher'), $container
      ->get('media.oembed.url_resolver'), $container
      ->get('renderer'), $container
      ->get('logger.factory')
      ->get('media'), $container
      ->get('media.oembed.iframe_url_helper'));
  }

  /**
   * Renders an oEmbed resource.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The request object.
   *
   * @return \Symfony\Component\HttpFoundation\Response
   *   The response object.
   *
   * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
   *   Will be thrown if either
   *   - the 'hash' parameter does not match the expected hash of the 'url'
   *     parameter;
   *   - the iframe_domain is set in media.settings and does not match the host
   *     in the request.
   */
  public function render(Request $request) {

    // @todo Move domain check logic to a separate method.
    $allowed_domain = \Drupal::config('media.settings')
      ->get('iframe_domain');
    if ($allowed_domain) {
      $allowed_host = parse_url($allowed_domain, PHP_URL_HOST);
      $host = parse_url($request
        ->getSchemeAndHttpHost(), PHP_URL_HOST);
      if ($allowed_host !== $host) {
        throw new AccessDeniedHttpException('This resource is not available');
      }
    }
    $url = $request->query
      ->get('url');
    $max_width = $request->query
      ->getInt('max_width');
    $max_height = $request->query
      ->getInt('max_height');

    // Hash the URL and max dimensions, and ensure it is equal to the hash
    // parameter passed in the query string.
    $hash = $this->iFrameUrlHelper
      ->getHash($url, $max_width, $max_height);
    if (!hash_equals($hash, $request->query
      ->get('hash', ''))) {
      throw new AccessDeniedHttpException('This resource is not available');
    }

    // Return a response instead of a render array so that the frame content
    // will not have all the blocks and page elements normally rendered by
    // Drupal.
    $response = new HtmlResponse('', HtmlResponse::HTTP_OK, [
      'Content-Type' => 'text/html; charset=UTF-8',
    ]);
    $response
      ->addCacheableDependency(Url::createFromRequest($request));
    try {
      $resource_url = $this->urlResolver
        ->getResourceUrl($url, $max_width, $max_height);
      $resource = $this->resourceFetcher
        ->fetchResource($resource_url);
      $placeholder_token = Crypt::randomBytesBase64(55);

      // Render the content in a new render context so that the cacheability
      // metadata of the rendered HTML will be captured correctly.
      $element = [
        '#theme' => 'media_oembed_iframe',
        '#resource' => $resource,
        // Even though the resource HTML is untrusted, IFrameMarkup::create()
        // will create a trusted string. The only reason this is okay is
        // because we are serving it in an iframe, which will mitigate the
        // potential dangers of displaying third-party markup.
        '#media' => IFrameMarkup::create($resource
          ->getHtml()),
        '#cache' => [
          // Add the 'rendered' cache tag as this response is not processed by
          // \Drupal\Core\Render\MainContent\HtmlRenderer::renderResponse().
          'tags' => [
            'rendered',
          ],
        ],
        '#attached' => [
          'html_response_attachment_placeholders' => [
            'styles' => '<css-placeholder token="' . $placeholder_token . '">',
          ],
          'library' => [
            'media/oembed.frame',
          ],
        ],
        '#placeholder_token' => $placeholder_token,
      ];
      $context = new RenderContext();
      $content = $this->renderer
        ->executeInRenderContext($context, function () use ($element) {
        return $this->renderer
          ->render($element);
      });
      $response
        ->setContent($content)
        ->setAttachments($element['#attached'])
        ->addCacheableDependency($resource)
        ->addCacheableDependency(CacheableMetadata::createFromRenderArray($element));

      // Modules and themes implementing hook_media_oembed_iframe_preprocess()
      // can add additional #cache and #attachments to a render array. If this
      // occurs, the render context won't be empty, and we need to ensure the
      // added metadata is bubbled up to the response.
      // @see \Drupal\Core\Theme\ThemeManager::render()
      if (!$context
        ->isEmpty()) {
        $bubbleable_metadata = $context
          ->pop();
        assert($bubbleable_metadata instanceof BubbleableMetadata);
        $response
          ->addCacheableDependency($bubbleable_metadata);
        $response
          ->addAttachments($bubbleable_metadata
          ->getAttachments());
      }
    } catch (ResourceException $e) {

      // Prevent the response from being cached.
      $response
        ->setMaxAge(0);

      // The oEmbed system makes heavy use of exception wrapping, so log the
      // entire exception chain to help with troubleshooting.
      do {

        // @todo Log additional information from ResourceException, to help with
        // debugging, in https://www.drupal.org/project/drupal/issues/2972846.
        $this->logger
          ->error($e
          ->getMessage());
        $e = $e
          ->getPrevious();
      } while ($e);
    }
    return $response;
  }

}

Members

Namesort descending Modifiers Type Description Overrides
OEmbedIframeController::$iFrameUrlHelper protected property The iFrame URL helper service.
OEmbedIframeController::$logger protected property The logger channel.
OEmbedIframeController::$renderer protected property The renderer service.
OEmbedIframeController::$resourceFetcher protected property The oEmbed resource fetcher service.
OEmbedIframeController::$urlResolver protected property The oEmbed URL resolver service.
OEmbedIframeController::create public static function Instantiates a new instance of this class. Overrides ContainerInjectionInterface::create
OEmbedIframeController::render public function Renders an oEmbed resource.
OEmbedIframeController::__construct public function Constructs an OEmbedIframeController instance.