Chris@17
|
1 <?php
|
Chris@17
|
2
|
Chris@17
|
3 namespace Drupal\media\Controller;
|
Chris@17
|
4
|
Chris@17
|
5 use Drupal\Component\Utility\Crypt;
|
Chris@17
|
6 use Drupal\Core\Cache\CacheableMetadata;
|
Chris@17
|
7 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
Chris@18
|
8 use Drupal\Core\Render\HtmlResponse;
|
Chris@17
|
9 use Drupal\Core\Render\RenderContext;
|
Chris@17
|
10 use Drupal\Core\Render\RendererInterface;
|
Chris@17
|
11 use Drupal\Core\Url;
|
Chris@17
|
12 use Drupal\media\IFrameMarkup;
|
Chris@17
|
13 use Drupal\media\IFrameUrlHelper;
|
Chris@17
|
14 use Drupal\media\OEmbed\ResourceException;
|
Chris@17
|
15 use Drupal\media\OEmbed\ResourceFetcherInterface;
|
Chris@17
|
16 use Drupal\media\OEmbed\UrlResolverInterface;
|
Chris@17
|
17 use Psr\Log\LoggerInterface;
|
Chris@17
|
18 use Symfony\Component\DependencyInjection\ContainerInterface;
|
Chris@17
|
19 use Symfony\Component\HttpFoundation\Request;
|
Chris@17
|
20 use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
Chris@17
|
21
|
Chris@17
|
22 /**
|
Chris@17
|
23 * Controller which renders an oEmbed resource in a bare page (without blocks).
|
Chris@17
|
24 *
|
Chris@17
|
25 * This controller is meant to render untrusted third-party HTML returned by
|
Chris@17
|
26 * an oEmbed provider in an iframe, so as to mitigate the potential dangers of
|
Chris@17
|
27 * of displaying third-party markup (i.e., XSS). The HTML returned by this
|
Chris@17
|
28 * controller should not be trusted, and should *never* be displayed outside
|
Chris@17
|
29 * of an iframe.
|
Chris@17
|
30 *
|
Chris@17
|
31 * @internal
|
Chris@17
|
32 * This is an internal part of the oEmbed system and should only be used by
|
Chris@17
|
33 * oEmbed-related code in Drupal core.
|
Chris@17
|
34 */
|
Chris@17
|
35 class OEmbedIframeController implements ContainerInjectionInterface {
|
Chris@17
|
36
|
Chris@17
|
37 /**
|
Chris@17
|
38 * The oEmbed resource fetcher service.
|
Chris@17
|
39 *
|
Chris@17
|
40 * @var \Drupal\media\OEmbed\ResourceFetcherInterface
|
Chris@17
|
41 */
|
Chris@17
|
42 protected $resourceFetcher;
|
Chris@17
|
43
|
Chris@17
|
44 /**
|
Chris@17
|
45 * The oEmbed URL resolver service.
|
Chris@17
|
46 *
|
Chris@17
|
47 * @var \Drupal\media\OEmbed\UrlResolverInterface
|
Chris@17
|
48 */
|
Chris@17
|
49 protected $urlResolver;
|
Chris@17
|
50
|
Chris@17
|
51 /**
|
Chris@17
|
52 * The renderer service.
|
Chris@17
|
53 *
|
Chris@17
|
54 * @var \Drupal\Core\Render\RendererInterface
|
Chris@17
|
55 */
|
Chris@17
|
56 protected $renderer;
|
Chris@17
|
57
|
Chris@17
|
58 /**
|
Chris@17
|
59 * The logger channel.
|
Chris@17
|
60 *
|
Chris@17
|
61 * @var \Psr\Log\LoggerInterface
|
Chris@17
|
62 */
|
Chris@17
|
63 protected $logger;
|
Chris@17
|
64
|
Chris@17
|
65 /**
|
Chris@17
|
66 * The iFrame URL helper service.
|
Chris@17
|
67 *
|
Chris@17
|
68 * @var \Drupal\media\IFrameUrlHelper
|
Chris@17
|
69 */
|
Chris@17
|
70 protected $iFrameUrlHelper;
|
Chris@17
|
71
|
Chris@17
|
72 /**
|
Chris@17
|
73 * Constructs an OEmbedIframeController instance.
|
Chris@17
|
74 *
|
Chris@17
|
75 * @param \Drupal\media\OEmbed\ResourceFetcherInterface $resource_fetcher
|
Chris@17
|
76 * The oEmbed resource fetcher service.
|
Chris@17
|
77 * @param \Drupal\media\OEmbed\UrlResolverInterface $url_resolver
|
Chris@17
|
78 * The oEmbed URL resolver service.
|
Chris@17
|
79 * @param \Drupal\Core\Render\RendererInterface $renderer
|
Chris@17
|
80 * The renderer service.
|
Chris@17
|
81 * @param \Psr\Log\LoggerInterface $logger
|
Chris@17
|
82 * The logger channel.
|
Chris@17
|
83 * @param \Drupal\media\IFrameUrlHelper $iframe_url_helper
|
Chris@17
|
84 * The iFrame URL helper service.
|
Chris@17
|
85 */
|
Chris@17
|
86 public function __construct(ResourceFetcherInterface $resource_fetcher, UrlResolverInterface $url_resolver, RendererInterface $renderer, LoggerInterface $logger, IFrameUrlHelper $iframe_url_helper) {
|
Chris@17
|
87 $this->resourceFetcher = $resource_fetcher;
|
Chris@17
|
88 $this->urlResolver = $url_resolver;
|
Chris@17
|
89 $this->renderer = $renderer;
|
Chris@17
|
90 $this->logger = $logger;
|
Chris@17
|
91 $this->iFrameUrlHelper = $iframe_url_helper;
|
Chris@17
|
92 }
|
Chris@17
|
93
|
Chris@17
|
94 /**
|
Chris@17
|
95 * {@inheritdoc}
|
Chris@17
|
96 */
|
Chris@17
|
97 public static function create(ContainerInterface $container) {
|
Chris@17
|
98 return new static(
|
Chris@17
|
99 $container->get('media.oembed.resource_fetcher'),
|
Chris@17
|
100 $container->get('media.oembed.url_resolver'),
|
Chris@17
|
101 $container->get('renderer'),
|
Chris@17
|
102 $container->get('logger.factory')->get('media'),
|
Chris@17
|
103 $container->get('media.oembed.iframe_url_helper')
|
Chris@17
|
104 );
|
Chris@17
|
105 }
|
Chris@17
|
106
|
Chris@17
|
107 /**
|
Chris@17
|
108 * Renders an oEmbed resource.
|
Chris@17
|
109 *
|
Chris@17
|
110 * @param \Symfony\Component\HttpFoundation\Request $request
|
Chris@17
|
111 * The request object.
|
Chris@17
|
112 *
|
Chris@17
|
113 * @return \Symfony\Component\HttpFoundation\Response
|
Chris@17
|
114 * The response object.
|
Chris@17
|
115 *
|
Chris@17
|
116 * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
|
Chris@17
|
117 * Will be thrown if the 'hash' parameter does not match the expected hash
|
Chris@17
|
118 * of the 'url' parameter.
|
Chris@17
|
119 */
|
Chris@17
|
120 public function render(Request $request) {
|
Chris@17
|
121 $url = $request->query->get('url');
|
Chris@17
|
122 $max_width = $request->query->getInt('max_width', NULL);
|
Chris@17
|
123 $max_height = $request->query->getInt('max_height', NULL);
|
Chris@17
|
124
|
Chris@17
|
125 // Hash the URL and max dimensions, and ensure it is equal to the hash
|
Chris@17
|
126 // parameter passed in the query string.
|
Chris@17
|
127 $hash = $this->iFrameUrlHelper->getHash($url, $max_width, $max_height);
|
Chris@17
|
128 if (!Crypt::hashEquals($hash, $request->query->get('hash', ''))) {
|
Chris@17
|
129 throw new AccessDeniedHttpException('This resource is not available');
|
Chris@17
|
130 }
|
Chris@17
|
131
|
Chris@17
|
132 // Return a response instead of a render array so that the frame content
|
Chris@17
|
133 // will not have all the blocks and page elements normally rendered by
|
Chris@17
|
134 // Drupal.
|
Chris@18
|
135 $response = new HtmlResponse();
|
Chris@17
|
136 $response->addCacheableDependency(Url::createFromRequest($request));
|
Chris@17
|
137
|
Chris@17
|
138 try {
|
Chris@17
|
139 $resource_url = $this->urlResolver->getResourceUrl($url, $max_width, $max_height);
|
Chris@17
|
140 $resource = $this->resourceFetcher->fetchResource($resource_url);
|
Chris@17
|
141
|
Chris@18
|
142 $placeholder_token = Crypt::randomBytesBase64(55);
|
Chris@18
|
143
|
Chris@17
|
144 // Render the content in a new render context so that the cacheability
|
Chris@17
|
145 // metadata of the rendered HTML will be captured correctly.
|
Chris@17
|
146 $element = [
|
Chris@17
|
147 '#theme' => 'media_oembed_iframe',
|
Chris@17
|
148 // Even though the resource HTML is untrusted, IFrameMarkup::create()
|
Chris@17
|
149 // will create a trusted string. The only reason this is okay is
|
Chris@17
|
150 // because we are serving it in an iframe, which will mitigate the
|
Chris@17
|
151 // potential dangers of displaying third-party markup.
|
Chris@17
|
152 '#media' => IFrameMarkup::create($resource->getHtml()),
|
Chris@17
|
153 '#cache' => [
|
Chris@17
|
154 // Add the 'rendered' cache tag as this response is not processed by
|
Chris@17
|
155 // \Drupal\Core\Render\MainContent\HtmlRenderer::renderResponse().
|
Chris@17
|
156 'tags' => ['rendered'],
|
Chris@17
|
157 ],
|
Chris@18
|
158 '#attached' => [
|
Chris@18
|
159 'html_response_attachment_placeholders' => [
|
Chris@18
|
160 'styles' => '<css-placeholder token="' . $placeholder_token . '">',
|
Chris@18
|
161 ],
|
Chris@18
|
162 'library' => [
|
Chris@18
|
163 'media/oembed.frame',
|
Chris@18
|
164 ],
|
Chris@18
|
165 ],
|
Chris@18
|
166 '#placeholder_token' => $placeholder_token,
|
Chris@17
|
167 ];
|
Chris@17
|
168 $content = $this->renderer->executeInRenderContext(new RenderContext(), function () use ($resource, $element) {
|
Chris@17
|
169 return $this->renderer->render($element);
|
Chris@17
|
170 });
|
Chris@17
|
171 $response
|
Chris@17
|
172 ->setContent($content)
|
Chris@18
|
173 ->setAttachments($element['#attached'])
|
Chris@17
|
174 ->addCacheableDependency($resource)
|
Chris@17
|
175 ->addCacheableDependency(CacheableMetadata::createFromRenderArray($element));
|
Chris@17
|
176 }
|
Chris@17
|
177 catch (ResourceException $e) {
|
Chris@17
|
178 // Prevent the response from being cached.
|
Chris@17
|
179 $response->setMaxAge(0);
|
Chris@17
|
180
|
Chris@17
|
181 // The oEmbed system makes heavy use of exception wrapping, so log the
|
Chris@17
|
182 // entire exception chain to help with troubleshooting.
|
Chris@17
|
183 do {
|
Chris@17
|
184 // @todo Log additional information from ResourceException, to help with
|
Chris@17
|
185 // debugging, in https://www.drupal.org/project/drupal/issues/2972846.
|
Chris@17
|
186 $this->logger->error($e->getMessage());
|
Chris@17
|
187 $e = $e->getPrevious();
|
Chris@17
|
188 } while ($e);
|
Chris@17
|
189 }
|
Chris@17
|
190
|
Chris@17
|
191 return $response;
|
Chris@17
|
192 }
|
Chris@17
|
193
|
Chris@17
|
194 }
|