Esempio n. 1
0
 /**
  * {@inheritdoc}
  */
 public function process($text, $langcode)
 {
     $result = new FilterProcessResult($text);
     if (stristr($text, 'data-entity-type="file"') !== FALSE) {
         $dom = Html::load($text);
         $xpath = new \DOMXPath($dom);
         $processed_uuids = array();
         foreach ($xpath->query('//*[@data-entity-type="file" and @data-entity-uuid]') as $node) {
             $uuid = $node->getAttribute('data-entity-uuid');
             // If there is a 'src' attribute, set it to the file entity's current
             // URL. This ensures the URL works even after the file location changes.
             if ($node->hasAttribute('src')) {
                 $file = $this->entityManager->loadEntityByUuid('file', $uuid);
                 if ($file) {
                     $node->setAttribute('src', file_url_transform_relative(file_create_url($file->getFileUri())));
                 }
             }
             // Only process the first occurrence of each file UUID.
             if (!isset($processed_uuids[$uuid])) {
                 $processed_uuids[$uuid] = TRUE;
                 $file = $this->entityManager->loadEntityByUuid('file', $uuid);
                 if ($file) {
                     $result->addCacheTags($file->getCacheTags());
                 }
             }
         }
         $result->setProcessedText(Html::serialize($dom));
     }
     return $result;
 }
 /**
  * @covers ::createPlaceholder
  * @dataProvider providerCreatePlaceholderGeneratesValidHtmlMarkup
  *
  * Ensure that the generated placeholder markup is valid. If it is not, then
  * simply using DOMDocument on HTML that contains placeholders may modify the
  * placeholders' markup, which would make it impossible to replace the
  * placeholders: the placeholder markup in #attached versus that in the HTML
  * processed by DOMDocument would no longer match.
  */
 public function testCreatePlaceholderGeneratesValidHtmlMarkup(array $element)
 {
     $build = $this->placeholderGenerator->createPlaceholder($element);
     $original_placeholder_markup = (string) $build['#markup'];
     $processed_placeholder_markup = Html::serialize(Html::load($build['#markup']));
     $this->assertEquals($original_placeholder_markup, $processed_placeholder_markup);
 }
 /**
  * {@inheritdoc}
  */
 public function process($text, $langcode)
 {
     $document = Html::load($text);
     foreach ($this->settings['tags'] as $tag) {
         $tag_elements = $document->getElementsByTagName($tag);
         foreach ($tag_elements as $tag_element) {
             $tag_element->setAttribute('test_attribute', 'test attribute value');
         }
     }
     return new FilterProcessResult(Html::serialize($document));
 }
Esempio n. 4
0
 /**
  * Replace the contents of a DOMNode.
  *
  * @param \DOMNode $node
  *   A DOMNode object.
  * @param string $content
  *   The text or HTML that will replace the contents of $node.
  */
 protected function replaceNodeContent(\DOMNode &$node, $content)
 {
     if (strlen($content)) {
         // Load the content into a new DOMDocument and retrieve the DOM nodes.
         $replacement_nodes = Html::load($content)->getElementsByTagName('body')->item(0)->childNodes;
     } else {
         $replacement_nodes = [$node->ownerDocument->createTextNode('')];
     }
     foreach ($replacement_nodes as $replacement_node) {
         // Import the replacement node from the new DOMDocument into the original
         // one, importing also the child nodes of the replacement node.
         $replacement_node = $node->ownerDocument->importNode($replacement_node, TRUE);
         $node->parentNode->insertBefore($replacement_node, $node);
     }
     $node->parentNode->removeChild($node);
 }
Esempio n. 5
0
 /**
  * {@inheritdoc}
  */
 public function process($text, $langcode)
 {
     $result = new FilterProcessResult($text);
     if (strpos($text, 'data-entity-type') !== FALSE && (strpos($text, 'data-entity-embed-display') !== FALSE || strpos($text, 'data-view-mode') !== FALSE)) {
         $dom = Html::load($text);
         $xpath = new \DOMXPath($dom);
         foreach ($xpath->query('//drupal-entity[@data-entity-type and (@data-entity-uuid or @data-entity-id) and (@data-entity-embed-display or @data-view-mode)]') as $node) {
             /** @var \DOMElement $node */
             $entity_type = $node->getAttribute('data-entity-type');
             $entity = NULL;
             $entity_output = '';
             try {
                 // Load the entity either by UUID (preferred) or ID.
                 $id = $node->getAttribute('data-entity-uuid') ?: $node->getAttribute('data-entity-id');
                 $entity = $this->loadEntity($entity_type, $id);
                 if ($entity) {
                     // Protect ourselves from recursive rendering.
                     static $depth = 0;
                     $depth++;
                     if ($depth > 20) {
                         throw new RecursiveRenderingException(sprintf('Recursive rendering detected when rendering embedded %s entity %s.', $entity_type, $entity->id()));
                     }
                     // If a UUID was not used, but is available, add it to the HTML.
                     if (!$node->getAttribute('data-entity-uuid') && ($uuid = $entity->uuid())) {
                         $node->setAttribute('data-entity-uuid', $uuid);
                     }
                     $access = $entity->access('view', NULL, TRUE);
                     $access_metadata = CacheableMetadata::createFromObject($access);
                     $entity_metadata = CacheableMetadata::createFromObject($entity);
                     $result = $result->merge($entity_metadata)->merge($access_metadata);
                     $context = $this->getNodeAttributesAsArray($node);
                     $context += array('data-langcode' => $langcode);
                     $entity_output = $this->renderEntityEmbed($entity, $context);
                     $depth--;
                 } else {
                     throw new EntityNotFoundException(sprintf('Unable to load embedded %s entity %s.', $entity_type, $id));
                 }
             } catch (\Exception $e) {
                 watchdog_exception('entity_embed', $e);
             }
             $this->replaceNodeContent($node, $entity_output);
         }
         $result->setProcessedText(Html::serialize($dom));
     }
     return $result;
 }
 /**
  * {@inheritdoc}
  */
 public function process($text, $langcode)
 {
     $result = new FilterProcessResult($text);
     if (stristr($text, '<pre') !== FALSE) {
         $attach = FALSE;
         $nodes = Html::load($text)->getElementsByTagName('pre');
         foreach ($nodes as $node) {
             if (preg_match('/\\bbrush\\b:(.*?);/i', $node->getAttribute('class'))) {
                 $attach = TRUE;
                 break;
             }
         }
         if ($attach) {
             $result->addAttachments(['library' => ['syntax_highlighter/highlight']]);
         }
     }
     return $result;
 }
