OEmbedIframeController.php

Same filename in other branches
  1. 9 core/modules/media/src/Controller/OEmbedIframeController.php
  2. 8.9.x core/modules/media/src/Controller/OEmbedIframeController.php
  3. 10 core/modules/media/src/Controller/OEmbedIframeController.php

Namespace

Drupal\media\Controller

File

core/modules/media/src/Controller/OEmbedIframeController.php

View source
<?php

namespace Drupal\media\Controller;

use Drupal\Component\Utility\Crypt;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\Render\HtmlResponse;
use Drupal\Core\Render\RenderContext;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Url;
use Drupal\media\IFrameMarkup;
use Drupal\media\IFrameUrlHelper;
use Drupal\media\OEmbed\ResourceException;
use Drupal\media\OEmbed\ResourceFetcherInterface;
use Drupal\media\OEmbed\UrlResolverInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;

/**
 * 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.
 */
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\BadRequestHttpException
     *   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 BadRequestHttpException('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 BadRequestHttpException('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;
    }

}

Classes

Title Deprecated Summary
OEmbedIframeController Controller which renders an oEmbed resource in a bare page (without blocks).

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.