big_pipe.libraries.yml | 2 +- js/big_pipe.js | 10 ++-- src/Render/BigPipe.php | 26 +++++------ src/Render/Placeholder/BigPipeStrategy.php | 53 ++++++++++++++-------- .../Render/Placeholder/BigPipeStrategyTest.php | 4 +- 5 files changed, 55 insertions(+), 40 deletions(-) diff --git a/big_pipe.libraries.yml b/big_pipe.libraries.yml index 88ecff6..4a9331d 100644 --- a/big_pipe.libraries.yml +++ b/big_pipe.libraries.yml @@ -3,7 +3,7 @@ big_pipe: js: js/big_pipe.js: {} drupalSettings: - bigPipePlaceholders: [] + bigPipePlaceholderIds: [] dependencies: - core/jquery - core/drupal diff --git a/js/big_pipe.js b/js/big_pipe.js index 026b0e0..7890ab5 100644 --- a/js/big_pipe.js +++ b/js/big_pipe.js @@ -12,16 +12,16 @@ * * @param {number} index * Current index. - * @param {HTMLScriptElement} placeholder + * @param {HTMLScriptElement} placeholderReplacement * Script tag created by bigPipe. */ - function bigPipeProcessPlaceholder(index, placeholder) { - var placeholderName = this.getAttribute('data-big-pipe-placeholder'); + function bigPipeProcessPlaceholderReplacement(index, placeholderReplacement) { + var placeholderId = this.getAttribute('data-big-pipe-replacement-for-placeholder-with-id'); var content = this.textContent.trim(); // Ignore any placeholders that are not in the known placeholder list. // This is used to avoid someone trying to XSS the site via the // placeholdering mechanism.; - if (typeof drupalSettings.bigPipePlaceholders[placeholderName] !== 'undefined') { + if (typeof drupalSettings.bigPipePlaceholderIds[placeholderId] !== 'undefined') { // If we try to parse the content too early textContent will be empty, // making JSON.parse fail. Remove once so that it can be processed again // later. @@ -61,7 +61,7 @@ } $(context).find('script[data-drupal-ajax-processor="big_pipe"]').once('big-pipe') - .each(bigPipeProcessPlaceholder); + .each(bigPipeProcessPlaceholderReplacement); // If we see a stop element always clear the timeout. if (context.querySelector('script[data-big-pipe-event="stop"]')) { diff --git a/src/Render/BigPipe.php b/src/Render/BigPipe.php index abe8133..7099414 100644 --- a/src/Render/BigPipe.php +++ b/src/Render/BigPipe.php @@ -270,11 +270,11 @@ class BigPipe implements BigPipeInterface { * * @param array $placeholders * Associative array; the BigPipe placeholders. Keys are the BigPipe - * selectors. + * placeholder IDs. * @param array $placeholder_order * Indexed array; the order in which the BigPipe placeholders must be sent. - * Values are the BigPipe selectors. (These values correspond to keys in - * $placeholders.) + * Values are the BigPipe placeholder IDs. (These values correspond to keys + * in $placeholders.) * @param \Drupal\Core\Asset\AttachedAssetsInterface $cumulative_assets * The cumulative assets sent so far; to be updated while rendering BigPipe * placeholders. @@ -300,23 +300,23 @@ class BigPipe implements BigPipeInterface { $fake_request = $this->requestStack->getMasterRequest()->duplicate(); $fake_request->headers->set('Accept', 'application/json'); - foreach ($placeholder_order as $placeholder) { - if (!isset($placeholders[$placeholder])) { + foreach ($placeholder_order as $placeholder_id) { + if (!isset($placeholders[$placeholder_id])) { continue; } // Render the placeholder. - $placeholder_render_array = $placeholders[$placeholder]; - $elements = $this->renderPlaceholder($placeholder, $placeholder_render_array); + $placeholder_render_array = $placeholders[$placeholder_id]; + $elements = $this->renderPlaceholder($placeholder_id, $placeholder_render_array); // Create a new AjaxResponse. $ajax_response = new AjaxResponse(); // JavaScript's querySelector automatically decodes HTML entities in // attributes, so we must decode the entities of the current BigPipe - // placeholder (which has HTML entities encoded since we use it to find + // placeholder ID (which has HTML entities encoded since we use it to find // the placeholders). - $big_pipe_js_selector = Html::decodeEntities($placeholder); - $ajax_response->addCommand(new ReplaceCommand(sprintf('[data-big-pipe-selector="%s"]', $big_pipe_js_selector), $elements['#markup'])); + $big_pipe_js_placeholder_id = Html::decodeEntities($placeholder_id); + $ajax_response->addCommand(new ReplaceCommand(sprintf('[data-big-pipe-placeholder-id="%s"]', $big_pipe_js_placeholder_id), $elements['#markup'])); $ajax_response->setAttachments($elements['#attached']); // Push a fake request with the asset libraries loaded so far and dispatch @@ -338,7 +338,7 @@ class BigPipe implements BigPipeInterface { // Send this embedded AJAX response. $json = $ajax_response->getContent(); $output = << + EOF; @@ -408,10 +408,10 @@ EOF; * * @return array * Indexed array; the order in which the BigPipe placeholders must be sent. - * Values are the BigPipe selectors. + * Values are the BigPipe placeholder IDs. */ protected function getPlaceholderOrder($html) { - $fragments = explode('
$callback, 'args' => $arguments, 'token' => $token]); - } - // When the placeholder's render array is not using a #lazy_builder, - // anything could be in there: only #lazy_builder has a strict contract that - // allows us to create a more sane selector. Therefore, simply the original - // placeholder into a usable selector, at the cost of it being obtuse. - else { - $big_pipe_js_selector = Html::getId($original_placeholder); - } + $big_pipe_placeholder_id = static::generateBigPipePlaceholderId($original_placeholder, $placeholder_render_array); return [ - '#markup' => '
', + '#markup' => '
', '#cache' => [ 'max-age' => 0, 'contexts' => [ @@ -190,12 +176,12 @@ class BigPipeStrategy implements PlaceholderStrategyInterface { 'library' => [ 'big_pipe/big_pipe', ], - // Inform BigPipe' JavaScript known BigPipe placeholders (a whitelist). + // Inform BigPipe' JavaScript known BigPipe placeholder IDs (a whitelist). 'drupalSettings' => [ - 'bigPipePlaceholders' => [$big_pipe_js_selector => TRUE], + 'bigPipePlaceholderIds' => [$big_pipe_placeholder_id => TRUE], ], 'big_pipe_placeholders' => [ - Html::escape($big_pipe_js_selector) => $placeholder_render_array, + Html::escape($big_pipe_placeholder_id) => $placeholder_render_array, ], ], ]; @@ -233,4 +219,33 @@ class BigPipeStrategy implements PlaceholderStrategyInterface { ]; } + /** + * Generates a BigPipe placeholder ID. + * + * @param string $original_placeholder + * The original placeholder. + * @param array $placeholder_render_array + * The render array for a placeholder. + * + * @return string + * The generated BigPipe placeholder ID. + */ + protected static function generateBigPipePlaceholderId($original_placeholder, array $placeholder_render_array) { + // Generate a BigPipe placeholder ID (to be used by BigPipe's JavaScript). + // @see \Drupal\Core\Render\PlaceholderGenerator::createPlaceholder() + 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)); + return UrlHelper::buildQuery(['callback' => $callback, 'args' => $arguments, 'token' => $token]); + } + // When the placeholder's render array is not using a #lazy_builder, + // anything could be in there: only #lazy_builder has a strict contract that + // allows us to create a more sane selector. Therefore, simply the original + // placeholder into a usable placeholder ID, at the cost of it being obtuse. + else { + return Html::getId($original_placeholder); + } + } + } diff --git a/tests/src/Unit/Render/Placeholder/BigPipeStrategyTest.php b/tests/src/Unit/Render/Placeholder/BigPipeStrategyTest.php index 34a8bd4..484f76b 100644 --- a/tests/src/Unit/Render/Placeholder/BigPipeStrategyTest.php +++ b/tests/src/Unit/Render/Placeholder/BigPipeStrategyTest.php @@ -69,12 +69,12 @@ class BigPipeStrategyTest extends UnitTestCase { ], ]; $messages_big_pipe_placeholder = [ - '#markup' => '
', + '#markup' => '
', '#cache' => $cacheability_depends_on_session_and_nojs_cookie, '#attached' => [ 'library' => ['big_pipe/big_pipe'], 'drupalSettings' => [ - 'bigPipePlaceholders' => [ + 'bigPipePlaceholderIds' => [ 'callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&args[0]&token=a8c34b5e' => TRUE, ], ],