Esempio n. 7
0
 /**
  * {@inheritdoc}
  */
 public function process($text, $langcode)
 {
     $result = new FilterProcessResult($text);
     if (stristr($text, 'data-caption') !== FALSE) {
         $dom = Html::load($text);
         $xpath = new \DOMXPath($dom);
         foreach ($xpath->query('//*[@data-caption]') as $node) {
             // Read the data-caption attribute's value, then delete it.
             $caption = Html::escape($node->getAttribute('data-caption'));
             $node->removeAttribute('data-caption');
             // Sanitize caption: decode HTML encoding, limit allowed HTML tags; only
             // allow inline tags that are allowed by default, plus <br>.
             $caption = Html::decodeEntities($caption);
             $caption = FilteredMarkup::create(Xss::filter($caption, array('a', 'em', 'strong', 'cite', 'code', 'br')));
             // The caption must be non-empty.
             if (Unicode::strlen($caption) === 0) {
                 continue;
             }
             // Given the updated node and caption: re-render it with a caption, but
             // bubble up the value of the class attribute of the captioned element,
             // this allows it to collaborate with e.g. the filter_align filter.
             $tag = $node->tagName;
             $classes = $node->getAttribute('class');
             $node->removeAttribute('class');
             $node = $node->parentNode->tagName === 'a' ? $node->parentNode : $node;
             $filter_caption = array('#theme' => 'filter_caption', '#node' => FilteredMarkup::create($node->C14N()), '#tag' => $tag, '#caption' => $caption, '#classes' => $classes);
             $altered_html = drupal_render($filter_caption);
             // Load the altered HTML into a new DOMDocument and retrieve the element.
             $updated_nodes = Html::load($altered_html)->getElementsByTagName('body')->item(0)->childNodes;
             foreach ($updated_nodes as $updated_node) {
                 // Import the updated node from the new DOMDocument into the original
                 // one, importing also the child nodes of the updated node.
                 $updated_node = $dom->importNode($updated_node, TRUE);
                 $node->parentNode->insertBefore($updated_node, $node);
             }
             // Finally, remove the original data-caption node.
             $node->parentNode->removeChild($node);
         }
         $result->setProcessedText(Html::serialize($dom))->addAttachments(array('library' => array('filter/caption')));
     }
     return $result;
 }
Esempio n. 8
0
 /**
  * {@inheritdoc}
  */
 public function process($text, $langcode)
 {
     $result = new FilterProcessResult($text);
     if (stristr($text, 'data-entity-type="file"') !== FALSE) {
         $dom = Html::load($text);
         $xpath = new \DOMXPath($dom);
         $processed_uuids = array();
         foreach ($xpath->query('//*[@data-entity-type="file" and @data-entity-uuid]') as $node) {
             $uuid = $node->getAttribute('data-entity-uuid');
             // Only process the first occurrence of each file UUID.
             if (!isset($processed_uuids[$uuid])) {
                 $processed_uuids[$uuid] = TRUE;
                 $file = $this->entityManager->loadEntityByUuid('file', $uuid);
                 if ($file) {
                     $result->addCacheTags($file->getCacheTags());
                 }
             }
         }
     }
     return $result;
 }
Esempio n. 9
0
 /**
  * {@inheritdoc}
  */
 public function process($text, $langcode)
 {
     $result = new FilterProcessResult($text);
     if (stristr($text, 'data-align') !== FALSE) {
         $dom = Html::load($text);
         $xpath = new \DOMXPath($dom);
         foreach ($xpath->query('//*[@data-align]') as $node) {
             // Read the data-align attribute's value, then delete it.
             $align = $node->getAttribute('data-align');
             $node->removeAttribute('data-align');
             // If one of the allowed alignments, add the corresponding class.
             if (in_array($align, array('left', 'center', 'right'))) {
                 $classes = $node->getAttribute('class');
                 $classes = strlen($classes) > 0 ? explode(' ', $classes) : array();
                 $classes[] = 'align-' . $align;
                 $node->setAttribute('class', implode(' ', $classes));
             }
         }
         $result->setProcessedText(Html::serialize($dom));
     }
     return $result;
 }
 /**
  * {@inheritdoc}
  */
 public function process($text, $langcode)
 {
     $result = new FilterProcessResult($text);
     $dom = Html::load($text);
     $xpath = new \DOMXPath($dom);
     /** @var \DOMNode $node */
     foreach ($xpath->query('//img') as $node) {
         // Read the data-align attribute's value, then delete it.
         $width = $node->getAttribute('width');
         $height = $node->getAttribute('height');
         $src = $node->getAttribute('src');
         if (!UrlHelper::isExternal($src)) {
             if ($width || $height) {
                 /** @var \DOMNode $element */
                 $element = $dom->createElement('a');
                 $element->setAttribute('href', $src);
                 $node->parentNode->replaceChild($element, $node);
                 $element->appendChild($node);
             }
         }
     }
     $result->setProcessedText(Html::serialize($dom));
     return $result;
 }
 /**
  * {@inheritdoc}
  */
 public function process($text, $langcode)
 {
     $result = new FilterProcessResult($text);
     if (stristr($text, '<img ') !== FALSE) {
         $dom = Html::load($text);
         $images = $dom->getElementsByTagName('img');
         foreach ($images as $image) {
             $src = $image->getAttribute("src");
             // The src must be non-empty.
             if (Unicode::strlen($src) === 0) {
                 continue;
             }
             // The src must not already be an external URL
             if (stristr($src, 'http://') !== FALSE || stristr($src, 'https://') !== FALSE) {
                 continue;
             }
             $url = Url::fromUri('internal:' . $src, array('absolute' => TRUE));
             $url_string = $url->toString();
             $image->setAttribute('src', $url_string);
         }
         $result->setProcessedText(Html::serialize($dom));
     }
     return $result;
 }
