Update core 8.3.0
This commit is contained in:
parent
da7a7918f8
commit
cd7a898e66
6144 changed files with 132297 additions and 87747 deletions
|
@ -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}
|
||||
*/
|
||||
|
|
|
@ -40,7 +40,7 @@ class NoBigPipeRouteAlterSubscriber implements EventSubscriberInterface {
|
|||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
static function getSubscribedEvents() {
|
||||
public static function getSubscribedEvents() {
|
||||
$events[RoutingEvents::ALTER][] = ['onRoutingRouteAlterSetNoBigPipe'];
|
||||
return $events;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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&args[0]&token=a8c34b5e';
|
||||
$status_messages->bigPipePlaceholderId = 'callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&args[0]&token=_HAdUpwWmet0TOTe2PSiJuMntExoshbm1kh2wQzzzAA';
|
||||
$status_messages->bigPipePlaceholderRenderArray = [
|
||||
'#markup' => '<div data-big-pipe-placeholder-id="callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&args[0]&token=a8c34b5e"></div>',
|
||||
'#markup' => '<span data-big-pipe-placeholder-id="callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&args[0]&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&args[0]&token=a8c34b5e' => $status_messages->placeholderRenderArray,
|
||||
'callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&args[0]&token=_HAdUpwWmet0TOTe2PSiJuMntExoshbm1kh2wQzzzAA' => $status_messages->placeholderRenderArray,
|
||||
],
|
||||
],
|
||||
];
|
||||
$status_messages->bigPipeNoJsPlaceholder = '<div data-big-pipe-nojs-placeholder-id="callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&args[0]&token=a8c34b5e"></div>';
|
||||
$status_messages->bigPipeNoJsPlaceholder = '<span data-big-pipe-nojs-placeholder-id="callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&args[0]&token=_HAdUpwWmet0TOTe2PSiJuMntExoshbm1kh2wQzzzAA"></span>';
|
||||
$status_messages->bigPipeNoJsPlaceholderRenderArray = [
|
||||
'#markup' => '<div data-big-pipe-nojs-placeholder-id="callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&args[0]&token=a8c34b5e"></div>',
|
||||
'#markup' => '<span data-big-pipe-nojs-placeholder-id="callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&args[0]&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&args[0]&token=a8c34b5e"></div>' => $status_messages->placeholderRenderArray,
|
||||
'<span data-big-pipe-nojs-placeholder-id="callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&args[0]&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&1=suck" token="68a75f1a"></drupal-render-placeholder>',
|
||||
'<drupal-render-placeholder callback="\Drupal\big_pipe_test\BigPipeTestController::exception" arguments="0=llamas&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&args[0]=llamas&args[1]=suck&token=68a75f1a';
|
||||
$exception->bigPipePlaceholderId = 'callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3Aexception&args[0]=llamas&args[1]=suck&token=uhKFNfT4eF449_W-kDQX8E5z4yHyt0-nSHUlwaGAQeU';
|
||||
$exception->bigPipePlaceholderRenderArray = [
|
||||
'#markup' => '<div data-big-pipe-placeholder-id="callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3Aexception&args[0]=llamas&args[1]=suck&token=68a75f1a"></div>',
|
||||
'#markup' => '<span data-big-pipe-placeholder-id="callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3Aexception&args[0]=llamas&args[1]=suck&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&args[0]=llamas&args[1]=suck&token=68a75f1a' => $exception->placeholderRenderArray,
|
||||
'callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3Aexception&args[0]=llamas&args[1]=suck&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&args[0]=llamas&args[1]=suck&token=68a75f1a"></div>';
|
||||
$exception->bigPipeNoJsPlaceholder = '<span data-big-pipe-nojs-placeholder-id="callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3Aexception&args[0]=llamas&args[1]=suck&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&&token=2a9bd022';
|
||||
$embedded_response_exception->bigPipePlaceholderId = 'callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3AresponseException&&token=PxOHfS_QL-T01NjBgu7Z7I04tIwMp6La5vM-mVxezbU';
|
||||
$embedded_response_exception->bigPipePlaceholderRenderArray = [
|
||||
'#markup' => '<div data-big-pipe-placeholder-id="callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3AresponseException&&token=2a9bd022"></div>',
|
||||
'#markup' => '<span data-big-pipe-placeholder-id="callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3AresponseException&&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&&token=2a9bd022' => $embedded_response_exception->placeholderRenderArray,
|
||||
'callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3AresponseException&&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&&token=2a9bd022"></div>';
|
||||
$embedded_response_exception->bigPipeNoJsPlaceholder = '<span data-big-pipe-nojs-placeholder-id="callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3AresponseException&&token=PxOHfS_QL-T01NjBgu7Z7I04tIwMp6La5vM-mVxezbU"></span>';
|
||||
$embedded_response_exception->bigPipeNoJsPlaceholderRenderArray = [
|
||||
'#markup' => $embedded_response_exception->bigPipeNoJsPlaceholder,
|
||||
'#cache' => $cacheability_depends_on_session_and_nojs_cookie,
|
||||
|
|
|
@ -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&args[0]&token=a8c34b5e';
|
||||
$big_pipe_placeholder_id = 'callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&args[0]&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);
|
||||
}
|
||||
|
||||
|
|
Reference in a new issue