PageRenderTime 29ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 1ms

/core/modules/big_pipe/src/Render/Placeholder/BigPipeStrategy.php

http://github.com/drupal/drupal
PHP | 291 lines | 111 code | 22 blank | 158 comment | 12 complexity | bfe286cd8cde3f77d5423b6603cc4be9 MD5 | raw file
Possible License(s): GPL-2.0, LGPL-2.1
  1. <?php
  2. namespace Drupal\big_pipe\Render\Placeholder;
  3. use Drupal\Component\Utility\Crypt;
  4. use Drupal\Component\Utility\Html;
  5. use Drupal\Component\Utility\UrlHelper;
  6. use Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface;
  7. use Drupal\Core\Routing\RouteMatchInterface;
  8. use Drupal\Core\Session\SessionConfigurationInterface;
  9. use Symfony\Component\HttpFoundation\RequestStack;
  10. /**
  11. * Defines the BigPipe placeholder strategy, to send HTML in chunks.
  12. *
  13. * First: the BigPipe placeholder strategy only activates if the current request
  14. * is associated with a session. Without a session, it is assumed this response
  15. * is not actually dynamic: if none of the placeholders show session-dependent
  16. * information, then none of the placeholders are uncacheable or poorly
  17. * cacheable, which means the Page Cache (for anonymous users) can deal with it.
  18. * In other words: BigPipe works for all authenticated users and for anonymous
  19. * users that have a session (typical example: a shopping cart).
  20. *
  21. * (This is the default, and other modules can subclass this placeholder
  22. * strategy to have different rules for enabling BigPipe.)
  23. *
  24. * The BigPipe placeholder strategy actually consists of two substrategies,
  25. * depending on whether the current session is in a browser with JavaScript
  26. * enabled or not:
  27. * 1. with JavaScript enabled: #attached[big_pipe_js_placeholders]. Their
  28. * replacements are streamed at the end of the page: chunk 1 is the entire
  29. * page until the closing </body> tag, chunks 2 to (N-1) are replacement
  30. * values for the placeholders, chunk N is </body> and everything after it.
  31. * 2. with JavaScript disabled: #attached[big_pipe_nojs_placeholders]. Their
  32. * replacements are streamed in situ: chunk 1 is the entire page until the
  33. * first no-JS BigPipe placeholder, chunk 2 is the replacement for that
  34. * placeholder, chunk 3 is the chunk from after that placeholder until the
  35. * next no-JS BigPipe placeholder, et cetera.
  36. *
  37. * JS BigPipe placeholders are preferred because they result in better perceived
  38. * performance: the entire page can be sent, minus the placeholders. But it
  39. * requires JavaScript.
  40. *
  41. * No-JS BigPipe placeholders result in more visible blocking: only the part of
  42. * the page can be sent until the first placeholder, after it is rendered until
  43. * the second, et cetera. (In essence: multiple flushes.)
  44. *
  45. * Finally, both of those substrategies can also be combined: some placeholders
  46. * live in places that cannot be efficiently replaced by JavaScript, for example
  47. * CSRF tokens in URLs. Using no-JS BigPipe placeholders in those cases allows
  48. * the first part of the page (until the first no-JS BigPipe placeholder) to be
  49. * sent sooner than when they would be replaced using SingleFlushStrategy, which
  50. * would prevent anything from being sent until all those non-HTML placeholders
  51. * would have been replaced.
  52. *
  53. * See \Drupal\big_pipe\Render\BigPipe for detailed documentation on how those
  54. * different placeholders are actually replaced.
  55. *
  56. * @see \Drupal\big_pipe\Render\BigPipe
  57. */
  58. class BigPipeStrategy implements PlaceholderStrategyInterface {
  59. /**
  60. * BigPipe no-JS cookie name.
  61. */
  62. const NOJS_COOKIE = 'big_pipe_nojs';
  63. /**
  64. * The session configuration.
  65. *
  66. * @var \Drupal\Core\Session\SessionConfigurationInterface
  67. */
  68. protected $sessionConfiguration;
  69. /**
  70. * The request stack.
  71. *
  72. * @var \Symfony\Component\HttpFoundation\RequestStack
  73. */
  74. protected $requestStack;
  75. /**
  76. * The current route match.
  77. *
  78. * @var \Drupal\Core\Routing\RouteMatchInterface
  79. */
  80. protected $routeMatch;
  81. /**
  82. * Constructs a new BigPipeStrategy class.
  83. *
  84. * @param \Drupal\Core\Session\SessionConfigurationInterface $session_configuration
  85. * The session configuration.
  86. * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
  87. * The request stack.
  88. * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
  89. * The current route match.
  90. */
  91. public function __construct(SessionConfigurationInterface $session_configuration, RequestStack $request_stack, RouteMatchInterface $route_match) {
  92. $this->sessionConfiguration = $session_configuration;
  93. $this->requestStack = $request_stack;
  94. $this->routeMatch = $route_match;
  95. }
  96. /**
  97. * {@inheritdoc}
  98. */
  99. public function processPlaceholders(array $placeholders) {
  100. $request = $this->requestStack->getCurrentRequest();
  101. // @todo remove this check when https://www.drupal.org/node/2367555 lands.
  102. if (!$request->isMethodCacheable()) {
  103. return [];
  104. }
  105. // Routes can opt out from using the BigPipe HTML delivery technique.
  106. if ($this->routeMatch->getRouteObject()->getOption('_no_big_pipe')) {
  107. return [];
  108. }
  109. if (!$this->sessionConfiguration->hasSession($request)) {
  110. return [];
  111. }
  112. return $this->doProcessPlaceholders($placeholders);
  113. }
  114. /**
  115. * Transforms placeholders to BigPipe placeholders, either no-JS or JS.
  116. *
  117. * @param array $placeholders
  118. * The placeholders to process.
  119. *
  120. * @return array
  121. * The BigPipe placeholders.
  122. */
  123. protected function doProcessPlaceholders(array $placeholders) {
  124. $overridden_placeholders = [];
  125. foreach ($placeholders as $placeholder => $placeholder_elements) {
  126. // BigPipe uses JavaScript and the DOM to find the placeholder to replace.
  127. // This means finding the placeholder to replace must be efficient. Most
  128. // placeholders are HTML, which we can find efficiently thanks to the
  129. // querySelector API. But some placeholders are HTML attribute values or
  130. // parts thereof, and potentially even plain text in DOM text nodes. For
  131. // BigPipe's JavaScript to find those placeholders, it would need to
  132. // iterate over all DOM text nodes. This is highly inefficient. Therefore,
  133. // the BigPipe placeholder strategy only converts HTML placeholders into
  134. // BigPipe placeholders. The other placeholders need to be replaced on the
  135. // server, not via BigPipe.
  136. // @see \Drupal\Core\Access\RouteProcessorCsrf::renderPlaceholderCsrfToken()
  137. // @see \Drupal\Core\Form\FormBuilder::renderFormTokenPlaceholder()
  138. // @see \Drupal\Core\Form\FormBuilder::renderPlaceholderFormAction()
  139. if (static::placeholderIsAttributeSafe($placeholder)) {
  140. $overridden_placeholders[$placeholder] = static::createBigPipeNoJsPlaceholder($placeholder, $placeholder_elements, TRUE);
  141. }
  142. else {
  143. // If the current request/session doesn't have JavaScript, fall back to
  144. // no-JS BigPipe.
  145. if ($this->requestStack->getCurrentRequest()->cookies->has(static::NOJS_COOKIE)) {
  146. $overridden_placeholders[$placeholder] = static::createBigPipeNoJsPlaceholder($placeholder, $placeholder_elements, FALSE);
  147. }
  148. else {
  149. $overridden_placeholders[$placeholder] = static::createBigPipeJsPlaceholder($placeholder, $placeholder_elements);
  150. }
  151. $overridden_placeholders[$placeholder]['#cache']['contexts'][] = 'cookies:' . static::NOJS_COOKIE;
  152. }
  153. }
  154. return $overridden_placeholders;
  155. }
  156. /**
  157. * Determines whether the given placeholder is attribute-safe or not.
  158. *
  159. * @param string $placeholder
  160. * A placeholder.
  161. *
  162. * @return bool
  163. * Whether the placeholder is safe for use in a HTML attribute (in case it's
  164. * a placeholder for a HTML attribute value or a subset of it).
  165. */
  166. protected static function placeholderIsAttributeSafe($placeholder) {
  167. assert(is_string($placeholder));
  168. return $placeholder[0] !== '<' || $placeholder !== Html::normalize($placeholder);
  169. }
  170. /**
  171. * Creates a BigPipe JS placeholder.
  172. *
  173. * @param string $original_placeholder
  174. * The original placeholder.
  175. * @param array $placeholder_render_array
  176. * The render array for a placeholder.
  177. *
  178. * @return array
  179. * The resulting BigPipe JS placeholder render array.
  180. */
  181. protected static function createBigPipeJsPlaceholder($original_placeholder, array $placeholder_render_array) {
  182. $big_pipe_placeholder_id = static::generateBigPipePlaceholderId($original_placeholder, $placeholder_render_array);
  183. return [
  184. '#markup' => '<span data-big-pipe-placeholder-id="' . Html::escape($big_pipe_placeholder_id) . '"></span>',
  185. '#cache' => [
  186. 'max-age' => 0,
  187. 'contexts' => [
  188. 'session.exists',
  189. ],
  190. ],
  191. '#attached' => [
  192. 'library' => [
  193. 'big_pipe/big_pipe',
  194. ],
  195. // Inform BigPipe' JavaScript known BigPipe placeholder IDs (a whitelist).
  196. 'drupalSettings' => [
  197. 'bigPipePlaceholderIds' => [$big_pipe_placeholder_id => TRUE],
  198. ],
  199. 'big_pipe_placeholders' => [
  200. Html::escape($big_pipe_placeholder_id) => $placeholder_render_array,
  201. ],
  202. ],
  203. ];
  204. }
  205. /**
  206. * Creates a BigPipe no-JS placeholder.
  207. *
  208. * @param string $original_placeholder
  209. * The original placeholder.
  210. * @param array $placeholder_render_array
  211. * The render array for a placeholder.
  212. * @param bool $placeholder_must_be_attribute_safe
  213. * Whether the placeholder must be safe for use in a HTML attribute (in case
  214. * it's a placeholder for a HTML attribute value or a subset of it).
  215. *
  216. * @return array
  217. * The resulting BigPipe no-JS placeholder render array.
  218. */
  219. protected static function createBigPipeNoJsPlaceholder($original_placeholder, array $placeholder_render_array, $placeholder_must_be_attribute_safe = FALSE) {
  220. if (!$placeholder_must_be_attribute_safe) {
  221. $big_pipe_placeholder = '<span data-big-pipe-nojs-placeholder-id="' . Html::escape(static::generateBigPipePlaceholderId($original_placeholder, $placeholder_render_array)) . '"></span>';
  222. }
  223. else {
  224. $big_pipe_placeholder = 'big_pipe_nojs_placeholder_attribute_safe:' . Html::escape($original_placeholder);
  225. }
  226. return [
  227. '#markup' => $big_pipe_placeholder,
  228. '#cache' => [
  229. 'max-age' => 0,
  230. 'contexts' => [
  231. 'session.exists',
  232. ],
  233. ],
  234. '#attached' => [
  235. 'big_pipe_nojs_placeholders' => [
  236. $big_pipe_placeholder => $placeholder_render_array,
  237. ],
  238. ],
  239. ];
  240. }
  241. /**
  242. * Generates a BigPipe placeholder ID.
  243. *
  244. * @param string $original_placeholder
  245. * The original placeholder.
  246. * @param array $placeholder_render_array
  247. * The render array for a placeholder.
  248. *
  249. * @return string
  250. * The generated BigPipe placeholder ID.
  251. */
  252. protected static function generateBigPipePlaceholderId($original_placeholder, array $placeholder_render_array) {
  253. // Generate a BigPipe placeholder ID (to be used by BigPipe's JavaScript).
  254. // @see \Drupal\Core\Render\PlaceholderGenerator::createPlaceholder()
  255. if (isset($placeholder_render_array['#lazy_builder'])) {
  256. $callback = $placeholder_render_array['#lazy_builder'][0];
  257. $arguments = $placeholder_render_array['#lazy_builder'][1];
  258. $token = Crypt::hashBase64(serialize($placeholder_render_array));
  259. return UrlHelper::buildQuery(['callback' => $callback, 'args' => $arguments, 'token' => $token]);
  260. }
  261. // When the placeholder's render array is not using a #lazy_builder,
  262. // anything could be in there: only #lazy_builder has a strict contract that
  263. // allows us to create a more sane selector. Therefore, simply the original
  264. // placeholder into a usable placeholder ID, at the cost of it being obtuse.
  265. else {
  266. return Html::getId($original_placeholder);
  267. }
  268. }
  269. }