Esempio n. 12
0
 /**
  * {@inheritdoc}
  */
 public function getHTMLRestrictions()
 {
     if ($this->restrictions) {
         return $this->restrictions;
     }
     // Parse the allowed HTML setting, and gradually make the whitelist more
     // specific.
     $restrictions = ['allowed' => []];
     // Make all the tags self-closing, so they will be parsed into direct
     // children of the body tag in the DomDocument.
     $html = str_replace('>', ' />', $this->settings['allowed_html']);
     // Protect any trailing * characters in attribute names, since DomDocument
     // strips them as invalid.
     $star_protector = '__zqh6vxfbk3cg__';
     $html = str_replace('*', $star_protector, $html);
     $body_child_nodes = Html::load($html)->getElementsByTagName('body')->item(0)->childNodes;
     foreach ($body_child_nodes as $node) {
         if ($node->nodeType !== XML_ELEMENT_NODE) {
             // Skip the empty text nodes inside tags.
             continue;
         }
         $tag = $node->tagName;
         if ($node->hasAttributes()) {
             // Mark the tag as allowed, assigning TRUE for each attribute name if
             // all values are allowed, or an array of specific allowed values.
             $restrictions['allowed'][$tag] = [];
             // Iterate over any attributes, and mark them as allowed.
             foreach ($node->attributes as $name => $attribute) {
                 // Put back any trailing * on wildcard attribute name.
                 $name = str_replace($star_protector, '*', $name);
                 if ($attribute->value === '') {
                     // If the value is the empty string all values are allowed.
                     $restrictions['allowed'][$tag][$name] = TRUE;
                 } else {
                     // A non-empty attribute value is assigned, mark each of the
                     // specified attribute values as allowed.
                     foreach (preg_split('/\\s+/', $attribute->value, -1, PREG_SPLIT_NO_EMPTY) as $value) {
                         // Put back any trailing * on wildcard attribute value.
                         $value = str_replace($star_protector, '*', $value);
                         $restrictions['allowed'][$tag][$name][$value] = TRUE;
                     }
                 }
             }
         } else {
             // Mark the tag as allowed, but with no attributes allowed.
             $restrictions['allowed'][$tag] = FALSE;
         }
     }
     // The 'style' and 'on*' ('onClick' etc.) attributes are always forbidden,
     // and are removed by Xss::filter().
     // The 'lang', and 'dir' attributes apply to all elements and are always
     // allowed. The value whitelist for the 'dir' attribute is enforced by
     // self::filterAttributes().  Note that those two attributes are in the
     // short list of globally usable attributes in HTML5. They are always
     // allowed since the correct values of lang and dir may only be known to
     // the content author. Of the other global attributes, they are not usually
     // added by hand to content, and especially the class attribute can have
     // undesired visual effects by allowing content authors to apply any
     // available style, so specific values should be explicitly whitelisted.
     // @see http://www.w3.org/TR/html5/dom.html#global-attributes
     $restrictions['allowed']['*'] = ['style' => FALSE, 'on*' => FALSE, 'lang' => TRUE, 'dir' => ['ltr' => TRUE, 'rtl' => TRUE]];
     // Save this calculated result for re-use.
     $this->restrictions = $restrictions;
     return $restrictions;
 }
Esempio n. 13
0
 /**
  * Applies a very permissive XSS/HTML filter to data-attributes.
  *
  * @param string $html
  *   The string to apply the data-attributes filtering to.
  *
  * @return string
  *   The filtered string.
  */
 protected static function filterXssDataAttributes($html)
 {
     if (stristr($html, 'data-') !== FALSE) {
         $dom = Html::load($html);
         $xpath = new \DOMXPath($dom);
         foreach ($xpath->query('//@*[starts-with(name(.), "data-")]') as $node) {
             // The data-attributes contain an HTML-encoded value, so we need to
             // decode the value, apply XSS filtering and then re-save as encoded
             // value. There is no need to explicitly decode $node->value, since the
             // DOMAttr::value getter returns the decoded value.
             $value = Xss::filterAdmin($node->value);
             $node->value = Html::escape($value);
         }
         $html = Html::serialize($dom);
     }
     return $html;
 }
