summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAlex Pott2016-12-21 08:38:52 (GMT)
committerAlex Pott2016-12-21 08:38:52 (GMT)
commit69c23474e73a532fdef2d0082dac1a4e85d12b4e (patch)
tree6be49eb8154070ce95be908b5a3befa971fb3e97
parent6fa5085d66bc34b2ae38bad885b18b25b5f8bbca (diff)
Issue #2657684 by Wim Leers, Fabianx, xjm, effulgentsia: Refactor BigPipe internals to allow a contrib module to extend BigPipe with the ability to stream anonymous responses and prime Page Cache for subsequent visits
-rw-r--r--core/modules/big_pipe/src/EventSubscriber/HtmlResponseBigPipeSubscriber.php44
-rw-r--r--core/modules/big_pipe/src/Render/BigPipe.php105
-rw-r--r--core/modules/big_pipe/src/Render/BigPipeInterface.php11
-rw-r--r--core/modules/big_pipe/src/Render/BigPipeResponse.php75
-rw-r--r--core/modules/big_pipe/src/Render/Placeholder/BigPipeStrategy.php17
-rw-r--r--core/modules/big_pipe/src/Tests/BigPipeTest.php14
-rw-r--r--core/modules/big_pipe/tests/modules/big_pipe_test/src/BigPipeTestController.php13
-rw-r--r--core/modules/big_pipe/tests/src/Unit/Render/BigPipeResponseAttachmentsProcessorTest.php2
8 files changed, 203 insertions, 78 deletions
diff --git a/core/modules/big_pipe/src/EventSubscriber/HtmlResponseBigPipeSubscriber.php b/core/modules/big_pipe/src/EventSubscriber/HtmlResponseBigPipeSubscriber.php
index 80d0cd6..f8cef10 100644
--- a/core/modules/big_pipe/src/EventSubscriber/HtmlResponseBigPipeSubscriber.php
+++ b/core/modules/big_pipe/src/EventSubscriber/HtmlResponseBigPipeSubscriber.php
@@ -91,39 +91,25 @@ 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\BigPipeInterface
+ * A BigPipe service.
+ */
+ protected function getBigPipeService(FilterResponseEvent $event) {
+ return $this->bigPipe;
+ }
+
+ /**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
diff --git a/core/modules/big_pipe/src/Render/BigPipe.php b/core/modules/big_pipe/src/Render/BigPipe.php
index 1894aa9..9337234 100644
--- a/core/modules/big_pipe/src/Render/BigPipe.php
+++ b/core/modules/big_pipe/src/Render/BigPipe.php
@@ -107,9 +107,48 @@ class BigPipe implements BigPipeInterface {
}
/**
+ * Performs tasks before sending content (and rendering placeholders).
+ */
+ protected function performPreSendTasks() {
+ // The content in the placeholders may depend on the session, and by the
+ // time the response is sent (see index.php), the session is already
+ // closed. Reopen it for the duration that we are rendering placeholders.
+ $this->session->start();
+ }
+
+ /**
+ * Performs tasks after sending content (and rendering placeholders).
+ */
+ protected function performPostSendTasks() {
+ // Close the session again.
+ $this->session->save();
+ }
+
+ /**
+ * Sends a chunk.
+ *
+ * @param string|\Drupal\Core\Render\HtmlResponse $chunk
+ * The string or response to append. String if there's no cacheability
+ * metadata or attachments to merge.
+ */
+ protected function sendChunk($chunk) {
+ assert(is_string($chunk) || $chunk instanceof HtmlResponse);
+ if ($chunk instanceof HtmlResponse) {
+ print $chunk->getContent();
+ }
+ else {
+ print $chunk;
+ }
+ flush();
+ }
+
+ /**
* {@inheritdoc}
*/
- public function sendContent($content, array $attachments) {
+ 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 +160,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 +173,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 +191,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 +234,7 @@ class BigPipe implements BigPipeInterface {
$scripts_bottom = $html_response->getContent();
}
- print $scripts_bottom;
- flush();
+ $this->sendChunk($scripts_bottom);
}
/**
@@ -244,8 +275,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 +283,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 +353,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 +397,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 +469,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 +480,7 @@ EOF;
}
// Send the stop signal.
- print "\n";
- print static::STOP_SIGNAL;
- print "\n";
- flush();
+ $this->sendChunk("\n" . static::STOP_SIGNAL . "\n");
}
/**
@@ -479,8 +500,26 @@ 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 \Symfony\Component\HttpKernel\HttpKernelInterface::MASTER_REQUEST|\Symfony\Component\HttpKernel\HttpKernelInterface::SUB_REQUEST $request_type
+ * The request type.
+ * @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 +533,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);
}
/**
diff --git a/core/modules/big_pipe/src/Render/BigPipeInterface.php b/core/modules/big_pipe/src/Render/BigPipeInterface.php
index 6d0e5a7..420b5db 100644
--- a/core/modules/big_pipe/src/Render/BigPipeInterface.php
+++ b/core/modules/big_pipe/src/Render/BigPipeInterface.php
@@ -134,17 +134,14 @@ 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.
+ * @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. Furthermore, the signature of this method will change in
- * https://www.drupal.org/node/2657684.
+ * class.
*/
- public function sendContent($content, array $attachments);
+ public function sendContent(BigPipeResponse $response);
}
diff --git a/core/modules/big_pipe/src/Render/BigPipeResponse.php b/core/modules/big_pipe/src/Render/BigPipeResponse.php
index c6871d5..f74cec9 100644
--- a/core/modules/big_pipe/src/Render/BigPipeResponse.php
+++ b/core/modules/big_pipe/src/Render/BigPipeResponse.php
@@ -28,6 +28,74 @@ class BigPipeResponse extends HtmlResponse {
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
@@ -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;
}
diff --git a/core/modules/big_pipe/src/Render/Placeholder/BigPipeStrategy.php b/core/modules/big_pipe/src/Render/Placeholder/BigPipeStrategy.php
index 8fe3e97..7af53f0 100644
--- a/core/modules/big_pipe/src/Render/Placeholder/BigPipeStrategy.php
+++ b/core/modules/big_pipe/src/Render/Placeholder/BigPipeStrategy.php
@@ -150,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 {
@@ -170,6 +170,21 @@ class BigPipeStrategy implements PlaceholderStrategyInterface {
}
/**
+ * 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.
*
* @param string $original_placeholder
diff --git a/core/modules/big_pipe/src/Tests/BigPipeTest.php b/core/modules/big_pipe/src/Tests/BigPipeTest.php
index bb032e5..25a59b0 100644
--- a/core/modules/big_pipe/src/Tests/BigPipeTest.php
+++ b/core/modules/big_pipe/src/Tests/BigPipeTest.php
@@ -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,
@@ -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);
}
diff --git a/core/modules/big_pipe/tests/modules/big_pipe_test/src/BigPipeTestController.php b/core/modules/big_pipe/tests/modules/big_pipe_test/src/BigPipeTestController.php
index eeb7b0d..2230770 100644
--- a/core/modules/big_pipe/tests/modules/big_pipe_test/src/BigPipeTestController.php
+++ b/core/modules/big_pipe/tests/modules/big_pipe_test/src/BigPipeTestController.php
@@ -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'],
+ ],
];
}
diff --git a/core/modules/big_pipe/tests/src/Unit/Render/BigPipeResponseAttachmentsProcessorTest.php b/core/modules/big_pipe/tests/src/Unit/Render/BigPipeResponseAttachmentsProcessorTest.php
index 9123129..f7d137d 100644
--- a/core/modules/big_pipe/tests/src/Unit/Render/BigPipeResponseAttachmentsProcessorTest.php
+++ b/core/modules/big_pipe/tests/src/Unit/Render/BigPipeResponseAttachmentsProcessorTest.php
@@ -51,7 +51,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