3 namespace Drupal\big_pipe\Render\Placeholder;
5 use Drupal\Component\Utility\Crypt;
6 use Drupal\Component\Utility\Html;
7 use Drupal\Component\Utility\UrlHelper;
8 use Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface;
9 use Drupal\Core\Routing\RouteMatchInterface;
10 use Drupal\Core\Session\SessionConfigurationInterface;
11 use Symfony\Component\HttpFoundation\RequestStack;
14 * Defines the BigPipe placeholder strategy, to send HTML in chunks.
16 * First: the BigPipe placeholder strategy only activates if the current request
17 * is associated with a session. Without a session, it is assumed this response
18 * is not actually dynamic: if none of the placeholders show session-dependent
19 * information, then none of the placeholders are uncacheable or poorly
20 * cacheable, which means the Page Cache (for anonymous users) can deal with it.
21 * In other words: BigPipe works for all authenticated users and for anonymous
22 * users that have a session (typical example: a shopping cart).
24 * (This is the default, and other modules can subclass this placeholder
25 * strategy to have different rules for enabling BigPipe.)
27 * The BigPipe placeholder strategy actually consists of two substrategies,
28 * depending on whether the current session is in a browser with JavaScript
30 * 1. with JavaScript enabled: #attached[big_pipe_js_placeholders]. Their
31 * replacements are streamed at the end of the page: chunk 1 is the entire
32 * page until the closing </body> tag, chunks 2 to (N-1) are replacement
33 * values for the placeholders, chunk N is </body> and everything after it.
34 * 2. with JavaScript disabled: #attached[big_pipe_nojs_placeholders]. Their
35 * replacements are streamed in situ: chunk 1 is the entire page until the
36 * first no-JS BigPipe placeholder, chunk 2 is the replacement for that
37 * placeholder, chunk 3 is the chunk from after that placeholder until the
38 * next no-JS BigPipe placeholder, et cetera.
40 * JS BigPipe placeholders are preferred because they result in better perceived
41 * performance: the entire page can be sent, minus the placeholders. But it
42 * requires JavaScript.
44 * No-JS BigPipe placeholders result in more visible blocking: only the part of
45 * the page can be sent until the first placeholder, after it is rendered until
46 * the second, et cetera. (In essence: multiple flushes.)
48 * Finally, both of those substrategies can also be combined: some placeholders
49 * live in places that cannot be efficiently replaced by JavaScript, for example
50 * CSRF tokens in URLs. Using no-JS BigPipe placeholders in those cases allows
51 * the first part of the page (until the first no-JS BigPipe placeholder) to be
52 * sent sooner than when they would be replaced using SingleFlushStrategy, which
53 * would prevent anything from being sent until all those non-HTML placeholders
54 * would have been replaced.
56 * See \Drupal\big_pipe\Render\BigPipe for detailed documentation on how those
57 * different placeholders are actually replaced.
59 * @see \Drupal\big_pipe\Render\BigPipe
61 class BigPipeStrategy implements PlaceholderStrategyInterface {
64 * BigPipe no-JS cookie name.
66 const NOJS_COOKIE = 'big_pipe_nojs';
69 * The session configuration.
71 * @var \Drupal\Core\Session\SessionConfigurationInterface
73 protected $sessionConfiguration;
78 * @var \Symfony\Component\HttpFoundation\RequestStack
80 protected $requestStack;
83 * The current route match.
85 * @var \Drupal\Core\Routing\RouteMatchInterface
87 protected $routeMatch;
90 * Constructs a new BigPipeStrategy class.
92 * @param \Drupal\Core\Session\SessionConfigurationInterface $session_configuration
93 * The session configuration.
94 * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
96 * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
97 * The current route match.
99 public function __construct(SessionConfigurationInterface $session_configuration, RequestStack $request_stack, RouteMatchInterface $route_match) {
100 $this->sessionConfiguration = $session_configuration;
101 $this->requestStack = $request_stack;
102 $this->routeMatch = $route_match;
108 public function processPlaceholders(array $placeholders) {
109 $request = $this->requestStack->getCurrentRequest();
111 // @todo remove this check when https://www.drupal.org/node/2367555 lands.
112 if (!$request->isMethodSafe()) {
116 // Routes can opt out from using the BigPipe HTML delivery technique.
117 if ($this->routeMatch->getRouteObject()->getOption('_no_big_pipe')) {
121 if (!$this->sessionConfiguration->hasSession($request)) {
125 return $this->doProcessPlaceholders($placeholders);
129 * Transforms placeholders to BigPipe placeholders, either no-JS or JS.
131 * @param array $placeholders
132 * The placeholders to process.
135 * The BigPipe placeholders.
137 protected function doProcessPlaceholders(array $placeholders) {
138 $overridden_placeholders = [];
139 foreach ($placeholders as $placeholder => $placeholder_elements) {
140 // BigPipe uses JavaScript and the DOM to find the placeholder to replace.
141 // This means finding the placeholder to replace must be efficient. Most
142 // placeholders are HTML, which we can find efficiently thanks to the
143 // querySelector API. But some placeholders are HTML attribute values or
144 // parts thereof, and potentially even plain text in DOM text nodes. For
145 // BigPipe's JavaScript to find those placeholders, it would need to
146 // iterate over all DOM text nodes. This is highly inefficient. Therefore,
147 // the BigPipe placeholder strategy only converts HTML placeholders into
148 // BigPipe placeholders. The other placeholders need to be replaced on the
149 // server, not via BigPipe.
150 // @see \Drupal\Core\Access\RouteProcessorCsrf::renderPlaceholderCsrfToken()
151 // @see \Drupal\Core\Form\FormBuilder::renderFormTokenPlaceholder()
152 // @see \Drupal\Core\Form\FormBuilder::renderPlaceholderFormAction()
153 if (static::placeholderIsAttributeSafe($placeholder)) {
154 $overridden_placeholders[$placeholder] = static::createBigPipeNoJsPlaceholder($placeholder, $placeholder_elements, TRUE);
157 // If the current request/session doesn't have JavaScript, fall back to
159 if ($this->requestStack->getCurrentRequest()->cookies->has(static::NOJS_COOKIE)) {
160 $overridden_placeholders[$placeholder] = static::createBigPipeNoJsPlaceholder($placeholder, $placeholder_elements, FALSE);
163 $overridden_placeholders[$placeholder] = static::createBigPipeJsPlaceholder($placeholder, $placeholder_elements);
165 $overridden_placeholders[$placeholder]['#cache']['contexts'][] = 'cookies:' . static::NOJS_COOKIE;
169 return $overridden_placeholders;
173 * Determines whether the given placeholder is attribute-safe or not.
175 * @param string $placeholder
179 * Whether the placeholder is safe for use in a HTML attribute (in case it's
180 * a placeholder for a HTML attribute value or a subset of it).
182 protected static function placeholderIsAttributeSafe($placeholder) {
183 assert('is_string($placeholder)');
184 return $placeholder[0] !== '<' || $placeholder !== Html::normalize($placeholder);
188 * Creates a BigPipe JS placeholder.
190 * @param string $original_placeholder
191 * The original placeholder.
192 * @param array $placeholder_render_array
193 * The render array for a placeholder.
196 * The resulting BigPipe JS placeholder render array.
198 protected static function createBigPipeJsPlaceholder($original_placeholder, array $placeholder_render_array) {
199 $big_pipe_placeholder_id = static::generateBigPipePlaceholderId($original_placeholder, $placeholder_render_array);
202 '#markup' => '<span data-big-pipe-placeholder-id="' . Html::escape($big_pipe_placeholder_id) . '"></span>',
213 // Inform BigPipe' JavaScript known BigPipe placeholder IDs (a whitelist).
214 'drupalSettings' => [
215 'bigPipePlaceholderIds' => [$big_pipe_placeholder_id => TRUE],
217 'big_pipe_placeholders' => [
218 Html::escape($big_pipe_placeholder_id) => $placeholder_render_array,
225 * Creates a BigPipe no-JS placeholder.
227 * @param string $original_placeholder
228 * The original placeholder.
229 * @param array $placeholder_render_array
230 * The render array for a placeholder.
231 * @param bool $placeholder_must_be_attribute_safe
232 * Whether the placeholder must be safe for use in a HTML attribute (in case
233 * it's a placeholder for a HTML attribute value or a subset of it).
236 * The resulting BigPipe no-JS placeholder render array.
238 protected static function createBigPipeNoJsPlaceholder($original_placeholder, array $placeholder_render_array, $placeholder_must_be_attribute_safe = FALSE) {
239 if (!$placeholder_must_be_attribute_safe) {
240 $big_pipe_placeholder = '<span data-big-pipe-nojs-placeholder-id="' . Html::escape(static::generateBigPipePlaceholderId($original_placeholder, $placeholder_render_array)) . '"></span>';
243 $big_pipe_placeholder = 'big_pipe_nojs_placeholder_attribute_safe:' . Html::escape($original_placeholder);
247 '#markup' => $big_pipe_placeholder,
255 'big_pipe_nojs_placeholders' => [
256 $big_pipe_placeholder => $placeholder_render_array,
263 * Generates a BigPipe placeholder ID.
265 * @param string $original_placeholder
266 * The original placeholder.
267 * @param array $placeholder_render_array
268 * The render array for a placeholder.
271 * The generated BigPipe placeholder ID.
273 protected static function generateBigPipePlaceholderId($original_placeholder, array $placeholder_render_array) {
274 // Generate a BigPipe placeholder ID (to be used by BigPipe's JavaScript).
275 // @see \Drupal\Core\Render\PlaceholderGenerator::createPlaceholder()
276 if (isset($placeholder_render_array['#lazy_builder'])) {
277 $callback = $placeholder_render_array['#lazy_builder'][0];
278 $arguments = $placeholder_render_array['#lazy_builder'][1];
279 $token = Crypt::hashBase64(serialize($placeholder_render_array));
280 return UrlHelper::buildQuery(['callback' => $callback, 'args' => $arguments, 'token' => $token]);
282 // When the placeholder's render array is not using a #lazy_builder,
283 // anything could be in there: only #lazy_builder has a strict contract that
284 // allows us to create a more sane selector. Therefore, simply the original
285 // placeholder into a usable placeholder ID, at the cost of it being obtuse.
287 return Html::getId($original_placeholder);