Esempio n. 14
0
 /**
  * Tests post-render cache-integrated 'render_cache_placeholder' child
  * element.
  */
 function testDrupalRenderChildElementRenderCachePlaceholder()
 {
     $container = array('#type' => 'container');
     $context = array('bar' => $this->randomContextValue());
     $callback = 'common_test_post_render_cache_placeholder';
     $placeholder = drupal_render_cache_generate_placeholder($callback, $context);
     $test_element = array('#post_render_cache' => array($callback => array($context)), '#markup' => $placeholder, '#prefix' => '<foo>', '#suffix' => '</foo>');
     $container['test_element'] = $test_element;
     $expected_output = '<div><foo><bar>' . $context['bar'] . '</bar></foo></div>' . "\n";
     // #cache disabled.
     drupal_static_reset('_drupal_add_js');
     $element = $container;
     $output = drupal_render($element);
     $this->assertIdentical($output, $expected_output, 'Placeholder was replaced in output');
     $settings = $this->parseDrupalSettings(drupal_get_js());
     $this->assertIdentical($settings['common_test'], $context, '#attached is modified; JavaScript setting is added to page.');
     // The cache system is turned off for POST requests.
     $request_method = \Drupal::request()->getMethod();
     \Drupal::request()->setMethod('GET');
     // GET request: #cache enabled, cache miss.
     drupal_static_reset('_drupal_add_js');
     $element = $container;
     $element['#cache'] = array('cid' => 'render_cache_placeholder_test_GET');
     $element['test_element']['#cache'] = array('cid' => 'render_cache_placeholder_test_child_GET');
     // Simulate element rendering in a template, where sub-items of a renderable
     // can be sent to drupal_render() before the parent.
     $child =& $element['test_element'];
     $element['#children'] = drupal_render($child, TRUE);
     // Eventually, drupal_render() gets called on the root element.
     $output = drupal_render($element);
     $this->assertIdentical($output, $expected_output, 'Placeholder was replaced in output');
     $this->assertTrue(isset($element['#printed']), 'No cache hit');
     $this->assertIdentical($element['#markup'], $expected_output, 'Placeholder was replaced in #markup.');
     $settings = $this->parseDrupalSettings(drupal_get_js());
     $this->assertIdentical($settings['common_test'], $context, '#attached is modified; JavaScript setting is added to page.');
     // GET request: validate cached data for child element.
     $child_tokens = $element['test_element']['#post_render_cache']['common_test_post_render_cache_placeholder'][0]['token'];
     $parent_tokens = $element['#post_render_cache']['common_test_post_render_cache_placeholder'][0]['token'];
     $expected_token = $child_tokens;
     $element = array('#cache' => array('cid' => 'render_cache_placeholder_test_child_GET'));
     $cached_element = \Drupal::cache('render')->get(drupal_render_cid_create($element))->data;
     // Parse unique token out of the cached markup.
     $dom = Html::load($cached_element['#markup']);
     $xpath = new \DOMXPath($dom);
     $nodes = $xpath->query('//*[@token]');
     $this->assertTrue($nodes->length, 'The token attribute was found in the cached child element markup');
     $token = '';
     if ($nodes->length) {
         $token = $nodes->item(0)->getAttribute('token');
     }
     $this->assertIdentical($token, $expected_token, 'The tokens are identical for the child element');
     // Verify the token is in the cached element.
     $expected_element = array('#markup' => '<foo><drupal-render-cache-placeholder callback="common_test_post_render_cache_placeholder" token="' . $expected_token . '"></drupal-render-cache-placeholder></foo>', '#post_render_cache' => array('common_test_post_render_cache_placeholder' => array($context)), '#cache' => array('tags' => array('rendered' => TRUE)));
     $this->assertIdentical($cached_element, $expected_element, 'The correct data is cached for the child element: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.');
     // GET request: validate cached data (for the parent/entire render array).
     $element = array('#cache' => array('cid' => 'render_cache_placeholder_test_GET'));
     $cached_element = \Drupal::cache('render')->get(drupal_render_cid_create($element))->data;
     // Parse unique token out of the cached markup.
     $dom = Html::load($cached_element['#markup']);
     $xpath = new \DOMXPath($dom);
     $nodes = $xpath->query('//*[@token]');
     $this->assertTrue($nodes->length, 'The token attribute was found in the cached parent element markup');
     $token = '';
     if ($nodes->length) {
         $token = $nodes->item(0)->getAttribute('token');
     }
     $this->assertIdentical($token, $expected_token, 'The tokens are identical for the parent element');
     // Verify the token is in the cached element.
     $expected_element = array('#markup' => '<div><foo><drupal-render-cache-placeholder callback="common_test_post_render_cache_placeholder" token="' . $expected_token . '"></drupal-render-cache-placeholder></foo></div>' . "\n", '#post_render_cache' => array('common_test_post_render_cache_placeholder' => array($context)), '#cache' => array('tags' => array('rendered' => TRUE)));
     $this->assertIdentical($cached_element, $expected_element, 'The correct data is cached for the parent element: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.');
     // GET request: validate cached data.
     // Check the cache of the child element again after the parent has been
     // rendered.
     $element = array('#cache' => array('cid' => 'render_cache_placeholder_test_child_GET'));
     $cached_element = \Drupal::cache('render')->get(drupal_render_cid_create($element))->data;
     // Verify that the child element contains the correct
     // render_cache_placeholder markup.
     $expected_token = $child_tokens;
     $dom = Html::load($cached_element['#markup']);
     $xpath = new \DOMXPath($dom);
     $nodes = $xpath->query('//*[@token]');
     $this->assertTrue($nodes->length, 'The token attribute was found in the cached child element markup');
     $token = '';
     if ($nodes->length) {
         $token = $nodes->item(0)->getAttribute('token');
     }
     $this->assertIdentical($token, $expected_token, 'The tokens are identical for the child element');
     // Verify the token is in the cached element.
     $expected_element = array('#markup' => '<foo><drupal-render-cache-placeholder callback="common_test_post_render_cache_placeholder" token="' . $expected_token . '"></drupal-render-cache-placeholder></foo>', '#post_render_cache' => array('common_test_post_render_cache_placeholder' => array($context)), '#cache' => array('tags' => array('rendered' => TRUE)));
     $this->assertIdentical($cached_element, $expected_element, 'The correct data is cached for the child element: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.');
     // GET request: #cache enabled, cache hit.
     drupal_static_reset('_drupal_add_js');
     $element = $container;
     $element['#cache'] = array('cid' => 'render_cache_placeholder_test_GET');
     // Simulate element rendering in a template, where sub-items of a renderable
     // can be sent to drupal_render before the parent.
     $child =& $element['test_element'];
     $element['#children'] = drupal_render($child, TRUE);
     $output = drupal_render($element);
     $this->assertIdentical($output, $expected_output, 'Placeholder was replaced in output');
     $this->assertFalse(isset($element['#printed']), 'Cache hit');
     $this->assertIdentical($element['#markup'], $expected_output, 'Placeholder was replaced in #markup.');
     $settings = $this->parseDrupalSettings(drupal_get_js());
     $this->assertIdentical($settings['common_test'], $context, '#attached is modified; JavaScript setting is added to page.');
     // Restore the previous request method.
     \Drupal::request()->setMethod($request_method);
 }
Esempio n. 15
0
 /**
  * {@inheritdoc}
  */
 public function processEmbeds($text)
 {
     $document = Html::load($text);
     $xpath = new \DOMXPath($document);
     foreach ($xpath->query('//oembed') as $node) {
         $embed = $this->getEmbedObject($node->nodeValue);
         if (!empty($embed) && !empty($embed->html)) {
             $this->swapEmbedHtml($node, $embed);
         }
     }
     return Html::serialize($document);
 }
Esempio n. 16
0
 /**
  * Builds the "format_tags" configuration part of the CKEditor JS settings.
  *
  * @see getConfig()
  *
  * @param \Drupal\editor\Entity\Editor $editor
  *   A configured text editor object.
  *
  * @return array
  *   An array containing the "format_tags" configuration.
  */
 protected function generateFormatTagsSetting(Editor $editor)
 {
     // When no text format is associated yet, assume no tag is allowed.
     // @see \Drupal\Editor\EditorInterface::hasAssociatedFilterFormat()
     if (!$editor->hasAssociatedFilterFormat()) {
         return array();
     }
     $format = $editor->getFilterFormat();
     $cid = 'ckeditor_internal_format_tags:' . $format->id();
     if ($cached = $this->cache->get($cid)) {
         $format_tags = $cached->data;
     } else {
         // The <p> tag is always allowed — HTML without <p> tags is nonsensical.
         $format_tags = ['p'];
         // Given the list of possible format tags, automatically determine whether
         // the current text format allows this tag, and thus whether it should show
         // up in the "Format" dropdown.
         $possible_format_tags = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'pre'];
         foreach ($possible_format_tags as $tag) {
             $input = '<' . $tag . '>TEST</' . $tag . '>';
             $output = trim(check_markup($input, $editor->id()));
             if (Html::load($output)->getElementsByTagName($tag)->length !== 0) {
                 $format_tags[] = $tag;
             }
         }
         $format_tags = implode(';', $format_tags);
         // Cache the "format_tags" configuration. This cache item is infinitely
         // valid; it only changes whenever the text format is changed, hence it's
         // tagged with the text format's cache tag.
         $this->cache->set($cid, $format_tags, Cache::PERMANENT, $format->getCacheTags());
     }
     return $format_tags;
 }
