— this contains BigPipe * placeholders for the personalized parts of the page. Hence this sends the * non-personalized parts of the page. Let's call it The Skeleton. * 2. N chunks: a '; /** * The BigPipe placeholder replacements stop signal. * * @var string */ const STOP_SIGNAL = ''; /** * The renderer. * * @var \Drupal\Core\Render\RendererInterface */ protected $renderer; /** * The session. * * @var \Symfony\Component\HttpFoundation\Session\SessionInterface */ protected $session; /** * The request stack. * * @var \Symfony\Component\HttpFoundation\RequestStack */ protected $requestStack; /** * The HTTP kernel. * * @var \Symfony\Component\HttpKernel\HttpKernelInterface */ protected $httpKernel; /** * The event dispatcher. * * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface */ protected $eventDispatcher; /** * The config factory. * * @var \Drupal\Core\Config\ConfigFactoryInterface */ protected $configFactory; /** * Constructs a new BigPipe class. * * @param \Drupal\Core\Render\RendererInterface $renderer * The renderer. * @param \Symfony\Component\HttpFoundation\Session\SessionInterface $session * The session. * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack * The request stack. * @param \Symfony\Component\HttpKernel\HttpKernelInterface $http_kernel * The HTTP kernel. * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher * The event dispatcher. * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory * The config factory. */ public function __construct(RendererInterface $renderer, SessionInterface $session, RequestStack $request_stack, HttpKernelInterface $http_kernel, EventDispatcherInterface $event_dispatcher, ConfigFactoryInterface $config_factory) { $this->renderer = $renderer; $this->session = $session; $this->requestStack = $request_stack; $this->httpKernel = $http_kernel; $this->eventDispatcher = $event_dispatcher; $this->configFactory = $config_factory; } /** * Performs tasks before sending content (and rendering placeholders). */ protected function performPreSendTasks() { // The content in the placeholders may depend on the session, and by the // time the response is sent (see index.php), the session is already // closed. Reopen it for the duration that we are rendering placeholders. $this->session->start(); } /** * Performs tasks after sending content (and rendering placeholders). */ protected function performPostSendTasks() { // Close the session again. $this->session->save(); } /** * Sends a chunk. * * @param string|\Drupal\Core\Render\HtmlResponse $chunk * The string or response to append. String if there's no cacheability * metadata or attachments to merge. */ protected function sendChunk($chunk) { assert(is_string($chunk) || $chunk instanceof HtmlResponse); if ($chunk instanceof HtmlResponse) { print $chunk->getContent(); } else { print $chunk; } flush(); } /** * Sends an HTML response in chunks using the BigPipe technique. * * @param \Drupal\big_pipe\Render\BigPipeResponse $response * The BigPipe response to send. * * @internal * This method should only be invoked by * \Drupal\big_pipe\Render\BigPipeResponse, which is itself an internal * class. */ public function sendContent(BigPipeResponse $response) { $content = $response->getContent(); $attachments = $response->getAttachments(); // First, gather the BigPipe placeholders that must be replaced. $placeholders = isset($attachments['big_pipe_placeholders']) ? $attachments['big_pipe_placeholders'] : []; $nojs_placeholders = isset($attachments['big_pipe_nojs_placeholders']) ? $attachments['big_pipe_nojs_placeholders'] : []; // BigPipe sends responses using "Transfer-Encoding: chunked". To avoid // sending already-sent assets, it is necessary to track cumulative assets // from all previously rendered/sent chunks. // @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.41 $cumulative_assets = AttachedAssets::createFromRenderArray(['#attached' => $attachments]); $cumulative_assets->setAlreadyLoadedLibraries($attachments['library']); $this->performPreSendTasks(); // Find the closing tag and get the strings before and after. But be // careful to use the latest occurrence of the string "", to ensure // that strings in inline JavaScript or CDATA sections aren't used instead. $parts = explode('', $content); $post_body = array_pop($parts); $pre_body = implode('', $parts); $this->sendPreBody($pre_body, $nojs_placeholders, $cumulative_assets); $this->sendPlaceholders($placeholders, $this->getPlaceholderOrder($pre_body, $placeholders), $cumulative_assets); $this->sendPostBody($post_body); $this->performPostSendTasks(); } /** * Sends everything until just before . * * @param string $pre_body * The HTML response's content until the closing tag. * @param array $no_js_placeholders * The no-JS BigPipe placeholders. * @param \Drupal\Core\Asset\AttachedAssetsInterface $cumulative_assets * The cumulative assets sent so far; to be updated while rendering no-JS * BigPipe placeholders. */ protected function sendPreBody($pre_body, array $no_js_placeholders, AttachedAssetsInterface $cumulative_assets) { // If there are no no-JS BigPipe placeholders, we can send the pre- // part of the page immediately. if (empty($no_js_placeholders)) { $this->sendChunk($pre_body); return; } // Extract the scripts_bottom markup: the no-JS BigPipe placeholders that we // will render may attach additional asset libraries, and if so, it will be // necessary to re-render scripts_bottom. list($pre_scripts_bottom, $scripts_bottom, $post_scripts_bottom) = explode('', $pre_body, 3); $cumulative_assets_initial = clone $cumulative_assets; $this->sendNoJsPlaceholders($pre_scripts_bottom . $post_scripts_bottom, $no_js_placeholders, $cumulative_assets); // If additional asset libraries or drupalSettings were attached by any of // the placeholders, then we need to re-render scripts_bottom. if ($cumulative_assets_initial != $cumulative_assets) { // Create a new HtmlResponse. Ensure the CSS and (non-bottom) JS is sent // before the HTML they're associated with. // @see \Drupal\Core\Render\HtmlResponseSubscriber // @see template_preprocess_html() $js_bottom_placeholder = ''; $html_response = new HtmlResponse(); $html_response->setContent([ '#markup' => BigPipeMarkup::create($js_bottom_placeholder), '#attached' => [ 'drupalSettings' => $cumulative_assets->getSettings(), 'library' => $cumulative_assets->getAlreadyLoadedLibraries(), 'html_response_attachment_placeholders' => [ 'scripts_bottom' => $js_bottom_placeholder, ], ], ]); $html_response->getCacheableMetadata()->setCacheMaxAge(0); // Push a fake request with the asset libraries loaded so far and dispatch // KernelEvents::RESPONSE event. This results in the attachments for the // HTML response being processed by HtmlResponseAttachmentsProcessor and // hence the HTML to load the bottom JavaScript can be rendered. $fake_request = $this->requestStack->getMasterRequest()->duplicate(); $html_response = $this->filterEmbeddedResponse($fake_request, $html_response); $scripts_bottom = $html_response->getContent(); } $this->sendChunk($scripts_bottom); } /** * Sends no-JS BigPipe placeholders' replacements as embedded HTML responses. * * @param string $html * HTML markup. * @param array $no_js_placeholders * Associative array; the no-JS BigPipe placeholders. Keys are the BigPipe * selectors. * @param \Drupal\Core\Asset\AttachedAssetsInterface $cumulative_assets * The cumulative assets sent so far; to be updated while rendering no-JS * BigPipe placeholders. * * @throws \Exception * If an exception is thrown during the rendering of a placeholder, it is * caught to allow the other placeholders to still be replaced. But when * error logging is configured to be verbose, the exception is rethrown to * simplify debugging. */ protected function sendNoJsPlaceholders($html, $no_js_placeholders, AttachedAssetsInterface $cumulative_assets) { // Split the HTML on every no-JS placeholder string. $prepare_for_preg_split = function ($placeholder_string) { return '(' . preg_quote($placeholder_string, '/') . ')'; }; $preg_placeholder_strings = array_map($prepare_for_preg_split, array_keys($no_js_placeholders)); $fragments = preg_split('/' . implode('|', $preg_placeholder_strings) . '/', $html, NULL, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE); // Determine how many occurrences there are of each no-JS placeholder. $placeholder_occurrences = array_count_values(array_intersect($fragments, array_keys($no_js_placeholders))); // Set up a variable to store the content of placeholders that have multiple // occurrences. $multi_occurrence_placeholders_content = []; foreach ($fragments as $fragment) { // If the fragment isn't one of the no-JS placeholders, it is the HTML in // between placeholders and it must be printed & flushed immediately. The // rest of the logic in the loop handles the placeholders. if (!isset($no_js_placeholders[$fragment])) { $this->sendChunk($fragment); continue; } // If there are multiple occurrences of this particular placeholder, and // this is the second occurrence, we can skip all calculations and just // send the same content. if ($placeholder_occurrences[$fragment] > 1 && isset($multi_occurrence_placeholders_content[$fragment])) { $this->sendChunk($multi_occurrence_placeholders_content[$fragment]); continue; } $placeholder = $fragment; assert(isset($no_js_placeholders[$placeholder])); $token = Crypt::randomBytesBase64(55); // Render the placeholder, but include the cumulative settings assets, so // we can calculate the overall settings for the entire page. $placeholder_plus_cumulative_settings = [ 'placeholder' => $no_js_placeholders[$placeholder], 'cumulative_settings_' . $token => [ '#attached' => [ 'drupalSettings' => $cumulative_assets->getSettings(), ], ], ]; try { $elements = $this->renderPlaceholder($placeholder, $placeholder_plus_cumulative_settings); } catch (\Exception $e) { if ($this->configFactory->get('system.logging')->get('error_level') === ERROR_REPORTING_DISPLAY_VERBOSE) { throw $e; } else { trigger_error($e, E_USER_ERROR); continue; } } // Create a new HtmlResponse. Ensure the CSS and (non-bottom) JS is sent // before the HTML they're associated with. In other words: ensure the // critical assets for this placeholder's markup are loaded first. // @see \Drupal\Core\Render\HtmlResponseSubscriber // @see template_preprocess_html() $css_placeholder = ''; $js_placeholder = ''; $elements['#markup'] = BigPipeMarkup::create($css_placeholder . $js_placeholder . (string) $elements['#markup']); $elements['#attached']['html_response_attachment_placeholders']['styles'] = $css_placeholder; $elements['#attached']['html_response_attachment_placeholders']['scripts'] = $js_placeholder; $html_response = new HtmlResponse(); $html_response->setContent($elements); $html_response->getCacheableMetadata()->setCacheMaxAge(0); // Push a fake request with the asset libraries loaded so far and dispatch // KernelEvents::RESPONSE event. This results in the attachments for the // HTML response being processed by HtmlResponseAttachmentsProcessor and // hence: // - the HTML to load the CSS can be rendered. // - the HTML to load the JS (at the top) can be rendered. $fake_request = $this->requestStack->getMasterRequest()->duplicate(); $fake_request->request->set('ajax_page_state', ['libraries' => implode(',', $cumulative_assets->getAlreadyLoadedLibraries())]); try { $html_response = $this->filterEmbeddedResponse($fake_request, $html_response); } catch (\Exception $e) { if ($this->configFactory->get('system.logging')->get('error_level') === ERROR_REPORTING_DISPLAY_VERBOSE) { throw $e; } else { trigger_error($e, E_USER_ERROR); continue; } } // Send this embedded HTML response. $this->sendChunk($html_response); // Another placeholder was rendered and sent, track the set of asset // libraries sent so far. Any new settings also need to be tracked, so // they can be sent in ::sendPreBody(). $cumulative_assets->setAlreadyLoadedLibraries(array_merge($cumulative_assets->getAlreadyLoadedLibraries(), $html_response->getAttachments()['library'])); $cumulative_assets->setSettings($html_response->getAttachments()['drupalSettings']); // If there are multiple occurrences of this particular placeholder, track // the content that was sent, so we can skip all calculations for the next // occurrence. if ($placeholder_occurrences[$fragment] > 1) { $multi_occurrence_placeholders_content[$fragment] = $html_response->getContent(); } } } /** * Sends BigPipe placeholders' replacements as embedded AJAX responses. * * @param array $placeholders * Associative array; the BigPipe placeholders. Keys are the BigPipe * placeholder IDs. * @param array $placeholder_order * Indexed array; the order in which the BigPipe placeholders must be sent. * Values are the BigPipe placeholder IDs. (These values correspond to keys * in $placeholders.) * @param \Drupal\Core\Asset\AttachedAssetsInterface $cumulative_assets * The cumulative assets sent so far; to be updated while rendering BigPipe * placeholders. * * @throws \Exception * If an exception is thrown during the rendering of a placeholder, it is * caught to allow the other placeholders to still be replaced. But when * error logging is configured to be verbose, the exception is rethrown to * simplify debugging. */ protected function sendPlaceholders(array $placeholders, array $placeholder_order, AttachedAssetsInterface $cumulative_assets) { // Return early if there are no BigPipe placeholders to send. if (empty($placeholders)) { return; } // Send the start signal. $this->sendChunk("\n" . static::START_SIGNAL . "\n"); // A BigPipe response consists of a HTML response plus multiple embedded // AJAX responses. To process the attachments of those AJAX responses, we // need a fake request that is identical to the master request, but with // one change: it must have the right Accept header, otherwise the work- // around for a bug in IE9 will cause not JSON, but