Update core 8.3.0

This commit is contained in:
Rob Davies 2017-04-13 15:53:35 +01:00
parent da7a7918f8
commit cd7a898e66
6144 changed files with 132297 additions and 87747 deletions

View file

@ -1,6 +1,6 @@
name: BigPipe
type: module
description: 'Sends pages using the BigPipe technique that allows browsers to show them much faster.'
package: Core (Experimental)
package: Core
version: VERSION
core: 8.x

View file

@ -58,8 +58,7 @@ function big_pipe_page_attachments(array &$page) {
'#noscript' => TRUE,
'#attributes' => [
'http-equiv' => 'Refresh',
// @todo: Switch to Url::fromRoute() once https://www.drupal.org/node/2589967 is resolved.
'content' => '0; URL=' . Url::fromUri('internal:/big_pipe/no-js', ['query' => \Drupal::service('redirect.destination')->getAsArray()])->toString(),
'content' => '0; URL=' . Url::fromRoute('big_pipe.nojs', [], ['query' => \Drupal::service('redirect.destination')->getAsArray()])->toString(),
],
],
'big_pipe_detect_nojs',

View file

@ -3,7 +3,7 @@
namespace Drupal\big_pipe\EventSubscriber;
use Drupal\Core\Render\HtmlResponse;
use Drupal\big_pipe\Render\BigPipeInterface;
use Drupal\big_pipe\Render\BigPipe;
use Drupal\big_pipe\Render\BigPipeResponse;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
@ -12,7 +12,7 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Response subscriber to replace the HtmlResponse with a BigPipeResponse.
*
* @see \Drupal\big_pipe\Render\BigPipeInterface
* @see \Drupal\big_pipe\Render\BigPipe
*
* @todo Refactor once https://www.drupal.org/node/2577631 lands.
*/
@ -21,17 +21,17 @@ class HtmlResponseBigPipeSubscriber implements EventSubscriberInterface {
/**
* The BigPipe service.
*
* @var \Drupal\big_pipe\Render\BigPipeInterface
* @var \Drupal\big_pipe\Render\BigPipe
*/
protected $bigPipe;
/**
* Constructs a HtmlResponseBigPipeSubscriber object.
*
* @param \Drupal\big_pipe\Render\BigPipeInterface $big_pipe
* @param \Drupal\big_pipe\Render\BigPipe $big_pipe
* The BigPipe service.
*/
public function __construct(BigPipeInterface $big_pipe) {
public function __construct(BigPipe $big_pipe) {
$this->bigPipe = $big_pipe;
}
@ -91,38 +91,24 @@ class HtmlResponseBigPipeSubscriber implements EventSubscriberInterface {
return;
}
$big_pipe_response = new BigPipeResponse();
$big_pipe_response->setBigPipeService($this->bigPipe);
// Clone the HtmlResponse's data into the new BigPipeResponse.
$big_pipe_response->headers = clone $response->headers;
$big_pipe_response
->setStatusCode($response->getStatusCode())
->setContent($response->getContent())
->setAttachments($attachments)
->addCacheableDependency($response->getCacheableMetadata());
// A BigPipe response can never be cached, because it is intended for a
// single user.
// @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.1
$big_pipe_response->setPrivate();
// Inform surrogates how they should handle BigPipe responses:
// - "no-store" specifies that the response should not be stored in cache;
// it is only to be used for the original request
// - "content" identifies what processing surrogates should perform on the
// response before forwarding it. We send, "BigPipe/1.0", which surrogates
// should not process at all, and in fact, they should not even buffer it
// at all.
// @see http://www.w3.org/TR/edge-arch/
$big_pipe_response->headers->set('Surrogate-Control', 'no-store, content="BigPipe/1.0"');
// Add header to support streaming on NGINX + php-fpm (nginx >= 1.5.6).
$big_pipe_response->headers->set('X-Accel-Buffering', 'no');
$big_pipe_response = new BigPipeResponse($response);
$big_pipe_response->setBigPipeService($this->getBigPipeService($event));
$event->setResponse($big_pipe_response);
}
/**
* Returns the BigPipe service to use to send the current response.
*
* @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
* A response event.
*
* @return \Drupal\big_pipe\Render\BigPipe
* The BigPipe service.
*/
protected function getBigPipeService(FilterResponseEvent $event) {
return $this->bigPipe;
}
/**
* {@inheritdoc}
*/

View file

@ -40,7 +40,7 @@ class NoBigPipeRouteAlterSubscriber implements EventSubscriberInterface {
/**
* {@inheritdoc}
*/
static function getSubscribedEvents() {
public static function getSubscribedEvents() {
$events[RoutingEvents::ALTER][] = ['onRoutingRouteAlterSetNoBigPipe'];
return $events;
}

View file

@ -21,9 +21,133 @@ use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* The default BigPipe service.
* Service for sending an HTML response in chunks (to get faster page loads).
*
* At a high level, BigPipe sends a HTML response in chunks:
* 1. one chunk: everything until just before </body> 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 <script> tag per BigPipe placeholder in The Skeleton.
* 3. one chunk: </body> and everything after it.
*
* This is conceptually identical to Facebook's BigPipe (hence the name).
*
* @see https://www.facebook.com/notes/facebook-engineering/bigpipe-pipelining-web-pages-for-high-performance/389414033919
*
* The major way in which Drupal differs from Facebook's implementation (and
* others) is in its ability to automatically figure out which parts of the page
* can benefit from BigPipe-style delivery. Drupal's render system has the
* concept of "auto-placeholdering": content that is too dynamic is replaced
* with a placeholder that can then be rendered at a later time. On top of that,
* it also has the concept of "placeholder strategies": by default, placeholders
* are replaced on the server side and the response is blocked on all of them
* being replaced. But it's possible to add additional placeholder strategies.
* BigPipe is just another placeholder strategy. Others could be ESI, AJAX
*
* @see https://www.drupal.org/developing/api/8/render/arrays/cacheability/auto-placeholdering
* @see \Drupal\Core\Render\PlaceholderGeneratorInterface::shouldAutomaticallyPlaceholder()
* @see \Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface
* @see \Drupal\Core\Render\Placeholder\SingleFlushStrategy
* @see \Drupal\big_pipe\Render\Placeholder\BigPipeStrategy
*
* There is also one noteworthy technical addition that Drupal makes. BigPipe as
* described above, and as implemented by Facebook, can only work if JavaScript
* is enabled. The BigPipe module also makes it possible to replace placeholders
* using BigPipe in-situ, without JavaScript. This is not technically BigPipe at
* all; it's just the use of multiple flushes. Since it is able to reuse much of
* the logic though, we choose to call this "no-JS BigPipe".
*
* However, there is also a tangible benefit: some dynamic/expensive content is
* not HTML, but for example a HTML attribute value (or part thereof). It's not
* possible to efficiently replace such content using JavaScript, so "classic"
* BigPipe is out of the question. For example: CSRF tokens in URLs.
*
* This allows us to use both no-JS BigPipe and "classic" BigPipe in the same
* response to maximize the amount of content we can send as early as possible.
*
* Finally, a closer look at the implementation, and how it supports and reuses
* existing Drupal concepts:
* 1. BigPipe placeholders: 1 HtmlResponse + N embedded AjaxResponses.
* - Before a BigPipe response is sent, it is just a HTML response that
* contains BigPipe placeholders. Those placeholders look like
* <span data-big-pipe-placeholder-id=""></span>. JavaScript is used to
* replace those placeholders.
* Therefore these placeholders are actually sent to the client.
* - The Skeleton of course has attachments, including most notably asset
* libraries. And those we track in drupalSettings.ajaxPageState.libraries
* so that when we load new content through AJAX, we don't load the same
* asset libraries again. A HTML page can have multiple AJAX responses, each
* of which should take into account the combined AJAX page state of the
* HTML document and all preceding AJAX responses.
* - BigPipe does not make use of multiple AJAX requests/responses. It uses a
* single HTML response. But it is a more long-lived one: The Skeleton is
* sent first, the closing </body> tag is not yet sent, and the connection
* is kept open. Whenever another BigPipe Placeholder is rendered, Drupal
* sends (and so actually appends to the already-sent HTML) something like
* <script type="application/vnd.drupal-ajax">[{"command":"settings","settings":{}}, {"command":}.
* - So, for every BigPipe placeholder, we send such a <script
* type="application/vnd.drupal-ajax"> tag. And the contents of that tag is
* exactly like an AJAX response. The BigPipe module has JavaScript that
* listens for these and applies them. Let's call it an Embedded AJAX
* Response (since it is embedded in the HTML response). Now for the
* interesting bit: each of those Embedded AJAX Responses must also take
* into account the cumulative AJAX page state of the HTML document and all
* preceding Embedded AJAX responses.
* 2. No-JS BigPipe placeholders: 1 HtmlResponse + N embedded HtmlResponses.
* - Before a BigPipe response is sent, it is just a HTML response that
* contains no-JS BigPipe placeholders. Those placeholders can take two
* different forms:
* 1. <span data-big-pipe-nojs-placeholder-id=""></span> if it's a
* placeholder that will be replaced by HTML
* 2. big_pipe_nojs_placeholder_attribute_safe: if it's a placeholder
* inside a HTML attribute, in which 1. would be invalid (angle brackets
* are not allowed inside HTML attributes)
* No-JS BigPipe placeholders are not replaced using JavaScript, they must
* be replaced upon sending the BigPipe response. So, while the response is
* being sent, upon encountering these placeholders, their corresponding
* placeholder replacements are sent instead.
* Therefore these placeholders are never actually sent to the client.
* - See second bullet of point 1.
* - No-JS BigPipe does not use multiple AJAX requests/responses. It uses a
* single HTML response. But it is a more long-lived one: The Skeleton is
* split into multiple parts, the separators are where the no-JS BigPipe
* placeholders used to be. Whenever another no-JS BigPipe placeholder is
* rendered, Drupal sends (and so actually appends to the already-sent HTML)
* something like
* <link rel="stylesheet" ><script ><content>.
* - So, for every no-JS BigPipe placeholder, we send its associated CSS and
* header JS that has not already been sent (the bottom JS is not yet sent,
* so we can accumulate all of it and send it together at the end). This
* ensures that the markup is rendered as it was originally intended: its
* CSS and JS used to be blocking, and it still is. Let's call it an
* Embedded HTML response. Each of those Embedded HTML Responses must also
* take into account the cumulative AJAX page state of the HTML document and
* all preceding Embedded HTML responses.
* - Finally: any non-critical JavaScript associated with all Embedded HTML
* Responses, i.e. any footer/bottom/non-header JavaScript, is loaded after
* The Skeleton.
*
* Combining all of the above, when using both BigPipe placeholders and no-JS
* BigPipe placeholders, we therefore send: 1 HtmlResponse + M Embedded HTML
* Responses + N Embedded AJAX Responses. Schematically, we send these chunks:
* 1. Byte zero until 1st no-JS placeholder: headers + <html><head /><span></span>
* 2. 1st no-JS placeholder replacement: <link rel="stylesheet" ><script ><content>
* 3. Content until 2nd no-JS placeholder: <span></span>
* 4. 2nd no-JS placeholder replacement: <link rel="stylesheet" ><script ><content>
* 5. Content until 3rd no-JS placeholder: <span></span>
* 6. [ repeat until all no-JS placeholder replacements are sent ]
* 7. Send content after last no-JS placeholder.
* 8. Send script_bottom (markup to load bottom i.e. non-critical JS).
* 9. 1st placeholder replacement: <script type="application/vnd.drupal-ajax">[{"command":"settings","settings":{}}, {"command":}
* 10. 2nd placeholder replacement: <script type="application/vnd.drupal-ajax">[{"command":"settings","settings":{}}, {"command":}
* 11. [ repeat until all placeholder replacements are sent ]
* 12. Send </body> and everything after it.
* 13. Terminate request/response cycle.
*
* @see \Drupal\big_pipe\EventSubscriber\HtmlResponseBigPipeSubscriber
* @see \Drupal\big_pipe\Render\Placeholder\BigPipeStrategy
*/
class BigPipe implements BigPipeInterface {
class BigPipe {
/**
* The BigPipe placeholder replacements start signal.
@ -107,9 +231,56 @@ class BigPipe implements BigPipeInterface {
}
/**
* {@inheritdoc}
* Performs tasks before sending content (and rendering placeholders).
*/
public function sendContent($content, array $attachments) {
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'] : [];
@ -121,10 +292,7 @@ class BigPipe implements BigPipeInterface {
$cumulative_assets = AttachedAssets::createFromRenderArray(['#attached' => $attachments]);
$cumulative_assets->setAlreadyLoadedLibraries($attachments['library']);
// 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();
$this->performPreSendTasks();
// Find the closing </body> tag and get the strings before and after. But be
// careful to use the latest occurrence of the string "</body>", to ensure
@ -137,10 +305,7 @@ class BigPipe implements BigPipeInterface {
$this->sendPlaceholders($placeholders, $this->getPlaceholderOrder($pre_body, $placeholders), $cumulative_assets);
$this->sendPostBody($post_body);
// Close the session again.
$this->session->save();
return $this;
$this->performPostSendTasks();
}
/**
@ -158,8 +323,7 @@ class BigPipe implements BigPipeInterface {
// If there are no no-JS BigPipe placeholders, we can send the pre-</body>
// part of the page immediately.
if (empty($no_js_placeholders)) {
print $pre_body;
flush();
$this->sendChunk($pre_body);
return;
}
@ -202,8 +366,7 @@ class BigPipe implements BigPipeInterface {
$scripts_bottom = $html_response->getContent();
}
print $scripts_bottom;
flush();
$this->sendChunk($scripts_bottom);
}
/**
@ -244,8 +407,7 @@ class BigPipe implements BigPipeInterface {
// 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])) {
print $fragment;
flush();
$this->sendChunk($fragment);
continue;
}
@ -253,8 +415,7 @@ class BigPipe implements BigPipeInterface {
// 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])) {
print $multi_occurrence_placeholders_content[$fragment];
flush();
$this->sendChunk($multi_occurrence_placeholders_content[$fragment]);
continue;
}
@ -324,8 +485,7 @@ class BigPipe implements BigPipeInterface {
// Send this embedded HTML response.
print $html_response->getContent();
flush();
$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
@ -369,10 +529,7 @@ class BigPipe implements BigPipeInterface {
}
// Send the start signal.
print "\n";
print static::START_SIGNAL;
print "\n";
flush();
$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
@ -444,8 +601,7 @@ class BigPipe implements BigPipeInterface {
$json
</script>
EOF;
print $output;
flush();
$this->sendChunk($output);
// Another placeholder was rendered and sent, track the set of asset
// libraries sent so far. Any new settings are already sent; we don't need
@ -456,10 +612,7 @@ EOF;
}
// Send the stop signal.
print "\n";
print static::STOP_SIGNAL;
print "\n";
flush();
$this->sendChunk("\n" . static::STOP_SIGNAL . "\n");
}
/**
@ -479,8 +632,28 @@ EOF;
*/
protected function filterEmbeddedResponse(Request $fake_request, Response $embedded_response) {
assert('$embedded_response instanceof \Drupal\Core\Render\HtmlResponse || $embedded_response instanceof \Drupal\Core\Ajax\AjaxResponse');
$this->requestStack->push($fake_request);
$event = new FilterResponseEvent($this->httpKernel, $fake_request, HttpKernelInterface::SUB_REQUEST, $embedded_response);
return $this->filterResponse($fake_request, HttpKernelInterface::SUB_REQUEST, $embedded_response);
}
/**
* Filters the given response.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request for which a response is being sent.
* @param int $request_type
* The request type. Can either be
* \Symfony\Component\HttpKernel\HttpKernelInterface::MASTER_REQUEST or
* \Symfony\Component\HttpKernel\HttpKernelInterface::SUB_REQUEST.
* @param \Symfony\Component\HttpFoundation\Response $response
* The response to filter.
*
* @return \Symfony\Component\HttpFoundation\Response
* The filtered response.
*/
protected function filterResponse(Request $request, $request_type, Response $response) {
assert('$request_type === \Symfony\Component\HttpKernel\HttpKernelInterface::MASTER_REQUEST || $request_type === \Symfony\Component\HttpKernel\HttpKernelInterface::SUB_REQUEST');
$this->requestStack->push($request);
$event = new FilterResponseEvent($this->httpKernel, $request, $request_type, $response);
$this->eventDispatcher->dispatch(KernelEvents::RESPONSE, $event);
$filtered_response = $event->getResponse();
$this->requestStack->pop();
@ -494,9 +667,7 @@ EOF;
* The HTML response's content after the closing </body> tag.
*/
protected function sendPostBody($post_body) {
print '</body>';
print $post_body;
flush();
$this->sendChunk('</body>' . $post_body);
}
/**
@ -545,12 +716,12 @@ EOF;
* only keep the first occurrence.
*/
protected function getPlaceholderOrder($html, $placeholders) {
$fragments = explode('<div data-big-pipe-placeholder-id="', $html);
$fragments = explode('<span data-big-pipe-placeholder-id="', $html);
array_shift($fragments);
$placeholder_ids = [];
foreach ($fragments as $fragment) {
$t = explode('"></div>', $fragment, 2);
$t = explode('"></span>', $fragment, 2);
$placeholder_id = $t[0];
$placeholder_ids[] = $placeholder_id;
}

View file

@ -1,150 +0,0 @@
<?php
namespace Drupal\big_pipe\Render;
/**
* Interface for sending an HTML response in chunks (to get faster page loads).
*
* At a high level, BigPipe sends a HTML response in chunks:
* 1. one chunk: everything until just before </body> 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 <script> tag per BigPipe placeholder in The Skeleton.
* 3. one chunk: </body> and everything after it.
*
* This is conceptually identical to Facebook's BigPipe (hence the name).
*
* @see https://www.facebook.com/notes/facebook-engineering/bigpipe-pipelining-web-pages-for-high-performance/389414033919
*
* The major way in which Drupal differs from Facebook's implementation (and
* others) is in its ability to automatically figure out which parts of the page
* can benefit from BigPipe-style delivery. Drupal's render system has the
* concept of "auto-placeholdering": content that is too dynamic is replaced
* with a placeholder that can then be rendered at a later time. On top of that,
* it also has the concept of "placeholder strategies": by default, placeholders
* are replaced on the server side and the response is blocked on all of them
* being replaced. But it's possible to add additional placeholder strategies.
* BigPipe is just another placeholder strategy. Others could be ESI, AJAX
*
* @see https://www.drupal.org/developing/api/8/render/arrays/cacheability/auto-placeholdering
* @see \Drupal\Core\Render\PlaceholderGeneratorInterface::shouldAutomaticallyPlaceholder()
* @see \Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface
* @see \Drupal\Core\Render\Placeholder\SingleFlushStrategy
* @see \Drupal\big_pipe\Render\Placeholder\BigPipeStrategy
*
* There is also one noteworthy technical addition that Drupal makes. BigPipe as
* described above, and as implemented by Facebook, can only work if JavaScript
* is enabled. The BigPipe module also makes it possible to replace placeholders
* using BigPipe in-situ, without JavaScript. This is not technically BigPipe at
* all; it's just the use of multiple flushes. Since it is able to reuse much of
* the logic though, we choose to call this "no-JS BigPipe".
*
* However, there is also a tangible benefit: some dynamic/expensive content is
* not HTML, but for example a HTML attribute value (or part thereof). It's not
* possible to efficiently replace such content using JavaScript, so "classic"
* BigPipe is out of the question. For example: CSRF tokens in URLs.
*
* This allows us to use both no-JS BigPipe and "classic" BigPipe in the same
* response to maximize the amount of content we can send as early as possible.
*
* Finally, a closer look at the implementation, and how it supports and reuses
* existing Drupal concepts:
* 1. BigPipe placeholders: 1 HtmlResponse + N embedded AjaxResponses.
* - Before a BigPipe response is sent, it is just a HTML response that
* contains BigPipe placeholders. Those placeholders look like
* <div data-big-pipe-placeholder-id=""></div>. JavaScript is used to
* replace those placeholders.
* Therefore these placeholders are actually sent to the client.
* - The Skeleton of course has attachments, including most notably asset
* libraries. And those we track in drupalSettings.ajaxPageState.libraries
* so that when we load new content through AJAX, we don't load the same
* asset libraries again. A HTML page can have multiple AJAX responses, each
* of which should take into account the combined AJAX page state of the
* HTML document and all preceding AJAX responses.
* - BigPipe does not make use of multiple AJAX requests/responses. It uses a
* single HTML response. But it is a more long-lived one: The Skeleton is
* sent first, the closing </body> tag is not yet sent, and the connection
* is kept open. Whenever another BigPipe Placeholder is rendered, Drupal
* sends (and so actually appends to the already-sent HTML) something like
* <script type="application/vnd.drupal-ajax">[{"command":"settings","settings":{}}, {"command":}.
* - So, for every BigPipe placeholder, we send such a <script
* type="application/vnd.drupal-ajax"> tag. And the contents of that tag is
* exactly like an AJAX response. The BigPipe module has JavaScript that
* listens for these and applies them. Let's call it an Embedded AJAX
* Response (since it is embedded in the HTML response). Now for the
* interesting bit: each of those Embedded AJAX Responses must also take
* into account the cumulative AJAX page state of the HTML document and all
* preceding Embedded AJAX responses.
* 2. No-JS BigPipe placeholders: 1 HtmlResponse + N embedded HtmlResponses.
* - Before a BigPipe response is sent, it is just a HTML response that
* contains no-JS BigPipe placeholders. Those placeholders can take two
* different forms:
* 1. <div data-big-pipe-nojs-placeholder-id=""></div> if it's a
* placeholder that will be replaced by HTML
* 2. big_pipe_nojs_placeholder_attribute_safe: if it's a placeholder
* inside a HTML attribute, in which 1. would be invalid (angle brackets
* are not allowed inside HTML attributes)
* No-JS BigPipe placeholders are not replaced using JavaScript, they must
* be replaced upon sending the BigPipe response. So, while the response is
* being sent, upon encountering these placeholders, their corresponding
* placeholder replacements are sent instead.
* Therefore these placeholders are never actually sent to the client.
* - See second bullet of point 1.
* - No-JS BigPipe does not use multiple AJAX requests/responses. It uses a
* single HTML response. But it is a more long-lived one: The Skeleton is
* split into multiple parts, the separators are where the no-JS BigPipe
* placeholders used to be. Whenever another no-JS BigPipe placeholder is
* rendered, Drupal sends (and so actually appends to the already-sent HTML)
* something like
* <link rel="stylesheet" ><script ><content>.
* - So, for every no-JS BigPipe placeholder, we send its associated CSS and
* header JS that has not already been sent (the bottom JS is not yet sent,
* so we can accumulate all of it and send it together at the end). This
* ensures that the markup is rendered as it was originally intended: its
* CSS and JS used to be blocking, and it still is. Let's call it an
* Embedded HTML response. Each of those Embedded HTML Responses must also
* take into account the cumulative AJAX page state of the HTML document and
* all preceding Embedded HTML responses.
* - Finally: any non-critical JavaScript associated with all Embedded HTML
* Responses, i.e. any footer/bottom/non-header JavaScript, is loaded after
* The Skeleton.
*
* Combining all of the above, when using both BigPipe placeholders and no-JS
* BigPipe placeholders, we therefore send: 1 HtmlResponse + M Embedded HTML
* Responses + N Embedded AJAX Responses. Schematically, we send these chunks:
* 1. Byte zero until 1st no-JS placeholder: headers + <html><head /><div></div>
* 2. 1st no-JS placeholder replacement: <link rel="stylesheet" ><script ><content>
* 3. Content until 2nd no-JS placeholder: <div></div>
* 4. 2nd no-JS placeholder replacement: <link rel="stylesheet" ><script ><content>
* 5. Content until 3rd no-JS placeholder: <div></div>
* 6. [ repeat until all no-JS placeholder replacements are sent ]
* 7. Send content after last no-JS placeholder.
* 8. Send script_bottom (markup to load bottom i.e. non-critical JS).
* 9. 1st placeholder replacement: <script type="application/vnd.drupal-ajax">[{"command":"settings","settings":{}}, {"command":}
* 10. 2nd placeholder replacement: <script type="application/vnd.drupal-ajax">[{"command":"settings","settings":{}}, {"command":}
* 11. [ repeat until all placeholder replacements are sent ]
* 12. Send </body> and everything after it.
* 13. Terminate request/response cycle.
*
* @see \Drupal\big_pipe\EventSubscriber\HtmlResponseBigPipeSubscriber
* @see \Drupal\big_pipe\Render\Placeholder\BigPipeStrategy
*/
interface BigPipeInterface {
/**
* Sends an HTML response in chunks using the BigPipe technique.
*
* @param string $content
* The HTML response content to send.
* @param array $attachments
* The HTML response's attachments.
*
* @internal
* This method should only be invoked by
* \Drupal\big_pipe\Render\BigPipeResponse, which is itself an internal
* class. Furthermore, the signature of this method will change in
* https://www.drupal.org/node/2657684.
*/
public function sendContent($content, array $attachments);
}

View file

@ -11,7 +11,7 @@ use Drupal\Core\Render\HtmlResponse;
* it makes the content inaccessible (hidden behind a callback), which means no
* middlewares are able to modify the content anymore.
*
* @see \Drupal\big_pipe\Render\BigPipeInterface
* @see \Drupal\big_pipe\Render\BigPipe
*
* @internal
* This is a temporary solution until a generic response emitter interface is
@ -23,17 +23,85 @@ class BigPipeResponse extends HtmlResponse {
/**
* The BigPipe service.
*
* @var \Drupal\big_pipe\Render\BigPipeInterface
* @var \Drupal\big_pipe\Render\BigPipe
*/
protected $bigPipe;
/**
* The original HTML response.
*
* Still contains placeholders. Its cacheability metadata and attachments are
* for everything except the placeholders (since those are not yet rendered).
*
* @see \Drupal\Core\Render\StreamedResponseInterface
* @see ::getStreamedResponse()
*
* @var \Drupal\Core\Render\HtmlResponse
*/
protected $originalHtmlResponse;
/**
* Constructs a new BigPipeResponse.
*
* @param \Drupal\Core\Render\HtmlResponse $response
* The original HTML response.
*/
public function __construct(HtmlResponse $response) {
parent::__construct('', $response->getStatusCode(), []);
$this->originalHtmlResponse = $response;
$this->populateBasedOnOriginalHtmlResponse();
}
/**
* Returns the original HTML response.
*
* @return \Drupal\Core\Render\HtmlResponse
* The original HTML response.
*/
public function getOriginalHtmlResponse() {
return $this->originalHtmlResponse;
}
/**
* Populates this BigPipeResponse object based on the original HTML response.
*/
protected function populateBasedOnOriginalHtmlResponse() {
// Clone the HtmlResponse's data into the new BigPipeResponse.
$this->headers = clone $this->originalHtmlResponse->headers;
$this
->setStatusCode($this->originalHtmlResponse->getStatusCode())
->setContent($this->originalHtmlResponse->getContent())
->setAttachments($this->originalHtmlResponse->getAttachments())
->addCacheableDependency($this->originalHtmlResponse->getCacheableMetadata());
// A BigPipe response can never be cached, because it is intended for a
// single user.
// @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.1
$this->setPrivate();
// Inform surrogates how they should handle BigPipe responses:
// - "no-store" specifies that the response should not be stored in cache;
// it is only to be used for the original request
// - "content" identifies what processing surrogates should perform on the
// response before forwarding it. We send, "BigPipe/1.0", which surrogates
// should not process at all, and in fact, they should not even buffer it
// at all.
// @see http://www.w3.org/TR/edge-arch/
$this->headers->set('Surrogate-Control', 'no-store, content="BigPipe/1.0"');
// Add header to support streaming on NGINX + php-fpm (nginx >= 1.5.6).
$this->headers->set('X-Accel-Buffering', 'no');
}
/**
* Sets the BigPipe service to use.
*
* @param \Drupal\big_pipe\Render\BigPipeInterface $big_pipe
* @param \Drupal\big_pipe\Render\BigPipe $big_pipe
* The BigPipe service.
*/
public function setBigPipeService(BigPipeInterface $big_pipe) {
public function setBigPipeService(BigPipe $big_pipe) {
$this->bigPipe = $big_pipe;
}
@ -41,7 +109,12 @@ class BigPipeResponse extends HtmlResponse {
* {@inheritdoc}
*/
public function sendContent() {
$this->bigPipe->sendContent($this->content, $this->getAttachments());
$this->bigPipe->sendContent($this);
// All BigPipe placeholders are processed, so update this response's
// attachments.
unset($this->attachments['big_pipe_placeholders']);
unset($this->attachments['big_pipe_nojs_placeholders']);
return $this;
}

View file

@ -17,7 +17,7 @@ use Symfony\Component\HttpFoundation\RequestStack;
* Processes attachments of HTML responses with BigPipe enabled.
*
* @see \Drupal\Core\Render\HtmlResponseAttachmentsProcessor
* @see \Drupal\big_pipe\Render\BigPipeInterface
* @see \Drupal\big_pipe\Render\BigPipe
*/
class BigPipeResponseAttachmentsProcessor extends HtmlResponseAttachmentsProcessor {

View file

@ -2,6 +2,7 @@
namespace Drupal\big_pipe\Render\Placeholder;
use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface;
@ -55,7 +56,7 @@ use Symfony\Component\HttpFoundation\RequestStack;
* See \Drupal\big_pipe\Render\BigPipe for detailed documentation on how those
* different placeholders are actually replaced.
*
* @see \Drupal\big_pipe\Render\BigPipeInterface
* @see \Drupal\big_pipe\Render\BigPipe
*/
class BigPipeStrategy implements PlaceholderStrategyInterface {
@ -149,7 +150,7 @@ class BigPipeStrategy implements PlaceholderStrategyInterface {
// @see \Drupal\Core\Access\RouteProcessorCsrf::renderPlaceholderCsrfToken()
// @see \Drupal\Core\Form\FormBuilder::renderFormTokenPlaceholder()
// @see \Drupal\Core\Form\FormBuilder::renderPlaceholderFormAction()
if ($placeholder[0] !== '<' || $placeholder !== Html::normalize($placeholder)) {
if (static::placeholderIsAttributeSafe($placeholder)) {
$overridden_placeholders[$placeholder] = static::createBigPipeNoJsPlaceholder($placeholder, $placeholder_elements, TRUE);
}
else {
@ -168,6 +169,21 @@ class BigPipeStrategy implements PlaceholderStrategyInterface {
return $overridden_placeholders;
}
/**
* Determines whether the given placeholder is attribute-safe or not.
*
* @param string $placeholder
* A placeholder.
*
* @return bool
* Whether the placeholder is safe for use in a HTML attribute (in case it's
* a placeholder for a HTML attribute value or a subset of it).
*/
protected static function placeholderIsAttributeSafe($placeholder) {
assert('is_string($placeholder)');
return $placeholder[0] !== '<' || $placeholder !== Html::normalize($placeholder);
}
/**
* Creates a BigPipe JS placeholder.
*
@ -183,7 +199,7 @@ class BigPipeStrategy implements PlaceholderStrategyInterface {
$big_pipe_placeholder_id = static::generateBigPipePlaceholderId($original_placeholder, $placeholder_render_array);
return [
'#markup' => '<div data-big-pipe-placeholder-id="' . Html::escape($big_pipe_placeholder_id) . '"></div>',
'#markup' => '<span data-big-pipe-placeholder-id="' . Html::escape($big_pipe_placeholder_id) . '"></span>',
'#cache' => [
'max-age' => 0,
'contexts' => [
@ -221,7 +237,7 @@ class BigPipeStrategy implements PlaceholderStrategyInterface {
*/
protected static function createBigPipeNoJsPlaceholder($original_placeholder, array $placeholder_render_array, $placeholder_must_be_attribute_safe = FALSE) {
if (!$placeholder_must_be_attribute_safe) {
$big_pipe_placeholder = '<div data-big-pipe-nojs-placeholder-id="' . Html::escape(static::generateBigPipePlaceholderId($original_placeholder, $placeholder_render_array)) . '"></div>';
$big_pipe_placeholder = '<span data-big-pipe-nojs-placeholder-id="' . Html::escape(static::generateBigPipePlaceholderId($original_placeholder, $placeholder_render_array)) . '"></span>';
}
else {
$big_pipe_placeholder = 'big_pipe_nojs_placeholder_attribute_safe:' . Html::escape($original_placeholder);
@ -260,7 +276,7 @@ class BigPipeStrategy implements PlaceholderStrategyInterface {
if (isset($placeholder_render_array['#lazy_builder'])) {
$callback = $placeholder_render_array['#lazy_builder'][0];
$arguments = $placeholder_render_array['#lazy_builder'][1];
$token = hash('crc32b', serialize($placeholder_render_array));
$token = Crypt::hashBase64(serialize($placeholder_render_array));
return UrlHelper::buildQuery(['callback' => $callback, 'args' => $arguments, 'token' => $token]);
}
// When the placeholder's render array is not using a #lazy_builder,

View file

@ -51,7 +51,7 @@ class BigPipePlaceholderTestCases {
// 1. Real-world example of HTML placeholder.
$status_messages = new BigPipePlaceholderTestCase(
['#type' => 'status_messages'],
'<drupal-render-placeholder callback="Drupal\Core\Render\Element\StatusMessages::renderMessages" arguments="0" token="a8c34b5e"></drupal-render-placeholder>',
'<drupal-render-placeholder callback="Drupal\Core\Render\Element\StatusMessages::renderMessages" arguments="0" token="_HAdUpwWmet0TOTe2PSiJuMntExoshbm1kh2wQzzzAA"></drupal-render-placeholder>',
[
'#lazy_builder' => [
'Drupal\Core\Render\Element\StatusMessages::renderMessages',
@ -59,29 +59,29 @@ class BigPipePlaceholderTestCases {
],
]
);
$status_messages->bigPipePlaceholderId = 'callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&amp;args[0]&amp;token=a8c34b5e';
$status_messages->bigPipePlaceholderId = 'callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&amp;args[0]&amp;token=_HAdUpwWmet0TOTe2PSiJuMntExoshbm1kh2wQzzzAA';
$status_messages->bigPipePlaceholderRenderArray = [
'#markup' => '<div data-big-pipe-placeholder-id="callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&amp;args[0]&amp;token=a8c34b5e"></div>',
'#markup' => '<span data-big-pipe-placeholder-id="callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&amp;args[0]&amp;token=_HAdUpwWmet0TOTe2PSiJuMntExoshbm1kh2wQzzzAA"></span>',
'#cache' => $cacheability_depends_on_session_and_nojs_cookie,
'#attached' => [
'library' => ['big_pipe/big_pipe'],
'drupalSettings' => [
'bigPipePlaceholderIds' => [
'callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&args[0]&token=a8c34b5e' => TRUE,
'callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&args[0]&token=_HAdUpwWmet0TOTe2PSiJuMntExoshbm1kh2wQzzzAA' => TRUE,
],
],
'big_pipe_placeholders' => [
'callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&amp;args[0]&amp;token=a8c34b5e' => $status_messages->placeholderRenderArray,
'callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&amp;args[0]&amp;token=_HAdUpwWmet0TOTe2PSiJuMntExoshbm1kh2wQzzzAA' => $status_messages->placeholderRenderArray,
],
],
];
$status_messages->bigPipeNoJsPlaceholder = '<div data-big-pipe-nojs-placeholder-id="callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&amp;args[0]&amp;token=a8c34b5e"></div>';
$status_messages->bigPipeNoJsPlaceholder = '<span data-big-pipe-nojs-placeholder-id="callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&amp;args[0]&amp;token=_HAdUpwWmet0TOTe2PSiJuMntExoshbm1kh2wQzzzAA"></span>';
$status_messages->bigPipeNoJsPlaceholderRenderArray = [
'#markup' => '<div data-big-pipe-nojs-placeholder-id="callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&amp;args[0]&amp;token=a8c34b5e"></div>',
'#markup' => '<span data-big-pipe-nojs-placeholder-id="callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&amp;args[0]&amp;token=_HAdUpwWmet0TOTe2PSiJuMntExoshbm1kh2wQzzzAA"></span>',
'#cache' => $cacheability_depends_on_session_and_nojs_cookie,
'#attached' => [
'big_pipe_nojs_placeholders' => [
'<div data-big-pipe-nojs-placeholder-id="callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&amp;args[0]&amp;token=a8c34b5e"></div>' => $status_messages->placeholderRenderArray,
'<span data-big-pipe-nojs-placeholder-id="callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&amp;args[0]&amp;token=_HAdUpwWmet0TOTe2PSiJuMntExoshbm1kh2wQzzzAA"></span>' => $status_messages->placeholderRenderArray,
],
],
];
@ -109,7 +109,7 @@ class BigPipePlaceholderTestCases {
[
'command' => 'insert',
'method' => 'replaceWith',
'selector' => '[data-big-pipe-placeholder-id="callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&args[0]&token=a8c34b5e"]',
'selector' => '[data-big-pipe-placeholder-id="callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&args[0]&token=_HAdUpwWmet0TOTe2PSiJuMntExoshbm1kh2wQzzzAA"]',
'data' => "\n" . ' <div role="contentinfo" aria-label="Status message" class="messages messages--status">' . "\n" . ' <h2 class="visually-hidden">Status message</h2>' . "\n" . ' Hello from BigPipe!' . "\n" . ' </div>' . "\n ",
'settings' => NULL,
],
@ -225,7 +225,7 @@ class BigPipePlaceholderTestCases {
);
$current_time->bigPipePlaceholderId = 'timecurrent-timetime';
$current_time->bigPipePlaceholderRenderArray = [
'#markup' => '<div data-big-pipe-placeholder-id="timecurrent-timetime"></div>',
'#markup' => '<span data-big-pipe-placeholder-id="timecurrent-timetime"></span>',
'#cache' => $cacheability_depends_on_session_and_nojs_cookie,
'#attached' => [
'library' => ['big_pipe/big_pipe'],
@ -248,13 +248,13 @@ class BigPipePlaceholderTestCases {
'settings' => NULL,
],
];
$current_time->bigPipeNoJsPlaceholder = '<div data-big-pipe-nojs-placeholder-id="timecurrent-timetime"></div>';
$current_time->bigPipeNoJsPlaceholder = '<span data-big-pipe-nojs-placeholder-id="timecurrent-timetime"></span>';
$current_time->bigPipeNoJsPlaceholderRenderArray = [
'#markup' => '<div data-big-pipe-nojs-placeholder-id="timecurrent-timetime"></div>',
'#markup' => '<span data-big-pipe-nojs-placeholder-id="timecurrent-timetime"></span>',
'#cache' => $cacheability_depends_on_session_and_nojs_cookie,
'#attached' => [
'big_pipe_nojs_placeholders' => [
'<div data-big-pipe-nojs-placeholder-id="timecurrent-timetime"></div>' => $current_time->placeholderRenderArray,
'<span data-big-pipe-nojs-placeholder-id="timecurrent-timetime"></span>' => $current_time->placeholderRenderArray,
],
],
];
@ -267,29 +267,29 @@ class BigPipePlaceholderTestCases {
'#lazy_builder' => ['\Drupal\big_pipe_test\BigPipeTestController::exception', ['llamas', 'suck']],
'#create_placeholder' => TRUE,
],
'<drupal-render-placeholder callback="\Drupal\big_pipe_test\BigPipeTestController::exception" arguments="0=llamas&amp;1=suck" token="68a75f1a"></drupal-render-placeholder>',
'<drupal-render-placeholder callback="\Drupal\big_pipe_test\BigPipeTestController::exception" arguments="0=llamas&amp;1=suck" token="uhKFNfT4eF449_W-kDQX8E5z4yHyt0-nSHUlwaGAQeU"></drupal-render-placeholder>',
[
'#lazy_builder' => ['\Drupal\big_pipe_test\BigPipeTestController::exception', ['llamas', 'suck']],
]
);
$exception->bigPipePlaceholderId = 'callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3Aexception&amp;args[0]=llamas&amp;args[1]=suck&amp;token=68a75f1a';
$exception->bigPipePlaceholderId = 'callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3Aexception&amp;args[0]=llamas&amp;args[1]=suck&amp;token=uhKFNfT4eF449_W-kDQX8E5z4yHyt0-nSHUlwaGAQeU';
$exception->bigPipePlaceholderRenderArray = [
'#markup' => '<div data-big-pipe-placeholder-id="callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3Aexception&amp;args[0]=llamas&amp;args[1]=suck&amp;token=68a75f1a"></div>',
'#markup' => '<span data-big-pipe-placeholder-id="callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3Aexception&amp;args[0]=llamas&amp;args[1]=suck&amp;token=uhKFNfT4eF449_W-kDQX8E5z4yHyt0-nSHUlwaGAQeU"></span>',
'#cache' => $cacheability_depends_on_session_and_nojs_cookie,
'#attached' => [
'library' => ['big_pipe/big_pipe'],
'drupalSettings' => [
'bigPipePlaceholderIds' => [
'callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3Aexception&args[0]=llamas&args[1]=suck&token=68a75f1a' => TRUE,
'callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3Aexception&args[0]=llamas&args[1]=suck&token=uhKFNfT4eF449_W-kDQX8E5z4yHyt0-nSHUlwaGAQeU' => TRUE,
],
],
'big_pipe_placeholders' => [
'callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3Aexception&amp;args[0]=llamas&amp;args[1]=suck&amp;token=68a75f1a' => $exception->placeholderRenderArray,
'callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3Aexception&amp;args[0]=llamas&amp;args[1]=suck&amp;token=uhKFNfT4eF449_W-kDQX8E5z4yHyt0-nSHUlwaGAQeU' => $exception->placeholderRenderArray,
],
],
];
$exception->embeddedAjaxResponseCommands = NULL;
$exception->bigPipeNoJsPlaceholder = '<div data-big-pipe-nojs-placeholder-id="callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3Aexception&amp;args[0]=llamas&amp;args[1]=suck&amp;token=68a75f1a"></div>';
$exception->bigPipeNoJsPlaceholder = '<span data-big-pipe-nojs-placeholder-id="callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3Aexception&amp;args[0]=llamas&amp;args[1]=suck&amp;token=uhKFNfT4eF449_W-kDQX8E5z4yHyt0-nSHUlwaGAQeU"></span>';
$exception->bigPipeNoJsPlaceholderRenderArray = [
'#markup' => $exception->bigPipeNoJsPlaceholder,
'#cache' => $cacheability_depends_on_session_and_nojs_cookie,
@ -307,29 +307,29 @@ class BigPipePlaceholderTestCases {
'#lazy_builder' => ['\Drupal\big_pipe_test\BigPipeTestController::responseException', []],
'#create_placeholder' => TRUE,
],
'<drupal-render-placeholder callback="\Drupal\big_pipe_test\BigPipeTestController::responseException" arguments="" token="2a9bd022"></drupal-render-placeholder>',
'<drupal-render-placeholder callback="\Drupal\big_pipe_test\BigPipeTestController::responseException" arguments="" token="PxOHfS_QL-T01NjBgu7Z7I04tIwMp6La5vM-mVxezbU"></drupal-render-placeholder>',
[
'#lazy_builder' => ['\Drupal\big_pipe_test\BigPipeTestController::responseException', []],
]
);
$embedded_response_exception->bigPipePlaceholderId = 'callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3AresponseException&amp;&amp;token=2a9bd022';
$embedded_response_exception->bigPipePlaceholderId = 'callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3AresponseException&amp;&amp;token=PxOHfS_QL-T01NjBgu7Z7I04tIwMp6La5vM-mVxezbU';
$embedded_response_exception->bigPipePlaceholderRenderArray = [
'#markup' => '<div data-big-pipe-placeholder-id="callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3AresponseException&amp;&amp;token=2a9bd022"></div>',
'#markup' => '<span data-big-pipe-placeholder-id="callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3AresponseException&amp;&amp;token=PxOHfS_QL-T01NjBgu7Z7I04tIwMp6La5vM-mVxezbU"></span>',
'#cache' => $cacheability_depends_on_session_and_nojs_cookie,
'#attached' => [
'library' => ['big_pipe/big_pipe'],
'drupalSettings' => [
'bigPipePlaceholderIds' => [
'callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3AresponseException&&token=2a9bd022' => TRUE,
'callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3AresponseException&&token=PxOHfS_QL-T01NjBgu7Z7I04tIwMp6La5vM-mVxezbU' => TRUE,
],
],
'big_pipe_placeholders' => [
'callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3AresponseException&amp;&amp;token=2a9bd022' => $embedded_response_exception->placeholderRenderArray,
'callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3AresponseException&amp;&amp;token=PxOHfS_QL-T01NjBgu7Z7I04tIwMp6La5vM-mVxezbU' => $embedded_response_exception->placeholderRenderArray,
],
],
];
$embedded_response_exception->embeddedAjaxResponseCommands = NULL;
$embedded_response_exception->bigPipeNoJsPlaceholder = '<div data-big-pipe-nojs-placeholder-id="callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3AresponseException&amp;&amp;token=2a9bd022"></div>';
$embedded_response_exception->bigPipeNoJsPlaceholder = '<span data-big-pipe-nojs-placeholder-id="callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3AresponseException&amp;&amp;token=PxOHfS_QL-T01NjBgu7Z7I04tIwMp6La5vM-mVxezbU"></span>';
$embedded_response_exception->bigPipeNoJsPlaceholderRenderArray = [
'#markup' => $embedded_response_exception->bigPipeNoJsPlaceholder,
'#cache' => $cacheability_depends_on_session_and_nojs_cookie,

View file

@ -158,7 +158,9 @@ class BigPipeTest extends WebTestBase {
$this->drupalGet(Url::fromRoute('big_pipe_test'));
$this->assertBigPipeResponseHeadersPresent();
$this->assertNoCacheTag('cache_tag_set_in_lazy_builder');
$this->setCsrfTokenSeedInTestEnvironment();
$cases = $this->getTestCases();
$this->assertBigPipeNoJsPlaceholders([
$cases['edge_case__invalid_html']->bigPipeNoJsPlaceholder => $cases['edge_case__invalid_html']->embeddedHtmlResponse,
@ -236,7 +238,9 @@ class BigPipeTest extends WebTestBase {
$this->drupalGet(Url::fromRoute('big_pipe_test'));
$this->assertBigPipeResponseHeadersPresent();
$this->assertNoCacheTag('cache_tag_set_in_lazy_builder');
$this->setCsrfTokenSeedInTestEnvironment();
$cases = $this->getTestCases();
$this->assertBigPipeNoJsPlaceholders([
$cases['edge_case__invalid_html']->bigPipeNoJsPlaceholder => $cases['edge_case__invalid_html']->embeddedHtmlResponse,
@ -289,7 +293,7 @@ class BigPipeTest extends WebTestBase {
// @see performMetaRefresh()
$this->drupalGet(Url::fromRoute('big_pipe_test_multi_occurrence'));
$big_pipe_placeholder_id = 'callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&amp;args[0]&amp;token=a8c34b5e';
$big_pipe_placeholder_id = 'callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&amp;args[0]&amp;token=_HAdUpwWmet0TOTe2PSiJuMntExoshbm1kh2wQzzzAA';
$expected_placeholder_replacement = '<script type="application/vnd.drupal-ajax" data-big-pipe-replacement-for-placeholder-with-id="' . $big_pipe_placeholder_id . '">';
$this->assertRaw('The count is 1.');
$this->assertNoRaw('The count is 2.');
@ -353,7 +357,7 @@ class BigPipeTest extends WebTestBase {
foreach ($expected_big_pipe_placeholders as $big_pipe_placeholder_id => $expected_ajax_response) {
$this->pass('BigPipe placeholder: ' . $big_pipe_placeholder_id, 'Debug');
// Verify expected placeholder.
$expected_placeholder_html = '<div data-big-pipe-placeholder-id="' . $big_pipe_placeholder_id . '"></div>';
$expected_placeholder_html = '<span data-big-pipe-placeholder-id="' . $big_pipe_placeholder_id . '"></span>';
$this->assertRaw($expected_placeholder_html, 'BigPipe placeholder for placeholder ID "' . $big_pipe_placeholder_id . '" found.');
$pos = strpos($this->getRawContent(), $expected_placeholder_html);
$placeholder_positions[$pos] = $big_pipe_placeholder_id;
@ -402,14 +406,18 @@ class BigPipeTest extends WebTestBase {
}
/**
* @return \Drupal\big_pipe\Tests\BigPipePlaceholderTestCase[]
* Ensures CSRF tokens can be generated for the current user's session.
*/
protected function getTestCases() {
// Ensure we can generate CSRF tokens for the current user's session.
protected function setCsrfTokenSeedInTestEnvironment() {
$session_data = $this->container->get('session_handler.write_safe')->read($this->cookies[$this->getSessionName()]['value']);
$csrf_token_seed = unserialize(explode('_sf2_meta|', $session_data)[1])['s'];
$this->container->get('session_manager.metadata_bag')->setCsrfTokenSeed($csrf_token_seed);
}
/**
* @return \Drupal\big_pipe\Tests\BigPipePlaceholderTestCase[]
*/
protected function getTestCases($has_session = TRUE) {
return BigPipePlaceholderTestCases::cases($this->container, $this->rootUser);
}

View file

@ -4,3 +4,10 @@ big_pipe_regression_test.2678662:
_controller: '\Drupal\big_pipe_regression_test\BigPipeRegressionTestController::regression2678662'
requirements:
_access: 'TRUE'
big_pipe_regression_test.2802923:
path: '/big_pipe_regression_test/2802923'
defaults:
_controller: '\Drupal\big_pipe_regression_test\BigPipeRegressionTestController::regression2802923'
requirements:
_access: 'TRUE'

View file

@ -17,4 +17,30 @@ class BigPipeRegressionTestController {
];
}
/**
* @see \Drupal\Tests\big_pipe\FunctionalJavascript\BigPipeRegressionTest::testMultipleBodies_2678662()
*/
public function regression2802923() {
return [
'#prefix' => BigPipeMarkup::create('<p>Hi, my train will arrive at '),
'time' => [
'#lazy_builder' => [static::class . '::currentTime', []],
'#create_placeholder' => TRUE,
],
'#suffix' => BigPipeMarkup::create(' — will I still be able to catch the connection to the center?</p>'),
];
}
/**
* #lazy_builder callback; builds <time> markup with current time.
*
* @return array
*/
public static function currentTime() {
return [
'#markup' => '<time datetime="' . date('Y-m-d', time()) . '"></time>',
'#cache' => ['max-age' => 0]
];
}
}

View file

@ -14,13 +14,19 @@ class BigPipeTestController {
* @return array
*/
public function test() {
$has_session = \Drupal::service('session_configuration')->hasSession(\Drupal::requestStack()->getMasterRequest());
$build = [];
$cases = BigPipePlaceholderTestCases::cases(\Drupal::getContainer());
// 1. HTML placeholder: status messages. Drupal renders those automatically,
// so all that we need to do in this controller is set a message.
drupal_set_message('Hello from BigPipe!');
if ($has_session) {
// Only set a message if a session already exists, otherwise we always
// trigger a session, which means we can't test no-session requests.
drupal_set_message('Hello from BigPipe!');
}
$build['html'] = $cases['html']->renderArray;
// 2. HTML attribute value placeholder: form action.
@ -98,7 +104,10 @@ class BigPipeTestController {
public static function helloOrYarhar() {
return [
'#markup' => BigPipeMarkup::create('<marquee>Yarhar llamas forever!</marquee>'),
'#cache' => ['max-age' => 0],
'#cache' => [
'max-age' => 0,
'tags' => ['cache_tag_set_in_lazy_builder'],
],
];
}

View file

@ -20,14 +20,14 @@ class BigPipeTestForm extends FormBase {
public function buildForm(array $form, FormStateInterface $form_state) {
$form['#token'] = FALSE;
$form['big_pipe'] = array(
$form['big_pipe'] = [
'#type' => 'checkboxes',
'#title' => $this->t('BigPipe works…'),
'#options' => [
'js' => $this->t('… with JavaScript'),
'nojs' => $this->t('… without JavaScript'),
],
);
];
return $form;
}

View file

@ -184,4 +184,16 @@ JS;
}
}
/**
* Ensure default BigPipe placeholder HTML cannot split paragraphs.
*
* @see https://www.drupal.org/node/2802923
*/
public function testPlaceholderInParagraph_2802923() {
$this->drupalLogin($this->drupalCreateUser());
$this->drupalGet(Url::fromRoute('big_pipe_regression_test.2802923'));
$this->assertJsCondition('document.querySelectorAll(\'p\').length === 1');
}
}

View file

@ -28,17 +28,16 @@ class BigPipeResponseAttachmentsProcessorTest extends UnitTestCase {
* @covers ::processAttachments
*
* @dataProvider nonHtmlResponseProvider
*
* @expectedException \AssertionError
*/
public function testNonHtmlResponse($response_class) {
$big_pipe_response_attachments_processor = $this->createBigPipeResponseAttachmentsProcessor($this->prophesize(AttachmentsResponseProcessorInterface::class));
$non_html_response = new $response_class();
$this->setExpectedException(\AssertionError::class);
$big_pipe_response_attachments_processor->processAttachments($non_html_response);
}
function nonHtmlResponseProvider() {
public function nonHtmlResponseProvider() {
return [
'AjaxResponse, which implements AttachmentsInterface' => [AjaxResponse::class],
'A dummy that implements AttachmentsInterface' => [get_class($this->prophesize(AttachmentsInterface::class)->reveal())],
@ -51,7 +50,7 @@ class BigPipeResponseAttachmentsProcessorTest extends UnitTestCase {
* @dataProvider attachmentsProvider
*/
public function testHtmlResponse(array $attachments) {
$big_pipe_response = new BigPipeResponse('original');
$big_pipe_response = new BigPipeResponse(new HtmlResponse('original'));
$big_pipe_response->setAttachments($attachments);
// This mock is the main expectation of this test: verify that the decorated