Esempio n. 17
0
  /**
   * Sets up object for use.
   *
   * @param string $html
   *   Text to be prepared.
   * @param int $limit
   *   Amount of text to return.
   * @param string $ellipsis
   *   Characters to use at the end of the text.
   *
   * @return \DOMDocument
   *   Prepared DOMDocument to work with.
   */
  protected function init($html, $limit, $ellipsis) {

    $dom = Html::load($html);

    // The body tag node, our html fragment is automatically wrapped in
    // a <html><body> etc.
    $this->startNode = $dom->getElementsByTagName("body")->item(0);
    $this->limit = $limit;
    $this->ellipsis = $ellipsis;
    $this->charCount = 0;
    $this->wordCount = 0;
    $this->foundBreakpoint = FALSE;

    return $dom;
  }
Esempio n. 18
0
 /**
  * Tests child element that uses #post_render_cache but that is rendered via a
  * template.
  */
 function testDrupalRenderChildElementRenderCachePlaceholder()
 {
     $context = array('bar' => $this->randomContextValue());
     $callback = 'common_test_post_render_cache_placeholder';
     $placeholder = drupal_render_cache_generate_placeholder($callback, $context);
     $test_element = ['#theme' => 'common_test_render_element', 'foo' => ['#post_render_cache' => [$callback => [$context]], '#markup' => $placeholder, '#prefix' => '<pre>', '#suffix' => '</pre>']];
     $expected_output = '<pre><bar>' . $context['bar'] . '</bar></pre>' . "\n";
     // #cache disabled.
     $element = $test_element;
     $output = drupal_render_root($element);
     $this->assertIdentical($output, $expected_output, 'Placeholder was replaced in output');
     $expected_js = [['type' => 'setting', 'data' => ['common_test' => $context]]];
     $this->assertIdentical($element['#attached']['js'], $expected_js, '#attached is modified; JavaScript setting is added to page.');
     // The cache system is turned off for POST requests.
     $request_method = \Drupal::request()->getMethod();
     \Drupal::request()->setMethod('GET');
     // GET request: #cache enabled, cache miss.
     $element = $test_element;
     $element['#cache'] = array('cid' => 'render_cache_placeholder_test_GET');
     $element['foo']['#cache'] = array('cid' => 'render_cache_placeholder_test_child_GET');
     // Render, which will use the common-test-render-element.html.twig template.
     $output = drupal_render_root($element);
     $this->assertIdentical($output, $expected_output);
     //, 'Placeholder was replaced in output');
     $this->assertTrue(isset($element['#printed']), 'No cache hit');
     $this->assertIdentical($element['#markup'], $expected_output, 'Placeholder was replaced in #markup.');
     $this->assertIdentical($element['#attached']['js'], $expected_js, '#attached is modified; JavaScript setting is added to page.');
     // GET request: validate cached data for child element.
     $child_tokens = $element['foo']['#post_render_cache']['common_test_post_render_cache_placeholder'][0]['token'];
     $parent_tokens = $element['#post_render_cache']['common_test_post_render_cache_placeholder'][0]['token'];
     $expected_token = $child_tokens;
     $element = array('#cache' => array('cid' => 'render_cache_placeholder_test_child_GET'));
     $cached_element = \Drupal::cache('render')->get(drupal_render_cid_create($element))->data;
     // Parse unique token out of the cached markup.
     $dom = Html::load($cached_element['#markup']);
     $xpath = new \DOMXPath($dom);
     $nodes = $xpath->query('//*[@token]');
     $this->assertTrue($nodes->length, 'The token attribute was found in the cached child element markup');
     $token = '';
     if ($nodes->length) {
         $token = $nodes->item(0)->getAttribute('token');
     }
     $this->assertIdentical($token, $expected_token, 'The tokens are identical for the child element');
     // Verify the token is in the cached element.
     $expected_element = array('#markup' => '<pre><drupal-render-cache-placeholder callback="common_test_post_render_cache_placeholder" token="' . $expected_token . '"></drupal-render-cache-placeholder></pre>', '#attached' => array(), '#post_render_cache' => array('common_test_post_render_cache_placeholder' => array($context)), '#cache' => array('tags' => array('rendered')));
     $this->assertIdentical($cached_element, $expected_element, 'The correct data is cached for the child element: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.');
     // GET request: validate cached data (for the parent/entire render array).
     $element = array('#cache' => array('cid' => 'render_cache_placeholder_test_GET'));
     $cached_element = \Drupal::cache('render')->get(drupal_render_cid_create($element))->data;
     // Parse unique token out of the cached markup.
     $dom = Html::load($cached_element['#markup']);
     $xpath = new \DOMXPath($dom);
     $nodes = $xpath->query('//*[@token]');
     $this->assertTrue($nodes->length, 'The token attribute was found in the cached parent element markup');
     $token = '';
     if ($nodes->length) {
         $token = $nodes->item(0)->getAttribute('token');
     }
     $this->assertIdentical($token, $expected_token, 'The tokens are identical for the parent element');
     // Verify the token is in the cached element.
     $expected_element = array('#markup' => '<pre><drupal-render-cache-placeholder callback="common_test_post_render_cache_placeholder" token="' . $expected_token . '"></drupal-render-cache-placeholder></pre>' . "\n", '#attached' => array(), '#post_render_cache' => array('common_test_post_render_cache_placeholder' => array($context)), '#cache' => array('tags' => array('rendered')));
     $this->assertIdentical($cached_element, $expected_element);
     //, 'The correct data is cached for the parent element: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.');
     // GET request: validate cached data.
     // Check the cache of the child element again after the parent has been
     // rendered.
     $element = array('#cache' => array('cid' => 'render_cache_placeholder_test_child_GET'));
     $cached_element = \Drupal::cache('render')->get(drupal_render_cid_create($element))->data;
     // Verify that the child element contains the correct
     // render_cache_placeholder markup.
     $expected_token = $child_tokens;
     $dom = Html::load($cached_element['#markup']);
     $xpath = new \DOMXPath($dom);
     $nodes = $xpath->query('//*[@token]');
     $this->assertTrue($nodes->length, 'The token attribute was found in the cached child element markup');
     $token = '';
     if ($nodes->length) {
         $token = $nodes->item(0)->getAttribute('token');
     }
     $this->assertIdentical($token, $expected_token, 'The tokens are identical for the child element');
     // Verify the token is in the cached element.
     $expected_element = array('#markup' => '<pre><drupal-render-cache-placeholder callback="common_test_post_render_cache_placeholder" token="' . $expected_token . '"></drupal-render-cache-placeholder></pre>', '#attached' => array(), '#post_render_cache' => array('common_test_post_render_cache_placeholder' => array($context)), '#cache' => array('tags' => array('rendered')));
     $this->assertIdentical($cached_element, $expected_element, 'The correct data is cached for the child element: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.');
     // GET request: #cache enabled, cache hit.
     $element = $test_element;
     $element['#cache'] = array('cid' => 'render_cache_placeholder_test_GET');
     // Render, which will use the common-test-render-element.html.twig template.
     $output = drupal_render_root($element);
     $this->assertIdentical($output, $expected_output, 'Placeholder was replaced in output');
     $this->assertFalse(isset($element['#printed']), 'Cache hit');
     $this->assertIdentical($element['#markup'], $expected_output, 'Placeholder was replaced in #markup.');
     $this->assertIdentical($element['#attached']['js'], $expected_js, '#attached is modified; JavaScript setting is added to page.');
     // Restore the previous request method.
     \Drupal::request()->setMethod($request_method);
 }
