diff --git a/core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php b/core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php index 7d36b51..80bc7f9 100644 --- a/core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php +++ b/core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php @@ -105,16 +105,19 @@ public function __construct(AssetResolverInterface $asset_resolver, ConfigFactor /** * {@inheritdoc} */ - public function processAttachments(AttachmentsInterface $response) { + public function processAttachments(AttachmentsInterface $response, $ajax_page_state = NULL) { // @todo Convert to assertion once https://www.drupal.org/node/2408013 lands if (!$response instanceof AjaxResponse) { throw new \InvalidArgumentException('\Drupal\Core\Ajax\AjaxResponse instance expected.'); } - $request = $this->requestStack->getCurrentRequest(); - if ($response->getContent() == '{}') { - $response->setData($this->buildAttachmentsCommands($response, $request)); + if (!isset($ajax_page_state)) { + $request = $this->requestStack->getCurrentRequest(); + $ajax_page_state = $request->request->get('ajax_page_state'); + } + + $response->setData($this->buildAttachmentsCommands($response, $ajax_page_state)); } return $response; @@ -125,15 +128,13 @@ public function processAttachments(AttachmentsInterface $response) { * * @param \Drupal\Core\Ajax\AjaxResponse $response * The AJAX response to update. - * @param \Symfony\Component\HttpFoundation\Request $request - * The request object that the AJAX is responding to. + * @param array $ajax_page_state + * The current ajax page state. * * @return array * An array of commands ready to be returned as JSON. */ - protected function buildAttachmentsCommands(AjaxResponse $response, Request $request) { - $ajax_page_state = $request->request->get('ajax_page_state'); - + protected function buildAttachmentsCommands(AjaxResponse $response, array $ajax_page_state = NULL) { // Aggregate CSS/JS if necessary, but only during normal site operation. $optimize_css = !defined('MAINTENANCE_MODE') && $this->config->get('css.preprocess'); $optimize_js = !defined('MAINTENANCE_MODE') && $this->config->get('js.preprocess'); diff --git a/core/lib/Drupal/Core/Asset/LibraryDependencyResolver.php b/core/lib/Drupal/Core/Asset/LibraryDependencyResolver.php index 34ffc0d..f05fbbd 100644 --- a/core/lib/Drupal/Core/Asset/LibraryDependencyResolver.php +++ b/core/lib/Drupal/Core/Asset/LibraryDependencyResolver.php @@ -94,7 +94,7 @@ public function getMinimalRepresentativeSubset(array $libraries) { } } - return $minimal; + return array_unique($minimal); } } diff --git a/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php b/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php index 19edef1..eb13b11 100644 --- a/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php +++ b/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php @@ -123,7 +123,13 @@ public function processAttachments(AttachmentsInterface $response) { $attachment_placeholders = $attached['html_response_attachment_placeholders']; unset($attached['html_response_attachment_placeholders']); - $variables = $this->processAssetLibraries($attached, $attachment_placeholders); + // Take Ajax page state into account, to allow for something like Turbolinks + // to be implemented without altering core. + // @see https://github.com/rails/turbolinks/ + // @todo https://www.drupal.org/node/2497115 - Below line is broken due to ->request. + $ajax_page_state = $this->requestStack->getCurrentRequest()->request->get('ajax_page_state'); + + $variables = $this->processAssetLibraries($attached, $attachment_placeholders, $ajax_page_state); // Handle all non-asset attachments. This populates drupal_get_html_head(). $all_attached = ['#attached' => $attached]; @@ -201,6 +207,8 @@ protected function renderPlaceholders(HtmlResponse $response) { * The attachments to process. * @param array $placeholders * The placeholders that exist in the response. + * @param array $ajax_page_state + * (optional) The ajax page state of the page. * * @return array * An array keyed by asset type, with keys: @@ -208,15 +216,10 @@ protected function renderPlaceholders(HtmlResponse $response) { * - scripts * - scripts_bottom */ - protected function processAssetLibraries(array $attached, array $placeholders) { + public function processAssetLibraries(array $attached, array $placeholders, array $ajax_page_state = NULL) { $all_attached = ['#attached' => $attached]; $assets = AttachedAssets::createFromRenderArray($all_attached); - // Take Ajax page state into account, to allow for something like Turbolinks - // to be implemented without altering core. - // @see https://github.com/rails/turbolinks/ - // @todo https://www.drupal.org/node/2497115 - Below line is broken due to ->request. - $ajax_page_state = $this->requestStack->getCurrentRequest()->request->get('ajax_page_state'); $assets->setAlreadyLoadedLibraries(isset($ajax_page_state) ? explode(',', $ajax_page_state['libraries']) : []); $variables = []; @@ -233,7 +236,13 @@ protected function processAssetLibraries(array $attached, array $placeholders) { // Optimize JS if necessary, but only during normal site operation. $optimize_js = !defined('MAINTENANCE_MODE') && !\Drupal::state()->get('system.maintenance_mode') && $this->config->get('js.preprocess'); list($js_assets_header, $js_assets_footer) = $this->assetResolver->getJsAssets($assets, $optimize_js); + } + + if (isset($placeholders['scripts'])) { $variables['scripts'] = $this->jsCollectionRenderer->render($js_assets_header); + } + + if (isset($placeholders['scripts_bottom'])) { $variables['scripts_bottom'] = $this->jsCollectionRenderer->render($js_assets_footer); } diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php index 9c2876c..5d48203 100644 --- a/core/lib/Drupal/Core/Render/Renderer.php +++ b/core/lib/Drupal/Core/Render/Renderer.php @@ -330,6 +330,7 @@ protected function doRender(&$elements, $is_root_call = FALSE) { '#lazy_builder', '#cache', '#create_placeholder', + '#create_placeholder_options', // These keys are not actually supported, but they are added automatically // by the Renderer, so we don't crash on them; them being missing when // their #lazy_builder callback is invoked won't surprise the developer. @@ -699,6 +700,8 @@ protected function createPlaceholder(array $element) { // The cacheability metadata for the placeholder. The rendered result of // the placeholder may itself be cached, if [#cache][keys] are specified. '#cache' => TRUE, + // The options for creating the placeholder. (optional) + '#create_placeholder_options' => TRUE, ]); // Generate placeholder markup. Note that the only requirement is that this diff --git a/core/modules/big_pipe/big_pipe.module b/core/modules/big_pipe/big_pipe.module index 3bb6aca..724cd1c 100644 --- a/core/modules/big_pipe/big_pipe.module +++ b/core/modules/big_pipe/big_pipe.module @@ -13,6 +13,16 @@ use Drupal\Core\Render\SafeString; /** + * Implements hook_js_settings_alter(). + */ +function big_pipe_js_settings_alter(&$settings) { + // Store the settings for later usage. + if (isset($settings['bigPipeResponseMarker'])) { + \Drupal::service('big_pipe')->setAjaxPageState(isset($settings['ajaxPageState']) ? $settings['ajaxPageState'] : NULL); + } +} + +/** * Implements hook_form_FORM_ID_alter(). */ function big_pipe_form_user_login_form_alter(&$form, FormStateInterface $form_state, $form_id) { diff --git a/core/modules/big_pipe/big_pipe.services.yml b/core/modules/big_pipe/big_pipe.services.yml index 0218138..6e82d5c 100644 --- a/core/modules/big_pipe/big_pipe.services.yml +++ b/core/modules/big_pipe/big_pipe.services.yml @@ -11,4 +11,4 @@ services: arguments: ['@current_user'] big_pipe: class: Drupal\big_pipe\Render\BigPipe - arguments: ['@renderer', '@ajax_response.attachments_processor'] + arguments: ['@renderer', '@ajax_response.attachments_processor', '@html_response.attachments_processor'] diff --git a/core/modules/big_pipe/src/EventSubscriber/HtmlResponseBigPipeSubscriber.php b/core/modules/big_pipe/src/EventSubscriber/HtmlResponseBigPipeSubscriber.php index c9a3f44..1abf0b9 100644 --- a/core/modules/big_pipe/src/EventSubscriber/HtmlResponseBigPipeSubscriber.php +++ b/core/modules/big_pipe/src/EventSubscriber/HtmlResponseBigPipeSubscriber.php @@ -42,6 +42,37 @@ public function __construct(BigPipeInterface $big_pipe) { * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event * The event to process. */ + public function onRespondEarly(FilterResponseEvent $event) { + $response = $event->getResponse(); + if (!$response instanceof HtmlResponse) { + return; + } + + // Set a marker for our alter hook. + $attachments = $response->getAttachments(); + $attachments['drupalSettings']['bigPipeResponseMarker'] = 1; + $response->setAttachments($attachments); + + // Set a marker around 'scripts_bottom' + if (isset($attachments['html_response_attachment_placeholders']['scripts_bottom'])) { + $scripts_bottom_placeholder = $attachments['html_response_attachment_placeholders']['scripts_bottom']; + $content = $response->getContent(); + + // Remove any existing markers. + $content = str_replace('', '', $content); + + // Wrap scripts_bottom placeholder with a marker. + $content = str_replace($scripts_bottom_placeholder, '' . $scripts_bottom_placeholder . '', $content); + $response->setContent($content); + } + } + + /** + * Processes placeholders for HTML responses. + * + * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event + * The event to process. + */ public function onRespond(FilterResponseEvent $event) { if (!$event->isMasterRequest()) { return; @@ -54,6 +85,10 @@ public function onRespond(FilterResponseEvent $event) { $attachments = $response->getAttachments(); if (empty($attachments['big_pipe_placeholders'])) { + // Remove our marker again. + $content = $response->getContent(); + $content = str_replace('', '', $content); + $response->setContent($content); return; } @@ -82,6 +117,8 @@ public function onRespond(FilterResponseEvent $event) { * {@inheritdoc} */ public static function getSubscribedEvents() { + // Run after placeholder strategies. + $events[KernelEvents::RESPONSE][] = ['onRespondEarly', 3]; // Run as pretty much last subscriber. $events[KernelEvents::RESPONSE][] = ['onRespond', -10000]; return $events; diff --git a/core/modules/big_pipe/src/Render/BigPipe.php b/core/modules/big_pipe/src/Render/BigPipe.php index 7261d6d..d374d0f 100644 --- a/core/modules/big_pipe/src/Render/BigPipe.php +++ b/core/modules/big_pipe/src/Render/BigPipe.php @@ -9,6 +9,7 @@ use Drupal\Core\Ajax\AjaxResponse; use Drupal\Core\Ajax\ReplaceCommand; +use Drupal\Core\Render\BubbleableMetadata; use Drupal\Core\Render\RendererInterface; use Drupal\Core\Render\AttachmentsResponseProcessorInterface; @@ -33,22 +34,46 @@ class BigPipe implements BigPipeInterface { protected $ajaxResponseAttachmentsProcessor; /** + * The HTML response attachments processor service. + * + * @var \Drupal\Core\Render\AttachmentsResponseProcessorInterface + */ + protected $htmlResponseAttachmentsProcessor; + + /** + * The current ajax page state. + * + * @var array + */ + protected $ajaxPageState; + + /** * Constructs a new BigPipe class. * * @param \Drupal\Core\Render\RendererInterface * The renderer. * @param \Drupal\Core\Render\AttachmentsResponseProcessorInterface $ajax_response_attachments_processor * The AJAX response attachments processor service. + * @param \Drupal\Core\Render\AttachmentsResponseProcessorInterface $html_response_attachments_processor + * The HTML response attachments processor service. */ - public function __construct(RendererInterface $renderer, AttachmentsResponseProcessorInterface $ajax_response_attachments_processor) { + public function __construct(RendererInterface $renderer, AttachmentsResponseProcessorInterface $ajax_response_attachments_processor, AttachmentsResponseProcessorInterface $html_response_attachments_processor) { $this->renderer = $renderer; $this->ajaxResponseAttachmentsProcessor = $ajax_response_attachments_processor; + $this->htmlResponseAttachmentsProcessor= $html_response_attachments_processor; } /** * {@inheritdoc} */ - public function sendContent($content, array $placeholders) { + public function sendContent($content, $attachments, array $placeholders) { + // Split scripts_bottom section out. + $t = explode('', $content, 3); + assert('count($t) == 3', 'There are exactly three segments.'); + $scripts_bottom = $t[1]; + unset($t[1]); + $content = implode('', $t); + // Split it up in various chunks. $split = ''; if (strpos($content, $split) === FALSE) { @@ -63,42 +88,92 @@ public function sendContent($content, array $placeholders) { // Support streaming on NGINX + php-fpm (nginx >= 1.5.6). header('X-Accel-Buffering: no'); - print $page_parts[0]; + $half_pipe_placeholders = []; + + if (empty($_SESSION['big_pipe_has_js'])) { + $half_pipe_placeholders = $placeholders; + $placeholders = []; + } + + foreach ($placeholders as $key => $placeholder) { + if ($placeholder['#create_placeholder_options']['big_pipe']['renderer'] == 'half_pipe') { + $half_pipe_placeholders[$key] = $placeholder; + unset($placeholders[$key]); + } + } + + if (!empty($half_pipe_placeholders)) { + $extra_attachments = $this->doHalfPipe($page_parts[0], $half_pipe_placeholders); + // Print the extra attachments. + if (!empty($extra_attachments['library']) || !empty($extra_attachments['drupalSettings'])) { + $all_attachments = BubbleableMetadata::mergeAttachments($attachments, $extra_attachments); + + // Update the extra libraries using the Response's ajax page state. + // In the ideal case this will be empty and all libraries have been + // to the bottom js section already. + $variables_extra = $this->htmlResponseAttachmentsProcessor->processAssetLibraries($extra_attachments, [ 'scripts' => 'TRUE', 'styles' => TRUE ], $this->ajaxPageState); + if (!empty($variables_extra['styles'])) { + print $this->renderer->renderRoot($variables_extra['styles']); + } + if (!empty($variables_extra['styles'])) { + print $this->renderer->renderRoot($variables_extra['scripts']); + } + + // Update the bottom attachments with no ajax page state. + $variables = $this->htmlResponseAttachmentsProcessor->processAssetLibraries($all_attachments, [ 'scripts_bottom' => TRUE ]); + $scripts_bottom = $this->renderer->renderRoot($variables['scripts_bottom']); + } + } + else { + print $page_parts[0]; + ob_end_flush(); + } + + // Print the bottom attachments. + print $scripts_bottom; + + flush(); + + if (empty($placeholders)) { + print $split; + print $page_parts[1]; + return; + } // Print a container and the start signal. print "\n"; print '
' . "\n"; print ' ' . "\n"; - ob_end_flush(); flush(); - ksort($placeholders); + // Sort placeholders by the order in which they appear in the markup. + $order = $this->getPlaceholderOrder($content); + + foreach ($order as $placeholder) { + if (!isset($placeholders[$placeholder])) { + continue; + } - foreach ($placeholders as $placeholder => $placeholder_elements) { // Check if the placeholder is present at all. if (strpos($content, $placeholder) === FALSE) { continue; } - // Create elements to process in right format. - $elements = [ - '#markup' => $placeholder, - '#attached' => [ - 'placeholders' => [ - $placeholder => $placeholder_elements, - ], - ], - ]; + $placeholder_elements = $placeholders[$placeholder]; - $elements = $this->renderer->renderPlaceholder($placeholder, $elements); + // Render the placeholder. + $elements = $this->renderPlaceholder($placeholder, $placeholder_elements); + + // Ensure that we update the ajaxPageState again. + $elements['#attached']['drupalSettings']['bigPipeResponseMarker'] = 1; // Create a new AjaxResponse. $response = new AjaxResponse(); $response->addCommand(new ReplaceCommand(sprintf('[data-big-pipe-selector="%s"]', $placeholder), $elements['#markup'])); $response->setAttachments($elements['#attached']); - $this->ajaxResponseAttachmentsProcessor->processAttachments($response); + $this->ajaxResponseAttachmentsProcessor->processAttachments($response, $this->ajaxPageState); // @todo Filter response. $json = $response->getContent(); @@ -123,4 +198,66 @@ public function sendContent($content, array $placeholders) { return $this; } + + public function setAjaxPageState($ajax_page_state) { + $this->ajaxPageState = $ajax_page_state; + } + + protected function renderPlaceholder($placeholder, $placeholder_elements) { + // Create elements to process in right format. + $elements = [ + '#markup' => $placeholder, + '#attached' => [ + 'placeholders' => [ + $placeholder => $placeholder_elements, + ], + ], + ]; + + return $this->renderer->renderPlaceholder($placeholder, $elements); + } + + protected function getPlaceholderOrder($content, $selector = '
', $fragment, 2); + $placeholder = $t[0]; + $order[] = $placeholder; + } + + return $order; + } + + protected function doHalfPipe($content, $placeholders, $selector = '
', $fragment, 2); + $placeholder = $t[0]; + if (!isset($placeholders[$placeholder])) { + continue; + } + + // Render the placeholder. + $elements = $this->renderPlaceholder($placeholder, $placeholders[$placeholder]); + if (!empty($elements['#attached'])) { + $attachments = BubbleableMetadata::mergeAttachments($attachments, $elements['#attached']); + } + + print $elements['#markup']; + print $t[1]; + flush(); + } + + return $attachments; + } + } diff --git a/core/modules/big_pipe/src/Render/BigPipeInterface.php b/core/modules/big_pipe/src/Render/BigPipeInterface.php index 221791d..0adcd17 100644 --- a/core/modules/big_pipe/src/Render/BigPipeInterface.php +++ b/core/modules/big_pipe/src/Render/BigPipeInterface.php @@ -24,6 +24,6 @@ * @param string $content * The content to send. */ - public function sendContent($content, array $placeholders); + public function sendContent($content, $attachments, array $placeholders); } diff --git a/core/modules/big_pipe/src/Render/BigPipeResponse.php b/core/modules/big_pipe/src/Render/BigPipeResponse.php index 168b0d1..bb09370 100644 --- a/core/modules/big_pipe/src/Render/BigPipeResponse.php +++ b/core/modules/big_pipe/src/Render/BigPipeResponse.php @@ -53,7 +53,7 @@ public function setBigPipeService(BigPipeInterface $big_pipe) { * {@inheritdoc} */ public function sendContent() { - $this->bigPipe->sendContent($this->content, $this->bigPipePlaceholders); + $this->bigPipe->sendContent($this->content, $this->getAttachments(), $this->bigPipePlaceholders); return $this; } diff --git a/core/modules/big_pipe/src/Render/Placeholder/BigPipeStrategy.php b/core/modules/big_pipe/src/Render/Placeholder/BigPipeStrategy.php index ad369af..28e7b91 100644 --- a/core/modules/big_pipe/src/Render/Placeholder/BigPipeStrategy.php +++ b/core/modules/big_pipe/src/Render/Placeholder/BigPipeStrategy.php @@ -48,18 +48,13 @@ public function processPlaceholders(array $placeholders) { return $return; } - // @todo Add user.permissions cache context. - if (!$this->currentUser->hasPermission('Use BigPipe placeholder strategy')) { - return $return; - } - // @todo Add 'session' cache context. if (empty($_SESSION['big_pipe_has_js'])) { return $return; } // Ensure placeholders are unique per page. - $token = Crypt::randomBytesBase64(55); + $token = ''; foreach ($placeholders as $placeholder => $placeholder_elements) { // Blacklist some #lazy_builder callbacks. @@ -74,6 +69,13 @@ public function processPlaceholders(array $placeholders) { continue; } } + + $placeholder_elements += [ '#create_placeholder_options' => []]; + $placeholder_elements['#create_placeholder_options'] += [ 'big_pipe' => []]; + $placeholder_elements['#create_placeholder_options']['big_pipe'] += [ + 'renderer' => 'big_pipe', + ]; + $html_placeholder = Html::getId($placeholder . '-' . $token); $return[$placeholder] = [ '#markup' => '
',