3 namespace Drupal\media\Controller;
5 use Drupal\Component\Utility\Crypt;
6 use Drupal\Core\Cache\CacheableMetadata;
7 use Drupal\Core\Cache\CacheableResponse;
8 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
9 use Drupal\Core\Render\RenderContext;
10 use Drupal\Core\Render\RendererInterface;
12 use Drupal\media\IFrameMarkup;
13 use Drupal\media\IFrameUrlHelper;
14 use Drupal\media\OEmbed\ResourceException;
15 use Drupal\media\OEmbed\ResourceFetcherInterface;
16 use Drupal\media\OEmbed\UrlResolverInterface;
17 use Psr\Log\LoggerInterface;
18 use Symfony\Component\DependencyInjection\ContainerInterface;
19 use Symfony\Component\HttpFoundation\Request;
20 use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
23 * Controller which renders an oEmbed resource in a bare page (without blocks).
25 * This controller is meant to render untrusted third-party HTML returned by
26 * an oEmbed provider in an iframe, so as to mitigate the potential dangers of
27 * of displaying third-party markup (i.e., XSS). The HTML returned by this
28 * controller should not be trusted, and should *never* be displayed outside
32 * This is an internal part of the oEmbed system and should only be used by
33 * oEmbed-related code in Drupal core.
35 class OEmbedIframeController implements ContainerInjectionInterface {
38 * The oEmbed resource fetcher service.
40 * @var \Drupal\media\OEmbed\ResourceFetcherInterface
42 protected $resourceFetcher;
45 * The oEmbed URL resolver service.
47 * @var \Drupal\media\OEmbed\UrlResolverInterface
49 protected $urlResolver;
52 * The renderer service.
54 * @var \Drupal\Core\Render\RendererInterface
61 * @var \Psr\Log\LoggerInterface
66 * The iFrame URL helper service.
68 * @var \Drupal\media\IFrameUrlHelper
70 protected $iFrameUrlHelper;
73 * Constructs an OEmbedIframeController instance.
75 * @param \Drupal\media\OEmbed\ResourceFetcherInterface $resource_fetcher
76 * The oEmbed resource fetcher service.
77 * @param \Drupal\media\OEmbed\UrlResolverInterface $url_resolver
78 * The oEmbed URL resolver service.
79 * @param \Drupal\Core\Render\RendererInterface $renderer
80 * The renderer service.
81 * @param \Psr\Log\LoggerInterface $logger
83 * @param \Drupal\media\IFrameUrlHelper $iframe_url_helper
84 * The iFrame URL helper service.
86 public function __construct(ResourceFetcherInterface $resource_fetcher, UrlResolverInterface $url_resolver, RendererInterface $renderer, LoggerInterface $logger, IFrameUrlHelper $iframe_url_helper) {
87 $this->resourceFetcher = $resource_fetcher;
88 $this->urlResolver = $url_resolver;
89 $this->renderer = $renderer;
90 $this->logger = $logger;
91 $this->iFrameUrlHelper = $iframe_url_helper;
97 public static function create(ContainerInterface $container) {
99 $container->get('media.oembed.resource_fetcher'),
100 $container->get('media.oembed.url_resolver'),
101 $container->get('renderer'),
102 $container->get('logger.factory')->get('media'),
103 $container->get('media.oembed.iframe_url_helper')
108 * Renders an oEmbed resource.
110 * @param \Symfony\Component\HttpFoundation\Request $request
111 * The request object.
113 * @return \Symfony\Component\HttpFoundation\Response
114 * The response object.
116 * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
117 * Will be thrown if the 'hash' parameter does not match the expected hash
118 * of the 'url' parameter.
120 public function render(Request $request) {
121 $url = $request->query->get('url');
122 $max_width = $request->query->getInt('max_width', NULL);
123 $max_height = $request->query->getInt('max_height', NULL);
125 // Hash the URL and max dimensions, and ensure it is equal to the hash
126 // parameter passed in the query string.
127 $hash = $this->iFrameUrlHelper->getHash($url, $max_width, $max_height);
128 if (!Crypt::hashEquals($hash, $request->query->get('hash', ''))) {
129 throw new AccessDeniedHttpException('This resource is not available');
132 // Return a response instead of a render array so that the frame content
133 // will not have all the blocks and page elements normally rendered by
135 $response = new CacheableResponse();
136 $response->addCacheableDependency(Url::createFromRequest($request));
139 $resource_url = $this->urlResolver->getResourceUrl($url, $max_width, $max_height);
140 $resource = $this->resourceFetcher->fetchResource($resource_url);
142 // Render the content in a new render context so that the cacheability
143 // metadata of the rendered HTML will be captured correctly.
145 '#theme' => 'media_oembed_iframe',
146 // Even though the resource HTML is untrusted, IFrameMarkup::create()
147 // will create a trusted string. The only reason this is okay is
148 // because we are serving it in an iframe, which will mitigate the
149 // potential dangers of displaying third-party markup.
150 '#media' => IFrameMarkup::create($resource->getHtml()),
152 // Add the 'rendered' cache tag as this response is not processed by
153 // \Drupal\Core\Render\MainContent\HtmlRenderer::renderResponse().
154 'tags' => ['rendered'],
157 $content = $this->renderer->executeInRenderContext(new RenderContext(), function () use ($resource, $element) {
158 return $this->renderer->render($element);
161 ->setContent($content)
162 ->addCacheableDependency($resource)
163 ->addCacheableDependency(CacheableMetadata::createFromRenderArray($element));
165 catch (ResourceException $e) {
166 // Prevent the response from being cached.
167 $response->setMaxAge(0);
169 // The oEmbed system makes heavy use of exception wrapping, so log the
170 // entire exception chain to help with troubleshooting.
172 // @todo Log additional information from ResourceException, to help with
173 // debugging, in https://www.drupal.org/project/drupal/issues/2972846.
174 $this->logger->error($e->getMessage());
175 $e = $e->getPrevious();