Esempio n. 19
0
 /**
  * {@inheritdoc}
  */
 public function process($text, $langcode)
 {
     $result = new FilterProcessResult($text);
     if (stristr($text, 'data-caption') !== FALSE || stristr($text, 'data-align') !== FALSE) {
         $caption_found = FALSE;
         $dom = Html::load($text);
         $xpath = new \DOMXPath($dom);
         foreach ($xpath->query('//*[@data-caption or @data-align]') as $node) {
             $caption = NULL;
             $align = NULL;
             // Retrieve, then remove the data-caption and data-align attributes.
             if ($node->hasAttribute('data-caption')) {
                 $caption = String::checkPlain($node->getAttribute('data-caption'));
                 $node->removeAttribute('data-caption');
                 // Sanitize caption: decode HTML encoding, limit allowed HTML tags;
                 // only allow inline tags that are allowed by default, plus <br>.
                 $caption = String::decodeEntities($caption);
                 $caption = Xss::filter($caption, array('a', 'em', 'strong', 'cite', 'code', 'br'));
                 // The caption must be non-empty.
                 if (Unicode::strlen($caption) === 0) {
                     $caption = NULL;
                 }
             }
             if ($node->hasAttribute('data-align')) {
                 $align = $node->getAttribute('data-align');
                 $node->removeAttribute('data-align');
                 // Only allow 3 values: 'left', 'center' and 'right'.
                 if (!in_array($align, array('left', 'center', 'right'))) {
                     $align = NULL;
                 }
             }
             // Don't transform the HTML if there isn't a caption after validation.
             if ($caption === NULL) {
                 // If there is a valid alignment, then transform the data-align
                 // attribute to a corresponding alignment class.
                 if ($align !== NULL) {
                     $classes = $node->getAttribute('class');
                     $classes = strlen($classes) > 0 ? explode(' ', $classes) : array();
                     $classes[] = 'align-' . $align;
                     $node->setAttribute('class', implode(' ', $classes));
                 }
                 continue;
             } else {
                 $caption_found = TRUE;
             }
             // Given the updated node, caption and alignment: re-render it with a
             // caption.
             $filter_caption = array('#theme' => 'filter_caption', '#node' => SafeMarkup::set($node->C14N()), '#tag' => $node->tagName, '#caption' => $caption, '#align' => $align);
             $altered_html = drupal_render($filter_caption);
             // Load the altered HTML into a new DOMDocument and retrieve the element.
             $updated_node = Html::load($altered_html)->getElementsByTagName('body')->item(0)->childNodes->item(0);
             // Import the updated node from the new DOMDocument into the original
             // one, importing also the child nodes of the updated node.
             $updated_node = $dom->importNode($updated_node, TRUE);
             // Finally, replace the original image node with the new image node!
             $node->parentNode->replaceChild($updated_node, $node);
         }
         $result->setProcessedText(Html::serialize($dom));
         if ($caption_found) {
             $result->addAssets(array('library' => array('filter/caption')));
         }
     }
     return $result;
 }
 /**
  * Locate all images in a piece of text that need replacing.
  *
  *   An array of settings that will be used to identify which images need
  *   updating. Includes the following:
  *
  *   - image_locations: An array of acceptable image locations.
  *     of the following values: "remote". Remote image will be downloaded and
  *     saved locally. This procedure is intensive as the images need to
  *     be retrieved to have their dimensions checked.
  *
  * @param string $text
  *   The text to be updated with the new img src tags.
  *
  * @return array $images
  *   An list of images.
  */
 private function getImages($text)
 {
     $dom = Html::load($text);
     $xpath = new \DOMXPath($dom);
     /** @var \DOMNode $node */
     foreach ($xpath->query('//img') as $node) {
         $file = $this->entityRepository->loadEntityByUuid('file', $node->getAttribute('data-entity-uuid'));
         // If the image hasn't an uuid then don't try to resize it.
         if (is_null($file)) {
             continue;
         }
         $image = $this->imageFactory->get($node->getAttribute('src'));
         // Checking if the image needs to be resized.
         if ($image->getWidth() == $node->getAttribute('width') && $image->getHeight() == $node->getAttribute('height')) {
             continue;
         }
         $target = file_uri_target($file->getFileUri());
         $dirname = dirname($target) != '.' ? dirname($target) . '/' : '';
         $info = pathinfo($file->getFileUri());
         $resize_file_path = 'public://resize/' . $dirname . $info['filename'] . '-' . $node->getAttribute('width') . 'x' . $node->getAttribute('height') . '.' . $info['extension'];
         // Checking if the image was already resized:
         if (file_exists($resize_file_path)) {
             $node->setAttribute('src', file_url_transform_relative(file_create_url($resize_file_path)));
             continue;
         }
         // Delete this when https://www.drupal.org/node/2211657#comment-11510213
         // be fixed.
         $dirname = $this->fileSystem->dirname($resize_file_path);
         if (!file_exists($dirname)) {
             file_prepare_directory($dirname, FILE_CREATE_DIRECTORY);
         }
         // Checks if the resize filter exists if is not then create it.
         $copy = file_unmanaged_copy($file->getFileUri(), $resize_file_path, FILE_EXISTS_REPLACE);
         $copy_image = $this->imageFactory->get($copy);
         $copy_image->resize($node->getAttribute('width'), $node->getAttribute('height'));
         $copy_image->save();
         $node->setAttribute('src', file_url_transform_relative(file_create_url($copy)));
     }
     return Html::serialize($dom);
 }
