79aba6f9d64d234f3045fdc4d717be0f23234c41
[yaffs-website] / web / core / modules / big_pipe / src / Render / BigPipe.php
1 <?php
2
3 namespace Drupal\big_pipe\Render;
4
5 use Drupal\Component\Utility\Crypt;
6 use Drupal\Component\Utility\Html;
7 use Drupal\Core\Ajax\AjaxResponse;
8 use Drupal\Core\Ajax\ReplaceCommand;
9 use Drupal\Core\Asset\AttachedAssets;
10 use Drupal\Core\Asset\AttachedAssetsInterface;
11 use Drupal\Core\Config\ConfigFactoryInterface;
12 use Drupal\Core\Render\HtmlResponse;
13 use Drupal\Core\Render\RendererInterface;
14 use Symfony\Component\EventDispatcher\EventDispatcherInterface;
15 use Symfony\Component\HttpFoundation\Request;
16 use Symfony\Component\HttpFoundation\RequestStack;
17 use Symfony\Component\HttpFoundation\Response;
18 use Symfony\Component\HttpFoundation\Session\SessionInterface;
19 use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
20 use Symfony\Component\HttpKernel\HttpKernelInterface;
21 use Symfony\Component\HttpKernel\KernelEvents;
22
23 /**
24  * Service for sending an HTML response in chunks (to get faster page loads).
25  *
26  * At a high level, BigPipe sends a HTML response in chunks:
27  * 1. one chunk: everything until just before </body> — this contains BigPipe
28  *    placeholders for the personalized parts of the page. Hence this sends the
29  *    non-personalized parts of the page. Let's call it The Skeleton.
30  * 2. N chunks: a <script> tag per BigPipe placeholder in The Skeleton.
31  * 3. one chunk: </body> and everything after it.
32  *
33  * This is conceptually identical to Facebook's BigPipe (hence the name).
34  *
35  * @see https://www.facebook.com/notes/facebook-engineering/bigpipe-pipelining-web-pages-for-high-performance/389414033919
36  *
37  * The major way in which Drupal differs from Facebook's implementation (and
38  * others) is in its ability to automatically figure out which parts of the page
39  * can benefit from BigPipe-style delivery. Drupal's render system has the
40  * concept of "auto-placeholdering": content that is too dynamic is replaced
41  * with a placeholder that can then be rendered at a later time. On top of that,
42  * it also has the concept of "placeholder strategies": by default, placeholders
43  * are replaced on the server side and the response is blocked on all of them
44  * being replaced. But it's possible to add additional placeholder strategies.
45  * BigPipe is just another placeholder strategy. Others could be ESI, AJAX …
46  *
47  * @see https://www.drupal.org/developing/api/8/render/arrays/cacheability/auto-placeholdering
48  * @see \Drupal\Core\Render\PlaceholderGeneratorInterface::shouldAutomaticallyPlaceholder()
49  * @see \Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface
50  * @see \Drupal\Core\Render\Placeholder\SingleFlushStrategy
51  * @see \Drupal\big_pipe\Render\Placeholder\BigPipeStrategy
52  *
53  * There is also one noteworthy technical addition that Drupal makes. BigPipe as
54  * described above, and as implemented by Facebook, can only work if JavaScript
55  * is enabled. The BigPipe module also makes it possible to replace placeholders
56  * using BigPipe in-situ, without JavaScript. This is not technically BigPipe at
57  * all; it's just the use of multiple flushes. Since it is able to reuse much of
58  * the logic though, we choose to call this "no-JS BigPipe".
59  *
60  * However, there is also a tangible benefit: some dynamic/expensive content is
61  * not HTML, but for example a HTML attribute value (or part thereof). It's not
62  * possible to efficiently replace such content using JavaScript, so "classic"
63  * BigPipe is out of the question. For example: CSRF tokens in URLs.
64  *
65  * This allows us to use both no-JS BigPipe and "classic" BigPipe in the same
66  * response to maximize the amount of content we can send as early as possible.
67  *
68  * Finally, a closer look at the implementation, and how it supports and reuses
69  * existing Drupal concepts:
70  * 1. BigPipe placeholders: 1 HtmlResponse + N embedded AjaxResponses.
71  *   - Before a BigPipe response is sent, it is just a HTML response that
72  *     contains BigPipe placeholders. Those placeholders look like
73  *     <span data-big-pipe-placeholder-id="…"></span>. JavaScript is used to
74  *     replace those placeholders.
75  *     Therefore these placeholders are actually sent to the client.
76  *   - The Skeleton of course has attachments, including most notably asset
77  *     libraries. And those we track in drupalSettings.ajaxPageState.libraries —
78  *     so that when we load new content through AJAX, we don't load the same
79  *     asset libraries again. A HTML page can have multiple AJAX responses, each
80  *     of which should take into account the combined AJAX page state of the
81  *     HTML document and all preceding AJAX responses.
82  *   - BigPipe does not make use of multiple AJAX requests/responses. It uses a
83  *     single HTML response. But it is a more long-lived one: The Skeleton is
84  *     sent first, the closing </body> tag is not yet sent, and the connection
85  *     is kept open. Whenever another BigPipe Placeholder is rendered, Drupal
86  *     sends (and so actually appends to the already-sent HTML) something like
87  *     <script type="application/vnd.drupal-ajax">[{"command":"settings","settings":{…}}, {"command":…}.
88  *   - So, for every BigPipe placeholder, we send such a <script
89  *     type="application/vnd.drupal-ajax"> tag. And the contents of that tag is
90  *     exactly like an AJAX response. The BigPipe module has JavaScript that
91  *     listens for these and applies them. Let's call it an Embedded AJAX
92  *     Response (since it is embedded in the HTML response). Now for the
93  *     interesting bit: each of those Embedded AJAX Responses must also take
94  *     into account the cumulative AJAX page state of the HTML document and all
95  *     preceding Embedded AJAX responses.
96  * 2. No-JS BigPipe placeholders: 1 HtmlResponse + N embedded HtmlResponses.
97  *   - Before a BigPipe response is sent, it is just a HTML response that
98  *     contains no-JS BigPipe placeholders. Those placeholders can take two
99  *     different forms:
100  *     1. <span data-big-pipe-nojs-placeholder-id="…"></span> if it's a
101  *        placeholder that will be replaced by HTML
102  *     2. big_pipe_nojs_placeholder_attribute_safe:… if it's a placeholder
103  *        inside a HTML attribute, in which 1. would be invalid (angle brackets
104  *        are not allowed inside HTML attributes)
105  *     No-JS BigPipe placeholders are not replaced using JavaScript, they must
106  *     be replaced upon sending the BigPipe response. So, while the response is
107  *     being sent, upon encountering these placeholders, their corresponding
108  *     placeholder replacements are sent instead.
109  *     Therefore these placeholders are never actually sent to the client.
110  *   - See second bullet of point 1.
111  *   - No-JS BigPipe does not use multiple AJAX requests/responses. It uses a
112  *     single HTML response. But it is a more long-lived one: The Skeleton is
113  *     split into multiple parts, the separators are where the no-JS BigPipe
114  *     placeholders used to be. Whenever another no-JS BigPipe placeholder is
115  *     rendered, Drupal sends (and so actually appends to the already-sent HTML)
116  *     something like
117  *     <link rel="stylesheet" …><script …><content>.
118  *   - So, for every no-JS BigPipe placeholder, we send its associated CSS and
119  *     header JS that has not already been sent (the bottom JS is not yet sent,
120  *     so we can accumulate all of it and send it together at the end). This
121  *     ensures that the markup is rendered as it was originally intended: its
122  *     CSS and JS used to be blocking, and it still is. Let's call it an
123  *     Embedded HTML response. Each of those Embedded HTML Responses must also
124  *     take into account the cumulative AJAX page state of the HTML document and
125  *     all preceding Embedded HTML responses.
126  *   - Finally: any non-critical JavaScript associated with all Embedded HTML
127  *     Responses, i.e. any footer/bottom/non-header JavaScript, is loaded after
128  *     The Skeleton.
129  *
130  * Combining all of the above, when using both BigPipe placeholders and no-JS
131  * BigPipe placeholders, we therefore send: 1 HtmlResponse + M Embedded HTML
132  * Responses + N Embedded AJAX Responses. Schematically, we send these chunks:
133  *  1. Byte zero until 1st no-JS placeholder: headers + <html><head /><span>…</span>
134  *  2. 1st no-JS placeholder replacement: <link rel="stylesheet" …><script …><content>
135  *  3. Content until 2nd no-JS placeholder: <span>…</span>
136  *  4. 2nd no-JS placeholder replacement: <link rel="stylesheet" …><script …><content>
137  *  5. Content until 3rd no-JS placeholder: <span>…</span>
138  *  6. [… repeat until all no-JS placeholder replacements are sent …]
139  *  7. Send content after last no-JS placeholder.
140  *  8. Send script_bottom (markup to load bottom i.e. non-critical JS).
141  *  9. 1st placeholder replacement: <script type="application/vnd.drupal-ajax">[{"command":"settings","settings":{…}}, {"command":…}
142  * 10. 2nd placeholder replacement: <script type="application/vnd.drupal-ajax">[{"command":"settings","settings":{…}}, {"command":…}
143  * 11. [… repeat until all placeholder replacements are sent …]
144  * 12. Send </body> and everything after it.
145  * 13. Terminate request/response cycle.
146  *
147  * @see \Drupal\big_pipe\EventSubscriber\HtmlResponseBigPipeSubscriber
148  * @see \Drupal\big_pipe\Render\Placeholder\BigPipeStrategy
149  */
150 class BigPipe {
151
152   /**
153    * The BigPipe placeholder replacements start signal.
154    *
155    * @var string
156    */
157   const START_SIGNAL = '<script type="application/vnd.drupal-ajax" data-big-pipe-event="start"></script>';
158
159   /**
160    * The BigPipe placeholder replacements stop signal.
161    *
162    * @var string
163    */
164   const STOP_SIGNAL = '<script type="application/vnd.drupal-ajax" data-big-pipe-event="stop"></script>';
165
166   /**
167    * The renderer.
168    *
169    * @var \Drupal\Core\Render\RendererInterface
170    */
171   protected $renderer;
172
173   /**
174    * The session.
175    *
176    * @var \Symfony\Component\HttpFoundation\Session\SessionInterface
177    */
178   protected $session;
179
180   /**
181    * The request stack.
182    *
183    * @var \Symfony\Component\HttpFoundation\RequestStack
184    */
185   protected $requestStack;
186
187   /**
188    * The HTTP kernel.
189    *
190    * @var \Symfony\Component\HttpKernel\HttpKernelInterface
191    */
192   protected $httpKernel;
193
194   /**
195    * The event dispatcher.
196    *
197    * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
198    */
199   protected $eventDispatcher;
200
201   /**
202    * The config factory.
203    *
204    * @var \Drupal\Core\Config\ConfigFactoryInterface
205    */
206   protected $configFactory;
207
208   /**
209    * Constructs a new BigPipe class.
210    *
211    * @param \Drupal\Core\Render\RendererInterface $renderer
212    *   The renderer.
213    * @param \Symfony\Component\HttpFoundation\Session\SessionInterface $session
214    *   The session.
215    * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
216    *   The request stack.
217    * @param \Symfony\Component\HttpKernel\HttpKernelInterface $http_kernel
218    *   The HTTP kernel.
219    * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
220    *   The event dispatcher.
221    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
222    *   The config factory.
223    */
224   public function __construct(RendererInterface $renderer, SessionInterface $session, RequestStack $request_stack, HttpKernelInterface $http_kernel, EventDispatcherInterface $event_dispatcher, ConfigFactoryInterface $config_factory) {
225     $this->renderer = $renderer;
226     $this->session = $session;
227     $this->requestStack = $request_stack;
228     $this->httpKernel = $http_kernel;
229     $this->eventDispatcher = $event_dispatcher;
230     $this->configFactory = $config_factory;
231   }
232
233   /**
234    * Performs tasks before sending content (and rendering placeholders).
235    */
236   protected function performPreSendTasks() {
237     // The content in the placeholders may depend on the session, and by the
238     // time the response is sent (see index.php), the session is already
239     // closed. Reopen it for the duration that we are rendering placeholders.
240     $this->session->start();
241   }
242
243   /**
244    * Performs tasks after sending content (and rendering placeholders).
245    */
246   protected function performPostSendTasks() {
247     // Close the session again.
248     $this->session->save();
249   }
250
251   /**
252    * Sends a chunk.
253    *
254    * @param string|\Drupal\Core\Render\HtmlResponse $chunk
255    *   The string or response to append. String if there's no cacheability
256    *   metadata or attachments to merge.
257    */
258   protected function sendChunk($chunk) {
259     assert(is_string($chunk) || $chunk instanceof HtmlResponse);
260     if ($chunk instanceof HtmlResponse) {
261       print $chunk->getContent();
262     }
263     else {
264       print $chunk;
265     }
266     flush();
267   }
268
269   /**
270    * Sends an HTML response in chunks using the BigPipe technique.
271    *
272    * @param \Drupal\big_pipe\Render\BigPipeResponse $response
273    *   The BigPipe response to send.
274    *
275    * @internal
276    *   This method should only be invoked by
277    *   \Drupal\big_pipe\Render\BigPipeResponse, which is itself an internal
278    *   class.
279    */
280   public function sendContent(BigPipeResponse $response) {
281     $content = $response->getContent();
282     $attachments = $response->getAttachments();
283
284     // First, gather the BigPipe placeholders that must be replaced.
285     $placeholders = isset($attachments['big_pipe_placeholders']) ? $attachments['big_pipe_placeholders'] : [];
286     $nojs_placeholders = isset($attachments['big_pipe_nojs_placeholders']) ? $attachments['big_pipe_nojs_placeholders'] : [];
287
288     // BigPipe sends responses using "Transfer-Encoding: chunked". To avoid
289     // sending already-sent assets, it is necessary to track cumulative assets
290     // from all previously rendered/sent chunks.
291     // @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.41
292     $cumulative_assets = AttachedAssets::createFromRenderArray(['#attached' => $attachments]);
293     $cumulative_assets->setAlreadyLoadedLibraries($attachments['library']);
294
295     $this->performPreSendTasks();
296
297     // Find the closing </body> tag and get the strings before and after. But be
298     // careful to use the latest occurrence of the string "</body>", to ensure
299     // that strings in inline JavaScript or CDATA sections aren't used instead.
300     $parts = explode('</body>', $content);
301     $post_body = array_pop($parts);
302     $pre_body = implode('', $parts);
303
304     $this->sendPreBody($pre_body, $nojs_placeholders, $cumulative_assets);
305     $this->sendPlaceholders($placeholders, $this->getPlaceholderOrder($pre_body, $placeholders), $cumulative_assets);
306     $this->sendPostBody($post_body);
307
308     $this->performPostSendTasks();
309   }
310
311   /**
312    * Sends everything until just before </body>.
313    *
314    * @param string $pre_body
315    *   The HTML response's content until the closing </body> tag.
316    * @param array $no_js_placeholders
317    *   The no-JS BigPipe placeholders.
318    * @param \Drupal\Core\Asset\AttachedAssetsInterface $cumulative_assets
319    *   The cumulative assets sent so far; to be updated while rendering no-JS
320    *   BigPipe placeholders.
321    */
322   protected function sendPreBody($pre_body, array $no_js_placeholders, AttachedAssetsInterface $cumulative_assets) {
323     // If there are no no-JS BigPipe placeholders, we can send the pre-</body>
324     // part of the page immediately.
325     if (empty($no_js_placeholders)) {
326       $this->sendChunk($pre_body);
327       return;
328     }
329
330     // Extract the scripts_bottom markup: the no-JS BigPipe placeholders that we
331     // will render may attach additional asset libraries, and if so, it will be
332     // necessary to re-render scripts_bottom.
333     list($pre_scripts_bottom, $scripts_bottom, $post_scripts_bottom) = explode('<drupal-big-pipe-scripts-bottom-marker>', $pre_body, 3);
334     $cumulative_assets_initial = clone $cumulative_assets;
335
336     $this->sendNoJsPlaceholders($pre_scripts_bottom . $post_scripts_bottom, $no_js_placeholders, $cumulative_assets);
337
338     // If additional asset libraries or drupalSettings were attached by any of
339     // the placeholders, then we need to re-render scripts_bottom.
340     if ($cumulative_assets_initial != $cumulative_assets) {
341       // Create a new HtmlResponse. Ensure the CSS and (non-bottom) JS is sent
342       // before the HTML they're associated with.
343       // @see \Drupal\Core\Render\HtmlResponseSubscriber
344       // @see template_preprocess_html()
345       $js_bottom_placeholder = '<nojs-bigpipe-placeholder-scripts-bottom-placeholder token="' . Crypt::randomBytesBase64(55) . '">';
346
347       $html_response = new HtmlResponse();
348       $html_response->setContent([
349         '#markup' => BigPipeMarkup::create($js_bottom_placeholder),
350         '#attached' => [
351           'drupalSettings' => $cumulative_assets->getSettings(),
352           'library' => $cumulative_assets->getAlreadyLoadedLibraries(),
353           'html_response_attachment_placeholders' => [
354             'scripts_bottom' => $js_bottom_placeholder,
355           ],
356         ],
357       ]);
358       $html_response->getCacheableMetadata()->setCacheMaxAge(0);
359
360       // Push a fake request with the asset libraries loaded so far and dispatch
361       // KernelEvents::RESPONSE event. This results in the attachments for the
362       // HTML response being processed by HtmlResponseAttachmentsProcessor and
363       // hence the HTML to load the bottom JavaScript can be rendered.
364       $fake_request = $this->requestStack->getMasterRequest()->duplicate();
365       $html_response = $this->filterEmbeddedResponse($fake_request, $html_response);
366       $scripts_bottom = $html_response->getContent();
367     }
368
369     $this->sendChunk($scripts_bottom);
370   }
371
372   /**
373    * Sends no-JS BigPipe placeholders' replacements as embedded HTML responses.
374    *
375    * @param string $html
376    *   HTML markup.
377    * @param array $no_js_placeholders
378    *   Associative array; the no-JS BigPipe placeholders. Keys are the BigPipe
379    *   selectors.
380    * @param \Drupal\Core\Asset\AttachedAssetsInterface $cumulative_assets
381    *   The cumulative assets sent so far; to be updated while rendering no-JS
382    *   BigPipe placeholders.
383    *
384    * @throws \Exception
385    *   If an exception is thrown during the rendering of a placeholder, it is
386    *   caught to allow the other placeholders to still be replaced. But when
387    *   error logging is configured to be verbose, the exception is rethrown to
388    *   simplify debugging.
389    */
390   protected function sendNoJsPlaceholders($html, $no_js_placeholders, AttachedAssetsInterface $cumulative_assets) {
391     // Split the HTML on every no-JS placeholder string.
392     $prepare_for_preg_split = function ($placeholder_string) {
393       return '(' . preg_quote($placeholder_string, '/') . ')';
394     };
395     $preg_placeholder_strings = array_map($prepare_for_preg_split, array_keys($no_js_placeholders));
396     $fragments = preg_split('/' . implode('|', $preg_placeholder_strings) . '/', $html, NULL, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
397
398     // Determine how many occurrences there are of each no-JS placeholder.
399     $placeholder_occurrences = array_count_values(array_intersect($fragments, array_keys($no_js_placeholders)));
400
401     // Set up a variable to store the content of placeholders that have multiple
402     // occurrences.
403     $multi_occurrence_placeholders_content = [];
404
405     foreach ($fragments as $fragment) {
406       // If the fragment isn't one of the no-JS placeholders, it is the HTML in
407       // between placeholders and it must be printed & flushed immediately. The
408       // rest of the logic in the loop handles the placeholders.
409       if (!isset($no_js_placeholders[$fragment])) {
410         $this->sendChunk($fragment);
411         continue;
412       }
413
414       // If there are multiple occurrences of this particular placeholder, and
415       // this is the second occurrence, we can skip all calculations and just
416       // send the same content.
417       if ($placeholder_occurrences[$fragment] > 1 && isset($multi_occurrence_placeholders_content[$fragment])) {
418         $this->sendChunk($multi_occurrence_placeholders_content[$fragment]);
419         continue;
420       }
421
422       $placeholder = $fragment;
423       assert(isset($no_js_placeholders[$placeholder]));
424       $token = Crypt::randomBytesBase64(55);
425
426       // Render the placeholder, but include the cumulative settings assets, so
427       // we can calculate the overall settings for the entire page.
428       $placeholder_plus_cumulative_settings = [
429         'placeholder' => $no_js_placeholders[$placeholder],
430         'cumulative_settings_' . $token => [
431           '#attached' => [
432             'drupalSettings' => $cumulative_assets->getSettings(),
433           ],
434         ],
435       ];
436       try {
437         $elements = $this->renderPlaceholder($placeholder, $placeholder_plus_cumulative_settings);
438       }
439       catch (\Exception $e) {
440         if ($this->configFactory->get('system.logging')->get('error_level') === ERROR_REPORTING_DISPLAY_VERBOSE) {
441           throw $e;
442         }
443         else {
444           trigger_error($e, E_USER_ERROR);
445           continue;
446         }
447       }
448
449       // Create a new HtmlResponse. Ensure the CSS and (non-bottom) JS is sent
450       // before the HTML they're associated with. In other words: ensure the
451       // critical assets for this placeholder's markup are loaded first.
452       // @see \Drupal\Core\Render\HtmlResponseSubscriber
453       // @see template_preprocess_html()
454       $css_placeholder = '<nojs-bigpipe-placeholder-styles-placeholder token="' . $token . '">';
455       $js_placeholder = '<nojs-bigpipe-placeholder-scripts-placeholder token="' . $token . '">';
456       $elements['#markup'] = BigPipeMarkup::create($css_placeholder . $js_placeholder . (string) $elements['#markup']);
457       $elements['#attached']['html_response_attachment_placeholders']['styles'] = $css_placeholder;
458       $elements['#attached']['html_response_attachment_placeholders']['scripts'] = $js_placeholder;
459
460       $html_response = new HtmlResponse();
461       $html_response->setContent($elements);
462       $html_response->getCacheableMetadata()->setCacheMaxAge(0);
463
464       // Push a fake request with the asset libraries loaded so far and dispatch
465       // KernelEvents::RESPONSE event. This results in the attachments for the
466       // HTML response being processed by HtmlResponseAttachmentsProcessor and
467       // hence:
468       // - the HTML to load the CSS can be rendered.
469       // - the HTML to load the JS (at the top) can be rendered.
470       $fake_request = $this->requestStack->getMasterRequest()->duplicate();
471       $fake_request->request->set('ajax_page_state', ['libraries' => implode(',', $cumulative_assets->getAlreadyLoadedLibraries())]);
472       try {
473         $html_response = $this->filterEmbeddedResponse($fake_request, $html_response);
474       }
475       catch (\Exception $e) {
476         if ($this->configFactory->get('system.logging')->get('error_level') === ERROR_REPORTING_DISPLAY_VERBOSE) {
477           throw $e;
478         }
479         else {
480           trigger_error($e, E_USER_ERROR);
481           continue;
482         }
483       }
484
485       // Send this embedded HTML response.
486       $this->sendChunk($html_response);
487
488       // Another placeholder was rendered and sent, track the set of asset
489       // libraries sent so far. Any new settings also need to be tracked, so
490       // they can be sent in ::sendPreBody().
491       $cumulative_assets->setAlreadyLoadedLibraries(array_merge($cumulative_assets->getAlreadyLoadedLibraries(), $html_response->getAttachments()['library']));
492       $cumulative_assets->setSettings($html_response->getAttachments()['drupalSettings']);
493
494       // If there are multiple occurrences of this particular placeholder, track
495       // the content that was sent, so we can skip all calculations for the next
496       // occurrence.
497       if ($placeholder_occurrences[$fragment] > 1) {
498         $multi_occurrence_placeholders_content[$fragment] = $html_response->getContent();
499       }
500     }
501   }
502
503   /**
504    * Sends BigPipe placeholders' replacements as embedded AJAX responses.
505    *
506    * @param array $placeholders
507    *   Associative array; the BigPipe placeholders. Keys are the BigPipe
508    *   placeholder IDs.
509    * @param array $placeholder_order
510    *   Indexed array; the order in which the BigPipe placeholders must be sent.
511    *   Values are the BigPipe placeholder IDs. (These values correspond to keys
512    *   in $placeholders.)
513    * @param \Drupal\Core\Asset\AttachedAssetsInterface $cumulative_assets
514    *   The cumulative assets sent so far; to be updated while rendering BigPipe
515    *   placeholders.
516    *
517    * @throws \Exception
518    *   If an exception is thrown during the rendering of a placeholder, it is
519    *   caught to allow the other placeholders to still be replaced. But when
520    *   error logging is configured to be verbose, the exception is rethrown to
521    *   simplify debugging.
522    */
523   protected function sendPlaceholders(array $placeholders, array $placeholder_order, AttachedAssetsInterface $cumulative_assets) {
524     // Return early if there are no BigPipe placeholders to send.
525     if (empty($placeholders)) {
526       return;
527     }
528
529     // Send the start signal.
530     $this->sendChunk("\n" . static::START_SIGNAL . "\n");
531
532     // A BigPipe response consists of a HTML response plus multiple embedded
533     // AJAX responses. To process the attachments of those AJAX responses, we
534     // need a fake request that is identical to the master request, but with
535     // one change: it must have the right Accept header, otherwise the work-
536     // around for a bug in IE9 will cause not JSON, but <textarea>-wrapped JSON
537     // to be returned.
538     // @see \Drupal\Core\EventSubscriber\AjaxResponseSubscriber::onResponse()
539     $fake_request = $this->requestStack->getMasterRequest()->duplicate();
540     $fake_request->headers->set('Accept', 'application/vnd.drupal-ajax');
541
542     foreach ($placeholder_order as $placeholder_id) {
543       if (!isset($placeholders[$placeholder_id])) {
544         continue;
545       }
546
547       // Render the placeholder.
548       $placeholder_render_array = $placeholders[$placeholder_id];
549       try {
550         $elements = $this->renderPlaceholder($placeholder_id, $placeholder_render_array);
551       }
552       catch (\Exception $e) {
553         if ($this->configFactory->get('system.logging')->get('error_level') === ERROR_REPORTING_DISPLAY_VERBOSE) {
554           throw $e;
555         }
556         else {
557           trigger_error($e, E_USER_ERROR);
558           continue;
559         }
560       }
561
562       // Create a new AjaxResponse.
563       $ajax_response = new AjaxResponse();
564       // JavaScript's querySelector automatically decodes HTML entities in
565       // attributes, so we must decode the entities of the current BigPipe
566       // placeholder ID (which has HTML entities encoded since we use it to find
567       // the placeholders).
568       $big_pipe_js_placeholder_id = Html::decodeEntities($placeholder_id);
569       $ajax_response->addCommand(new ReplaceCommand(sprintf('[data-big-pipe-placeholder-id="%s"]', $big_pipe_js_placeholder_id), $elements['#markup']));
570       $ajax_response->setAttachments($elements['#attached']);
571
572       // Push a fake request with the asset libraries loaded so far and dispatch
573       // KernelEvents::RESPONSE event. This results in the attachments for the
574       // AJAX response being processed by AjaxResponseAttachmentsProcessor and
575       // hence:
576       // - the necessary AJAX commands to load the necessary missing asset
577       //   libraries and updated AJAX page state are added to the AJAX response
578       // - the attachments associated with the response are finalized, which
579       //   allows us to track the total set of asset libraries sent in the
580       //   initial HTML response plus all embedded AJAX responses sent so far.
581       $fake_request->request->set('ajax_page_state', ['libraries' => implode(',', $cumulative_assets->getAlreadyLoadedLibraries())] + $cumulative_assets->getSettings()['ajaxPageState']);
582       try {
583         $ajax_response = $this->filterEmbeddedResponse($fake_request, $ajax_response);
584       }
585       catch (\Exception $e) {
586         if ($this->configFactory->get('system.logging')->get('error_level') === ERROR_REPORTING_DISPLAY_VERBOSE) {
587           throw $e;
588         }
589         else {
590           trigger_error($e, E_USER_ERROR);
591           continue;
592         }
593       }
594
595       // Send this embedded AJAX response.
596       $json = $ajax_response->getContent();
597       $output = <<<EOF
598     <script type="application/vnd.drupal-ajax" data-big-pipe-replacement-for-placeholder-with-id="$placeholder_id">
599     $json
600     </script>
601 EOF;
602       $this->sendChunk($output);
603
604       // Another placeholder was rendered and sent, track the set of asset
605       // libraries sent so far. Any new settings are already sent; we don't need
606       // to track those.
607       if (isset($ajax_response->getAttachments()['drupalSettings']['ajaxPageState']['libraries'])) {
608         $cumulative_assets->setAlreadyLoadedLibraries(explode(',', $ajax_response->getAttachments()['drupalSettings']['ajaxPageState']['libraries']));
609       }
610     }
611
612     // Send the stop signal.
613     $this->sendChunk("\n" . static::STOP_SIGNAL . "\n");
614   }
615
616   /**
617    * Filters the given embedded response, using the cumulative AJAX page state.
618    *
619    * @param \Symfony\Component\HttpFoundation\Request $fake_request
620    *   A fake subrequest that contains the cumulative AJAX page state of the
621    *   HTML document and all preceding Embedded HTML or AJAX responses.
622    * @param \Symfony\Component\HttpFoundation\Response|\Drupal\Core\Render\HtmlResponse|\Drupal\Core\Ajax\AjaxResponse $embedded_response
623    *   Either a HTML response or an AJAX response that will be embedded in the
624    *   overall HTML response.
625    *
626    * @return \Symfony\Component\HttpFoundation\Response
627    *   The filtered response, which will load only the assets that $fake_request
628    *   did not indicate to already have been loaded, plus the updated cumulative
629    *   AJAX page state.
630    */
631   protected function filterEmbeddedResponse(Request $fake_request, Response $embedded_response) {
632     assert($embedded_response instanceof HtmlResponse || $embedded_response instanceof AjaxResponse);
633     return $this->filterResponse($fake_request, HttpKernelInterface::SUB_REQUEST, $embedded_response);
634   }
635
636   /**
637    * Filters the given response.
638    *
639    * @param \Symfony\Component\HttpFoundation\Request $request
640    *   The request for which a response is being sent.
641    * @param int $request_type
642    *   The request type. Can either be
643    *   \Symfony\Component\HttpKernel\HttpKernelInterface::MASTER_REQUEST or
644    *   \Symfony\Component\HttpKernel\HttpKernelInterface::SUB_REQUEST.
645    * @param \Symfony\Component\HttpFoundation\Response $response
646    *   The response to filter.
647    *
648    * @return \Symfony\Component\HttpFoundation\Response
649    *   The filtered response.
650    */
651   protected function filterResponse(Request $request, $request_type, Response $response) {
652     assert($request_type === HttpKernelInterface::MASTER_REQUEST || $request_type === HttpKernelInterface::SUB_REQUEST);
653     $this->requestStack->push($request);
654     $event = new FilterResponseEvent($this->httpKernel, $request, $request_type, $response);
655     $this->eventDispatcher->dispatch(KernelEvents::RESPONSE, $event);
656     $filtered_response = $event->getResponse();
657     $this->requestStack->pop();
658     return $filtered_response;
659   }
660
661   /**
662    * Sends </body> and everything after it.
663    *
664    * @param string $post_body
665    *   The HTML response's content after the closing </body> tag.
666    */
667   protected function sendPostBody($post_body) {
668     $this->sendChunk('</body>' . $post_body);
669   }
670
671   /**
672    * Renders a placeholder, and just that placeholder.
673    *
674    * BigPipe renders placeholders independently of the rest of the content, so
675    * it needs to be able to render placeholders by themselves.
676    *
677    * @param string $placeholder
678    *   The placeholder to render.
679    * @param array $placeholder_render_array
680    *   The render array associated with that placeholder.
681    *
682    * @return array
683    *   The render array representing the rendered placeholder.
684    *
685    * @see \Drupal\Core\Render\RendererInterface::renderPlaceholder()
686    */
687   protected function renderPlaceholder($placeholder, array $placeholder_render_array) {
688     $elements = [
689       '#markup' => $placeholder,
690       '#attached' => [
691         'placeholders' => [
692           $placeholder => $placeholder_render_array,
693         ],
694       ],
695     ];
696     return $this->renderer->renderPlaceholder($placeholder, $elements);
697   }
698
699   /**
700    * Gets the BigPipe placeholder order.
701    *
702    * Determines the order in which BigPipe placeholders must be replaced.
703    *
704    * @param string $html
705    *   HTML markup.
706    * @param array $placeholders
707    *   Associative array; the BigPipe placeholders. Keys are the BigPipe
708    *   placeholder IDs.
709    *
710    * @return array
711    *   Indexed array; the order in which the BigPipe placeholders must be sent.
712    *   Values are the BigPipe placeholder IDs. Note that only unique
713    *   placeholders are kept: if the same placeholder occurs multiple times, we
714    *   only keep the first occurrence.
715    */
716   protected function getPlaceholderOrder($html, $placeholders) {
717     $fragments = explode('<span data-big-pipe-placeholder-id="', $html);
718     array_shift($fragments);
719     $placeholder_ids = [];
720
721     foreach ($fragments as $fragment) {
722       $t = explode('"></span>', $fragment, 2);
723       $placeholder_id = $t[0];
724       $placeholder_ids[] = $placeholder_id;
725     }
726     $placeholder_ids = array_unique($placeholder_ids);
727
728     // The 'status messages' placeholder needs to be special cased, because it
729     // depends on global state that can be modified when other placeholders are
730     // being rendered: any code can add messages to render.
731     // This violates the principle that each lazy builder must be able to render
732     // itself in isolation, and therefore in any order. However, we cannot
733     // change the way drupal_set_message() works in the Drupal 8 cycle. So we
734     // have to accommodate its special needs.
735     // Allowing placeholders to be rendered in a particular order (in this case:
736     // last) would violate this isolation principle. Thus a monopoly is granted
737     // to this one special case, with this hard-coded solution.
738     // @see \Drupal\Core\Render\Element\StatusMessages
739     // @see \Drupal\Core\Render\Renderer::replacePlaceholders()
740     // @see https://www.drupal.org/node/2712935#comment-11368923
741     $message_placeholder_ids = [];
742     foreach ($placeholders as $placeholder_id => $placeholder_element) {
743       if (isset($placeholder_element['#lazy_builder']) && $placeholder_element['#lazy_builder'][0] === 'Drupal\Core\Render\Element\StatusMessages::renderMessages') {
744         $message_placeholder_ids[] = $placeholder_id;
745       }
746     }
747
748     // Return placeholder IDs in DOM order, but with the 'status messages'
749     // placeholders at the end, if they are present.
750     $ordered_placeholder_ids = array_merge(
751       array_diff($placeholder_ids, $message_placeholder_ids),
752       array_intersect($placeholder_ids, $message_placeholder_ids)
753     );
754     return $ordered_placeholder_ids;
755   }
756
757 }