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