Esempio n. 21
0
 /**
  * Test DomHelperTrait::getNodeAttributesAsArray().
  */
 public function testGetNodeAttributesAsArray()
 {
     $attributes = $this->getNodeAttributesAsArray($this->node);
     $this->assertArrayEquals(['foo' => 'bar', 'namespace:foo' => 'bar'], $attributes);
     // Test more complex attributes with special characters.
     $string = "TEST: A <complex> 'encoded' \"JSON\" string";
     $object = array('nested' => array('array' => true), 'string' => $string);
     $html = '<test data-json-string=\'' . Json::encode($string) . '\' data-json-object=\'' . Json::encode($object) . '\'></test>';
     $document = Html::load($html);
     $node = $document->getElementsByTagName('body')->item(0)->firstChild;
     $attributes = $this->getNodeAttributesAsArray($node);
     $this->assertArrayEquals(['data-json-string' => $string, 'data-json-object' => $object], $attributes);
 }
Esempio n. 22
0
    /**
     * Create an element with a child and subchild. Each element has the same
     * #lazy_builder callback, but with different contexts. They don't modify
     * markup, only attach additional drupalSettings.
     *
     * @covers ::render
     * @covers ::doRender
     * @covers \Drupal\Core\Render\RenderCache::get
     * @covers ::replacePlaceholders
     */
    public function testRenderChildrenPlaceholdersDifferentArguments()
    {
        $this->setUpRequest();
        $this->setupMemoryCache();
        $this->cacheContextsManager->expects($this->any())->method('convertTokensToKeys')->willReturnArgument(0);
        $this->controllerResolver->expects($this->any())->method('getControllerFromDefinition')->willReturnArgument(0);
        $this->setupThemeManagerForDetails();
        $args_1 = ['foo', TRUE];
        $args_2 = ['bar', TRUE];
        $args_3 = ['baz', TRUE];
        $test_element = $this->generatePlaceholdersWithChildrenTestElement($args_1, $args_2, $args_3);
        $element = $test_element;
        $output = $this->renderer->renderRoot($element);
        $expected_output = <<<HTML
<details>
  <summary>Parent</summary>
  <div class="details-wrapper"><details>
  <summary>Child</summary>
  <div class="details-wrapper">Subchild</div>
</details></div>
</details>
HTML;
        $this->assertSame($expected_output, (string) $output, 'Output is not overridden.');
        $this->assertTrue(isset($element['#printed']), 'No cache hit');
        $this->assertSame($expected_output, (string) $element['#markup'], '#markup is not overridden.');
        $expected_js_settings = ['foo' => 'bar', 'dynamic_animal' => [$args_1[0] => TRUE, $args_2[0] => TRUE, $args_3[0] => TRUE]];
        $this->assertSame($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; both the original JavaScript setting and the ones added by each placeholder #lazy_builder callback exist.');
        // GET request: validate cached data.
        $cached_element = $this->memoryCache->get('simpletest:drupal_render:children_placeholders')->data;
        $expected_element = ['#attached' => ['drupalSettings' => ['foo' => 'bar'], 'placeholders' => ['parent-x-parent' => ['#lazy_builder' => [__NAMESPACE__ . '\\PlaceholdersTest::callback', $args_1]], 'child-x-child' => ['#lazy_builder' => [__NAMESPACE__ . '\\PlaceholdersTest::callback', $args_2]], 'subchild-x-subchild' => ['#lazy_builder' => [__NAMESPACE__ . '\\PlaceholdersTest::callback', $args_3]]]], '#cache' => ['contexts' => [], 'tags' => [], 'max-age' => Cache::PERMANENT]];
        $dom = Html::load($cached_element['#markup']);
        $xpath = new \DOMXPath($dom);
        $parent = $xpath->query('//details/summary[text()="Parent"]')->length;
        $child = $xpath->query('//details/div[@class="details-wrapper"]/details/summary[text()="Child"]')->length;
        $subchild = $xpath->query('//details/div[@class="details-wrapper"]/details/div[@class="details-wrapper" and text()="Subchild"]')->length;
        $this->assertTrue($parent && $child && $subchild, 'The correct data is cached: the stored #markup is not affected by placeholder #lazy_builder callbacks.');
        // Remove markup because it's compared above in the xpath.
        unset($cached_element['#markup']);
        $this->assertEquals($cached_element, $expected_element, 'The correct data is cached: the stored #attached properties are not affected by placeholder #lazy_builder callbacks.');
        // GET request: #cache enabled, cache hit.
        $element = $test_element;
        $output = $this->renderer->renderRoot($element);
        $this->assertSame($expected_output, (string) $output, 'Output is not overridden.');
        $this->assertFalse(isset($element['#printed']), 'Cache hit');
        $this->assertSame($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; both the original JavaScript setting and the ones added by each placeholder #lazy_builder callback exist.');
        // Use the exact same element, but now unset #cache; ensure we get the same
        // result.
        unset($test_element['#cache']);
        $element = $test_element;
        $output = $this->renderer->renderRoot($element);
        $this->assertSame($expected_output, (string) $output, 'Output is not overridden.');
        $this->assertSame($expected_output, (string) $element['#markup'], '#markup is not overridden.');
        $this->assertSame($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; both the original JavaScript setting and the ones added by each #lazy_builder callback exist.');
    }
Esempio n. 23
0
 /**
  * Parse options from an attributes string.
  *
  * @param string $text
  *   A string of options.
  *
  * @return array
  *   An associative array of parsed name/value pairs.
  */
 public function parseOptions($text)
 {
     // Decode special characters.
     $text = html_entity_decode($text);
     // Convert decode &nbsp; to expected ASCII code 32 character.
     // See: http://stackoverflow.com/questions/6275380/does-html-entity-decode-replaces-nbsp-also-if-not-how-to-replace-it
     $text = str_replace("�", ' ', $text);
     // Convert camel case to hyphen delimited because HTML5 lower cases all
     // data-* attributes.
     // See: Drupal.jQueryUiFilter.getOptions.
     $text = strtolower(preg_replace('/([a-z])([A-Z])/', '\\1-\\2', $text));
     // Create a DomElement so that we can parse its attributes as options.
     $html = Html::load('<div ' . $text . ' />');
     $dom_node = $html->getElementsByTagName('div')->item(0);
     $options = [];
     foreach ($dom_node->attributes as $attribute_name => $attribute_node) {
         // Convert empty attributes (ie nothing inside the quotes) to 'true' string.
         $options[$attribute_name] = $attribute_node->nodeValue ?: 'true';
     }
     return $options;
 }
 /**
  * Tests child element that uses #post_render_cache but that is rendered via a
  * template.
  */
 public function testChildElementPlaceholder()
 {
     $this->setupMemoryCache();
     // Simulate the theme system/Twig: a recursive call to Renderer::render(),
     // just like the theme system or a Twig template would have done.
     $this->themeManager->expects($this->any())->method('render')->willReturnCallback(function ($hook, $vars) {
         return $this->renderer->render($vars['foo']) . "\n";
     });
     $context = ['bar' => $this->randomContextValue(), 'token' => \Drupal\Component\Utility\Crypt::randomBytesBase64(55)];
     $callback = __NAMESPACE__ . '\\PostRenderCache::placeholder';
     $placeholder = \Drupal::service('renderer')->generateCachePlaceholder($callback, $context);
     $test_element = ['#theme' => 'some_theme_function', 'foo' => ['#post_render_cache' => [$callback => [$context]], '#markup' => $placeholder, '#prefix' => '<pre>', '#suffix' => '</pre>']];
     $expected_output = '<pre><bar>' . $context['bar'] . '</bar></pre>' . "\n";
     // #cache disabled.
     $element = $test_element;
     $output = $this->renderer->renderRoot($element);
     $this->assertSame($output, $expected_output, 'Placeholder was replaced in output');
     $expected_js_settings = ['common_test' => $context];
     $this->assertSame($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; JavaScript setting is added to page.');
     // GET request: #cache enabled, cache miss.
     $this->setUpRequest();
     $element = $test_element;
     $element['#cache'] = ['cid' => 'render_cache_placeholder_test_GET'];
     $element['foo']['#cache'] = ['cid' => 'render_cache_placeholder_test_child_GET'];
     // Render, which will use the common-test-render-element.html.twig template.
     $output = $this->renderer->renderRoot($element);
     $this->assertSame($output, $expected_output, 'Placeholder was replaced in output');
     $this->assertTrue(isset($element['#printed']), 'No cache hit');
     $this->assertSame($element['#markup'], $expected_output, 'Placeholder was replaced in #markup.');
     $this->assertSame($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; JavaScript setting is added to page.');
     // GET request: validate cached data for child element.
     $expected_token = $context['token'];
     $cached_element = $this->memoryCache->get('render_cache_placeholder_test_child_GET')->data;
     // Parse unique token out of the cached markup.
     $dom = Html::load($cached_element['#markup']);
     $xpath = new \DOMXPath($dom);
     $nodes = $xpath->query('//*[@token]');
     $this->assertEquals(1, $nodes->length, 'The token attribute was found in the cached child element markup');
     $token = '';
     if ($nodes->length) {
         $token = $nodes->item(0)->getAttribute('token');
     }
     $this->assertSame($token, $expected_token, 'The tokens are identical for the child element');
     // Verify the token is in the cached element.
     $expected_element = ['#markup' => '<pre><drupal-render-cache-placeholder callback="' . $callback . '" token="' . $expected_token . '"></drupal-render-cache-placeholder></pre>', '#attached' => [], '#post_render_cache' => [$callback => [$context]], '#cache' => ['contexts' => [], 'tags' => [], 'max-age' => Cache::PERMANENT]];
     $this->assertSame($cached_element, $expected_element, 'The correct data is cached for the child element: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.');
     // GET request: validate cached data (for the parent/entire render array).
     $cached_element = $this->memoryCache->get('render_cache_placeholder_test_GET')->data;
     // Parse unique token out of the cached markup.
     $dom = Html::load($cached_element['#markup']);
     $xpath = new \DOMXPath($dom);
     $nodes = $xpath->query('//*[@token]');
     $this->assertEquals(1, $nodes->length, 'The token attribute was found in the cached parent element markup');
     $token = '';
     if ($nodes->length) {
         $token = $nodes->item(0)->getAttribute('token');
     }
     $this->assertSame($token, $expected_token, 'The tokens are identical for the parent element');
     // Verify the token is in the cached element.
     $expected_element = ['#markup' => '<pre><drupal-render-cache-placeholder callback="' . $callback . '" token="' . $expected_token . '"></drupal-render-cache-placeholder></pre>' . "\n", '#attached' => [], '#post_render_cache' => [$callback => [$context]], '#cache' => ['contexts' => [], 'tags' => [], 'max-age' => Cache::PERMANENT]];
     $this->assertSame($cached_element, $expected_element, 'The correct data is cached for the parent element: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.');
     // GET request: validate cached data.
     // Check the cache of the child element again after the parent has been
     // rendered.
     $cached_element = $this->memoryCache->get('render_cache_placeholder_test_child_GET')->data;
     // Verify that the child element contains the correct
     // render_cache_placeholder markup.
     $dom = Html::load($cached_element['#markup']);
     $xpath = new \DOMXPath($dom);
     $nodes = $xpath->query('//*[@token]');
     $this->assertEquals(1, $nodes->length, 'The token attribute was found in the cached child element markup');
     $token = '';
     if ($nodes->length) {
         $token = $nodes->item(0)->getAttribute('token');
     }
     $this->assertSame($token, $expected_token, 'The tokens are identical for the child element');
     // Verify the token is in the cached element.
     $expected_element = ['#markup' => '<pre><drupal-render-cache-placeholder callback="' . $callback . '" token="' . $expected_token . '"></drupal-render-cache-placeholder></pre>', '#attached' => [], '#post_render_cache' => [$callback => [$context]], '#cache' => ['contexts' => [], 'tags' => [], 'max-age' => Cache::PERMANENT]];
     $this->assertSame($cached_element, $expected_element, 'The correct data is cached for the child element: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.');
     // GET request: #cache enabled, cache hit.
     $element = $test_element;
     $element['#cache'] = ['cid' => 'render_cache_placeholder_test_GET'];
     // Render, which will use the common-test-render-element.html.twig template.
     $output = $this->renderer->renderRoot($element);
     $this->assertSame($output, $expected_output, 'Placeholder was replaced in output');
     $this->assertFalse(isset($element['#printed']), 'Cache hit');
     $this->assertSame($element['#markup'], $expected_output, 'Placeholder was replaced in #markup.');
     $this->assertSame($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; JavaScript setting is added to page.');
 }