Version 1
[yaffs-website] / web / core / modules / big_pipe / src / Render / Placeholder / BigPipeStrategy.php
1 <?php
2
3 namespace Drupal\big_pipe\Render\Placeholder;
4
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;
12
13 /**
14  * Defines the BigPipe placeholder strategy, to send HTML in chunks.
15  *
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).
23  *
24  * (This is the default, and other modules can subclass this placeholder
25  * strategy to have different rules for enabling BigPipe.)
26  *
27  * The BigPipe placeholder strategy actually consists of two substrategies,
28  * depending on whether the current session is in a browser with JavaScript
29  * enabled or not:
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.
39  *
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.
43  *
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.)
47  *
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.
55  *
56  * See \Drupal\big_pipe\Render\BigPipe for detailed documentation on how those
57  * different placeholders are actually replaced.
58  *
59  * @see \Drupal\big_pipe\Render\BigPipe
60  */
61 class BigPipeStrategy implements PlaceholderStrategyInterface {
62
63   /**
64    * BigPipe no-JS cookie name.
65    */
66   const NOJS_COOKIE = 'big_pipe_nojs';
67
68   /**
69    * The session configuration.
70    *
71    * @var \Drupal\Core\Session\SessionConfigurationInterface
72    */
73   protected $sessionConfiguration;
74
75   /**
76    * The request stack.
77    *
78    * @var \Symfony\Component\HttpFoundation\RequestStack
79    */
80   protected $requestStack;
81
82   /**
83    * The current route match.
84    *
85    * @var \Drupal\Core\Routing\RouteMatchInterface
86    */
87   protected $routeMatch;
88
89   /**
90    * Constructs a new BigPipeStrategy class.
91    *
92    * @param \Drupal\Core\Session\SessionConfigurationInterface $session_configuration
93    *   The session configuration.
94    * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
95    *   The request stack.
96    * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
97    *   The current route match.
98    */
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;
103   }
104
105   /**
106    * {@inheritdoc}
107    */
108   public function processPlaceholders(array $placeholders) {
109     $request = $this->requestStack->getCurrentRequest();
110
111     // @todo remove this check when https://www.drupal.org/node/2367555 lands.
112     if (!$request->isMethodSafe()) {
113       return [];
114     }
115
116     // Routes can opt out from using the BigPipe HTML delivery technique.
117     if ($this->routeMatch->getRouteObject()->getOption('_no_big_pipe')) {
118       return [];
119     }
120
121     if (!$this->sessionConfiguration->hasSession($request)) {
122       return [];
123     }
124
125     return $this->doProcessPlaceholders($placeholders);
126   }
127
128   /**
129    * Transforms placeholders to BigPipe placeholders, either no-JS or JS.
130    *
131    * @param array $placeholders
132    *   The placeholders to process.
133    *
134    * @return array
135    *   The BigPipe placeholders.
136    */
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);
155       }
156       else {
157         // If the current request/session doesn't have JavaScript, fall back to
158         // no-JS BigPipe.
159         if ($this->requestStack->getCurrentRequest()->cookies->has(static::NOJS_COOKIE)) {
160           $overridden_placeholders[$placeholder] = static::createBigPipeNoJsPlaceholder($placeholder, $placeholder_elements, FALSE);
161         }
162         else {
163           $overridden_placeholders[$placeholder] = static::createBigPipeJsPlaceholder($placeholder, $placeholder_elements);
164         }
165         $overridden_placeholders[$placeholder]['#cache']['contexts'][] = 'cookies:' . static::NOJS_COOKIE;
166       }
167     }
168
169     return $overridden_placeholders;
170   }
171
172   /**
173    * Determines whether the given placeholder is attribute-safe or not.
174    *
175    * @param string $placeholder
176    *   A placeholder.
177    *
178    * @return bool
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).
181    */
182   protected static function placeholderIsAttributeSafe($placeholder) {
183     assert('is_string($placeholder)');
184     return $placeholder[0] !== '<' || $placeholder !== Html::normalize($placeholder);
185   }
186
187   /**
188    * Creates a BigPipe JS placeholder.
189    *
190    * @param string $original_placeholder
191    *   The original placeholder.
192    * @param array $placeholder_render_array
193    *   The render array for a placeholder.
194    *
195    * @return array
196    *   The resulting BigPipe JS placeholder render array.
197    */
198   protected static function createBigPipeJsPlaceholder($original_placeholder, array $placeholder_render_array) {
199     $big_pipe_placeholder_id = static::generateBigPipePlaceholderId($original_placeholder, $placeholder_render_array);
200
201     return [
202       '#markup' => '<span data-big-pipe-placeholder-id="' . Html::escape($big_pipe_placeholder_id) . '"></span>',
203       '#cache' => [
204         'max-age' => 0,
205         'contexts' => [
206           'session.exists',
207         ],
208       ],
209       '#attached' => [
210         'library' => [
211           'big_pipe/big_pipe',
212         ],
213         // Inform BigPipe' JavaScript known BigPipe placeholder IDs (a whitelist).
214         'drupalSettings' => [
215           'bigPipePlaceholderIds' => [$big_pipe_placeholder_id => TRUE],
216         ],
217         'big_pipe_placeholders' => [
218           Html::escape($big_pipe_placeholder_id) => $placeholder_render_array,
219         ],
220       ],
221     ];
222   }
223
224   /**
225    * Creates a BigPipe no-JS placeholder.
226    *
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).
234    *
235    * @return array
236    *   The resulting BigPipe no-JS placeholder render array.
237    */
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>';
241     }
242     else {
243       $big_pipe_placeholder = 'big_pipe_nojs_placeholder_attribute_safe:' . Html::escape($original_placeholder);
244     }
245
246     return [
247       '#markup' => $big_pipe_placeholder,
248       '#cache' => [
249         'max-age' => 0,
250         'contexts' => [
251           'session.exists',
252         ],
253       ],
254       '#attached' => [
255         'big_pipe_nojs_placeholders' => [
256           $big_pipe_placeholder => $placeholder_render_array,
257         ],
258       ],
259     ];
260   }
261
262   /**
263    * Generates a BigPipe placeholder ID.
264    *
265    * @param string $original_placeholder
266    *   The original placeholder.
267    * @param array $placeholder_render_array
268    *   The render array for a placeholder.
269    *
270    * @return string
271    *   The generated BigPipe placeholder ID.
272    */
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]);
281     }
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.
286     else {
287       return Html::getId($original_placeholder);
288     }
289   }
290
291 }