HtmlResponseAttachmentsProcessor.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  1. <?php
  2. namespace Drupal\Core\Render;
  3. use Drupal\Core\Asset\AssetCollectionRendererInterface;
  4. use Drupal\Core\Asset\AssetResolverInterface;
  5. use Drupal\Core\Asset\AttachedAssets;
  6. use Drupal\Core\Asset\AttachedAssetsInterface;
  7. use Drupal\Core\Config\ConfigFactoryInterface;
  8. use Drupal\Core\Form\EnforcedResponseException;
  9. use Drupal\Core\Extension\ModuleHandlerInterface;
  10. use Drupal\Component\Utility\Html;
  11. use Symfony\Component\HttpFoundation\RequestStack;
  12. /**
  13. * Processes attachments of HTML responses.
  14. *
  15. * This class is used by the rendering service to process the #attached part of
  16. * the render array, for HTML responses.
  17. *
  18. * To render attachments to HTML for testing without a controller, use the
  19. * 'bare_html_page_renderer' service to generate a
  20. * Drupal\Core\Render\HtmlResponse object. Then use its getContent(),
  21. * getStatusCode(), and/or the headers property to access the result.
  22. *
  23. * @see template_preprocess_html()
  24. * @see \Drupal\Core\Render\AttachmentsResponseProcessorInterface
  25. * @see \Drupal\Core\Render\BareHtmlPageRenderer
  26. * @see \Drupal\Core\Render\HtmlResponse
  27. * @see \Drupal\Core\Render\MainContent\HtmlRenderer
  28. */
  29. class HtmlResponseAttachmentsProcessor implements AttachmentsResponseProcessorInterface {
  30. /**
  31. * The asset resolver service.
  32. *
  33. * @var \Drupal\Core\Asset\AssetResolverInterface
  34. */
  35. protected $assetResolver;
  36. /**
  37. * A config object for the system performance configuration.
  38. *
  39. * @var \Drupal\Core\Config\Config
  40. */
  41. protected $config;
  42. /**
  43. * The CSS asset collection renderer service.
  44. *
  45. * @var \Drupal\Core\Asset\AssetCollectionRendererInterface
  46. */
  47. protected $cssCollectionRenderer;
  48. /**
  49. * The JS asset collection renderer service.
  50. *
  51. * @var \Drupal\Core\Asset\AssetCollectionRendererInterface
  52. */
  53. protected $jsCollectionRenderer;
  54. /**
  55. * The request stack.
  56. *
  57. * @var \Symfony\Component\HttpFoundation\RequestStack
  58. */
  59. protected $requestStack;
  60. /**
  61. * The renderer.
  62. *
  63. * @var \Drupal\Core\Render\RendererInterface
  64. */
  65. protected $renderer;
  66. /**
  67. * The module handler service.
  68. *
  69. * @var \Drupal\Core\Extension\ModuleHandlerInterface
  70. */
  71. protected $moduleHandler;
  72. /**
  73. * Constructs a HtmlResponseAttachmentsProcessor object.
  74. *
  75. * @param \Drupal\Core\Asset\AssetResolverInterface $asset_resolver
  76. * An asset resolver.
  77. * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
  78. * A config factory for retrieving required config objects.
  79. * @param \Drupal\Core\Asset\AssetCollectionRendererInterface $css_collection_renderer
  80. * The CSS asset collection renderer.
  81. * @param \Drupal\Core\Asset\AssetCollectionRendererInterface $js_collection_renderer
  82. * The JS asset collection renderer.
  83. * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
  84. * The request stack.
  85. * @param \Drupal\Core\Render\RendererInterface $renderer
  86. * The renderer.
  87. * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
  88. * The module handler service.
  89. */
  90. public function __construct(AssetResolverInterface $asset_resolver, ConfigFactoryInterface $config_factory, AssetCollectionRendererInterface $css_collection_renderer, AssetCollectionRendererInterface $js_collection_renderer, RequestStack $request_stack, RendererInterface $renderer, ModuleHandlerInterface $module_handler) {
  91. $this->assetResolver = $asset_resolver;
  92. $this->config = $config_factory->get('system.performance');
  93. $this->cssCollectionRenderer = $css_collection_renderer;
  94. $this->jsCollectionRenderer = $js_collection_renderer;
  95. $this->requestStack = $request_stack;
  96. $this->renderer = $renderer;
  97. $this->moduleHandler = $module_handler;
  98. }
  99. /**
  100. * {@inheritdoc}
  101. */
  102. public function processAttachments(AttachmentsInterface $response) {
  103. // @todo Convert to assertion once https://www.drupal.org/node/2408013 lands
  104. if (!$response instanceof HtmlResponse) {
  105. throw new \InvalidArgumentException('\Drupal\Core\Render\HtmlResponse instance expected.');
  106. }
  107. // First, render the actual placeholders; this may cause additional
  108. // attachments to be added to the response, which the attachment
  109. // placeholders rendered by renderHtmlResponseAttachmentPlaceholders() will
  110. // need to include.
  111. //
  112. // @todo Exceptions should not be used for code flow control. However, the
  113. // Form API does not integrate with the HTTP Kernel based architecture of
  114. // Drupal 8. In order to resolve this issue properly it is necessary to
  115. // completely separate form submission from rendering.
  116. // @see https://www.drupal.org/node/2367555
  117. try {
  118. $response = $this->renderPlaceholders($response);
  119. }
  120. catch (EnforcedResponseException $e) {
  121. return $e->getResponse();
  122. }
  123. // Get a reference to the attachments.
  124. $attached = $response->getAttachments();
  125. // Send a message back if the render array has unsupported #attached types.
  126. $unsupported_types = array_diff(
  127. array_keys($attached),
  128. ['html_head', 'feed', 'html_head_link', 'http_header', 'library', 'html_response_attachment_placeholders', 'placeholders', 'drupalSettings']
  129. );
  130. if (!empty($unsupported_types)) {
  131. throw new \LogicException(sprintf('You are not allowed to use %s in #attached.', implode(', ', $unsupported_types)));
  132. }
  133. // If we don't have any placeholders, there is no need to proceed.
  134. if (!empty($attached['html_response_attachment_placeholders'])) {
  135. // Get the placeholders from attached and then remove them.
  136. $attachment_placeholders = $attached['html_response_attachment_placeholders'];
  137. unset($attached['html_response_attachment_placeholders']);
  138. $assets = AttachedAssets::createFromRenderArray(['#attached' => $attached]);
  139. // Take Ajax page state into account, to allow for something like
  140. // Turbolinks to be implemented without altering core.
  141. // @see https://github.com/rails/turbolinks/
  142. $ajax_page_state = $this->requestStack->getCurrentRequest()->get('ajax_page_state');
  143. $assets->setAlreadyLoadedLibraries(isset($ajax_page_state) ? explode(',', $ajax_page_state['libraries']) : []);
  144. $variables = $this->processAssetLibraries($assets, $attachment_placeholders);
  145. // $variables now contains the markup to load the asset libraries. Update
  146. // $attached with the final list of libraries and JavaScript settings, so
  147. // that $response can be updated with those. Then the response object will
  148. // list the final, processed attachments.
  149. $attached['library'] = $assets->getLibraries();
  150. $attached['drupalSettings'] = $assets->getSettings();
  151. // Since we can only replace content in the HTML head section if there's a
  152. // placeholder for it, we can safely avoid processing the render array if
  153. // it's not present.
  154. if (!empty($attachment_placeholders['head'])) {
  155. // 'feed' is a special case of 'html_head_link'. We process them into
  156. // 'html_head_link' entries and merge them.
  157. if (!empty($attached['feed'])) {
  158. $attached = BubbleableMetadata::mergeAttachments(
  159. $attached,
  160. $this->processFeed($attached['feed'])
  161. );
  162. unset($attached['feed']);
  163. }
  164. // 'html_head_link' is a special case of 'html_head' which can be present
  165. // as a head element, but also as a Link: HTTP header depending on
  166. // settings in the render array. Processing it can add to both the
  167. // 'html_head' and 'http_header' keys of '#attached', so we must address
  168. // it before 'html_head'.
  169. if (!empty($attached['html_head_link'])) {
  170. // Merge the processed 'html_head_link' into $attached so that its
  171. // 'html_head' and 'http_header' values are present for further
  172. // processing.
  173. $attached = BubbleableMetadata::mergeAttachments(
  174. $attached,
  175. $this->processHtmlHeadLink($attached['html_head_link'])
  176. );
  177. unset($attached['html_head_link']);
  178. }
  179. // Now we can process 'html_head', which contains both 'feed' and
  180. // 'html_head_link'.
  181. if (!empty($attached['html_head'])) {
  182. $variables['head'] = $this->processHtmlHead($attached['html_head']);
  183. }
  184. }
  185. // Now replace the attachment placeholders.
  186. $this->renderHtmlResponseAttachmentPlaceholders($response, $attachment_placeholders, $variables);
  187. }
  188. // Set the HTTP headers and status code on the response if any bubbled.
  189. if (!empty($attached['http_header'])) {
  190. $this->setHeaders($response, $attached['http_header']);
  191. }
  192. // AttachmentsResponseProcessorInterface mandates that the response it
  193. // processes contains the final attachment values.
  194. $response->setAttachments($attached);
  195. return $response;
  196. }
  197. /**
  198. * Renders placeholders (#attached['placeholders']).
  199. *
  200. * First, the HTML response object is converted to an equivalent render array,
  201. * with #markup being set to the response's content and #attached being set to
  202. * the response's attachments. Among these attachments, there may be
  203. * placeholders that need to be rendered (replaced).
  204. *
  205. * Next, RendererInterface::renderRoot() is called, which renders the
  206. * placeholders into their final markup.
  207. *
  208. * The markup that results from RendererInterface::renderRoot() is now the
  209. * original HTML response's content, but with the placeholders rendered. We
  210. * overwrite the existing content in the original HTML response object with
  211. * this markup. The markup that was rendered for the placeholders may also
  212. * have attachments (e.g. for CSS/JS assets) itself, and cacheability metadata
  213. * that indicates what that markup depends on. That metadata is also added to
  214. * the HTML response object.
  215. *
  216. * @param \Drupal\Core\Render\HtmlResponse $response
  217. * The HTML response whose placeholders are being replaced.
  218. *
  219. * @return \Drupal\Core\Render\HtmlResponse
  220. * The updated HTML response, with replaced placeholders.
  221. *
  222. * @see \Drupal\Core\Render\Renderer::replacePlaceholders()
  223. * @see \Drupal\Core\Render\Renderer::renderPlaceholder()
  224. */
  225. protected function renderPlaceholders(HtmlResponse $response) {
  226. $build = [
  227. '#markup' => Markup::create($response->getContent()),
  228. '#attached' => $response->getAttachments(),
  229. ];
  230. // RendererInterface::renderRoot() renders the $build render array and
  231. // updates it in place. We don't care about the return value (which is just
  232. // $build['#markup']), but about the resulting render array.
  233. // @todo Simplify this when https://www.drupal.org/node/2495001 lands.
  234. $this->renderer->renderRoot($build);
  235. // Update the Response object now that the placeholders have been rendered.
  236. $placeholders_bubbleable_metadata = BubbleableMetadata::createFromRenderArray($build);
  237. $response
  238. ->setContent($build['#markup'])
  239. ->addCacheableDependency($placeholders_bubbleable_metadata)
  240. ->setAttachments($placeholders_bubbleable_metadata->getAttachments());
  241. return $response;
  242. }
  243. /**
  244. * Processes asset libraries into render arrays.
  245. *
  246. * @param \Drupal\Core\Asset\AttachedAssetsInterface $assets
  247. * The attached assets collection for the current response.
  248. * @param array $placeholders
  249. * The placeholders that exist in the response.
  250. *
  251. * @return array
  252. * An array keyed by asset type, with keys:
  253. * - styles
  254. * - scripts
  255. * - scripts_bottom
  256. */
  257. protected function processAssetLibraries(AttachedAssetsInterface $assets, array $placeholders) {
  258. $variables = [];
  259. // Print styles - if present.
  260. if (isset($placeholders['styles'])) {
  261. // Optimize CSS if necessary, but only during normal site operation.
  262. $optimize_css = !defined('MAINTENANCE_MODE') && $this->config->get('css.preprocess');
  263. $variables['styles'] = $this->cssCollectionRenderer->render($this->assetResolver->getCssAssets($assets, $optimize_css));
  264. }
  265. // Print scripts - if any are present.
  266. if (isset($placeholders['scripts']) || isset($placeholders['scripts_bottom'])) {
  267. // Optimize JS if necessary, but only during normal site operation.
  268. $optimize_js = !defined('MAINTENANCE_MODE') && !\Drupal::state()->get('system.maintenance_mode') && $this->config->get('js.preprocess');
  269. list($js_assets_header, $js_assets_footer) = $this->assetResolver->getJsAssets($assets, $optimize_js);
  270. $variables['scripts'] = $this->jsCollectionRenderer->render($js_assets_header);
  271. $variables['scripts_bottom'] = $this->jsCollectionRenderer->render($js_assets_footer);
  272. }
  273. return $variables;
  274. }
  275. /**
  276. * Renders HTML response attachment placeholders.
  277. *
  278. * This is the last step where all of the attachments are placed into the
  279. * response object's contents.
  280. *
  281. * @param \Drupal\Core\Render\HtmlResponse $response
  282. * The HTML response to update.
  283. * @param array $placeholders
  284. * An array of placeholders, keyed by type with the placeholders
  285. * present in the content of the response as values.
  286. * @param array $variables
  287. * The variables to render and replace, keyed by type with renderable
  288. * arrays as values.
  289. */
  290. protected function renderHtmlResponseAttachmentPlaceholders(HtmlResponse $response, array $placeholders, array $variables) {
  291. $content = $response->getContent();
  292. foreach ($placeholders as $type => $placeholder) {
  293. if (isset($variables[$type])) {
  294. $content = str_replace($placeholder, $this->renderer->renderPlain($variables[$type]), $content);
  295. }
  296. }
  297. $response->setContent($content);
  298. }
  299. /**
  300. * Sets headers on a response object.
  301. *
  302. * @param \Drupal\Core\Render\HtmlResponse $response
  303. * The HTML response to update.
  304. * @param array $headers
  305. * The headers to set, as an array. The items in this array should be as
  306. * follows:
  307. * - The header name.
  308. * - The header value.
  309. * - (optional) Whether to replace a current value with the new one, or add
  310. * it to the others. If the value is not replaced, it will be appended,
  311. * resulting in a header like this: 'Header: value1,value2'
  312. */
  313. protected function setHeaders(HtmlResponse $response, array $headers) {
  314. foreach ($headers as $values) {
  315. $name = $values[0];
  316. $value = $values[1];
  317. $replace = !empty($values[2]);
  318. // Drupal treats the HTTP response status code like a header, even though
  319. // it really is not.
  320. if (strtolower($name) === 'status') {
  321. $response->setStatusCode($value);
  322. }
  323. else {
  324. $response->headers->set($name, $value, $replace);
  325. }
  326. }
  327. }
  328. /**
  329. * Ensure proper key/data order and defaults for renderable head items.
  330. *
  331. * @param array $html_head
  332. * The ['#attached']['html_head'] portion of a render array.
  333. *
  334. * @return array
  335. * The ['#attached']['html_head'] portion of a render array with #type of
  336. * html_tag added for items without a #type.
  337. */
  338. protected function processHtmlHead(array $html_head) {
  339. $head = [];
  340. foreach ($html_head as $item) {
  341. list($data, $key) = $item;
  342. if (!isset($data['#type'])) {
  343. $data['#type'] = 'html_tag';
  344. }
  345. $head[$key] = $data;
  346. }
  347. return $head;
  348. }
  349. /**
  350. * Transform a html_head_link array into html_head and http_header arrays.
  351. *
  352. * Variable html_head_link is a special case of html_head which can be present
  353. * as a link item in the HTML head section, and also as a Link: HTTP header,
  354. * depending on options in the render array. Processing it can add to both the
  355. * html_head and http_header sections.
  356. *
  357. * @param array $html_head_link
  358. * The 'html_head_link' value of a render array. Each head link is specified
  359. * by a two-element array:
  360. * - An array specifying the attributes of the link.
  361. * - A boolean specifying whether the link should also be a Link: HTTP
  362. * header.
  363. *
  364. * @return array
  365. * An ['#attached'] section of a render array. This allows us to easily
  366. * merge the results with other render arrays. The array could contain the
  367. * following keys:
  368. * - http_header
  369. * - html_head
  370. */
  371. protected function processHtmlHeadLink(array $html_head_link) {
  372. $attached = [];
  373. foreach ($html_head_link as $item) {
  374. $attributes = $item[0];
  375. $should_add_header = isset($item[1]) ? $item[1] : FALSE;
  376. $element = [
  377. '#tag' => 'link',
  378. '#attributes' => $attributes,
  379. ];
  380. $href = $attributes['href'];
  381. $attached['html_head'][] = [$element, 'html_head_link:' . $attributes['rel'] . ':' . $href];
  382. if ($should_add_header) {
  383. // Also add a HTTP header "Link:".
  384. $href = '<' . Html::escape($attributes['href']) . '>';
  385. unset($attributes['href']);
  386. if ($param = drupal_http_header_attributes($attributes)) {
  387. $href .= ';' . $param;
  388. }
  389. $attached['http_header'][] = ['Link', $href, FALSE];
  390. }
  391. }
  392. return $attached;
  393. }
  394. /**
  395. * Transform a 'feed' attachment into an 'html_head_link' attachment.
  396. *
  397. * The RSS feed is a special case of 'html_head_link', so we just turn it into
  398. * one.
  399. *
  400. * @param array $attached_feed
  401. * The ['#attached']['feed'] portion of a render array.
  402. *
  403. * @return array
  404. * An ['#attached']['html_head_link'] array, suitable for merging with
  405. * another 'html_head_link' array.
  406. */
  407. protected function processFeed($attached_feed) {
  408. $html_head_link = [];
  409. foreach ($attached_feed as $item) {
  410. $feed_link = [
  411. 'href' => $item[0],
  412. 'rel' => 'alternate',
  413. 'title' => empty($item[1]) ? '' : $item[1],
  414. 'type' => 'application/rss+xml',
  415. ];
  416. $html_head_link[] = [$feed_link, FALSE];
  417. }
  418. return ['html_head_link' => $html_head_link];
  419. }
  420. }