/** * {@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)); }
/** * 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); }
/** * {@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; }
/** * {@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; }
/** * {@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; }
/** * {@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; }
/** * {@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; }
/** * 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; }
/** * 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); }
/** * {@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); }
/** * 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; }
/** * 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; }
/** * 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); }
/** * {@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); }
/** * 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); }
/** * 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.'); }
/** * 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 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.'); }