/**
  * Filters an HTML string to prevent XSS vulnerabilities.
  *
  * Like \Drupal\Component\Utility\Xss::filterAdmin(), but with a shorter list
  * of allowed tags.
  *
  * Used for items entered by administrators, like field descriptions, allowed
  * values, where some (mainly inline) mark-up may be desired (so
  * \Drupal\Component\Utility\SafeMarkup::checkPlain() is not acceptable).
  *
  * @param string $string
  *   The string with raw HTML in it.
  *
  * @return \Drupal\Component\Utility\SafeMarkup
  *   An XSS safe version of $string, or an empty string if $string is not
  *   valid UTF-8.
  */
 public function fieldFilterXss($string)
 {
     // All known XSS vectors are filtered out by
     // \Drupal\Component\Utility\Xss::filter(), all tags in the markup are
     // allowed intentionally by the trait, and no danger is added in by
     // \Drupal\Component\Utility\HTML::normalize(). Since the normalized value
     // is essentially the same markup, designate this string as safe as well.
     // This method is an internal part of field sanitization, so the resultant,
     // sanitized string should be printable as is.
     //
     // @todo Free this memory in https://www.drupal.org/node/2505963.
     return SafeMarkup::set(Html::normalize(Xss::filter($string, $this->allowedTags())));
 }
 /**
  * Overrides \Drupal\Component\Utility\SafeStringTrait::create().
  *
  * @return string|\Drupal\Component\Utility\SafeStringInterface
  *   A safe string filtered with the allowed tag list and normalized.
  *
  * @see \Drupal\Core\Field\FieldFilteredString::allowedTags()
  * @see \Drupal\Component\Utility\Xss::filter()
  * @see \Drupal\Component\Utility\Html::normalize()
  */
 public static function create($string)
 {
     $string = (string) $string;
     if ($string === '') {
         return '';
     }
     $safe_string = new static();
     // All known XSS vectors are filtered out by
     // \Drupal\Component\Utility\Xss::filter(), all tags in the markup are
     // allowed intentionally by the trait, and no danger is added in by
     // \Drupal\Component\Utility\HTML::normalize(). Since the normalized value
     // is essentially the same markup, designate this string as safe as well.
     // This method is an internal part of field sanitization, so the resultant,
     // sanitized string should be printable as is.
     $safe_string->string = Html::normalize(Xss::filter($string, static::allowedTags()));
     return $safe_string;
 }
 /**
  * @covers ::render
  * @covers ::doRender
  * @covers \Drupal\Core\Render\RenderCache::get
  * @covers \Drupal\Core\Render\RenderCache::set
  * @covers \Drupal\Core\Render\RenderCache::createCacheID
  *
  * @dataProvider providerPlaceholders
  */
 public function testCacheableParent($test_element, $args, array $expected_placeholder_render_array, $placeholder_cid_parts, array $bubbled_cache_contexts, array $bubbled_cache_tags, array $placeholder_expected_render_cache_array)
 {
     $element = $test_element;
     $this->setupMemoryCache();
     $this->setUpRequest('GET');
     $token = hash('crc32b', serialize($expected_placeholder_render_array));
     $placeholder_callback = $expected_placeholder_render_array['#lazy_builder'][0];
     $expected_placeholder_markup = '<drupal-render-placeholder callback="' . $placeholder_callback . '" arguments="0=' . $args[0] . '" token="' . $token . '"></drupal-render-placeholder>';
     $this->assertSame($expected_placeholder_markup, Html::normalize($expected_placeholder_markup), 'Placeholder unaltered by Html::normalize() which is used by FilterHtmlCorrector.');
     // GET request: #cache enabled, cache miss.
     $element['#cache'] = ['keys' => ['placeholder_test_GET']];
     $element['#prefix'] = '<p>#cache enabled, GET</p>';
     $output = $this->renderer->renderRoot($element);
     $this->assertSame('<p>#cache enabled, GET</p><p>This is a rendered placeholder!</p>', (string) $output, 'Output is overridden.');
     $this->assertTrue(isset($element['#printed']), 'No cache hit');
     $this->assertSame('<p>#cache enabled, GET</p><p>This is a rendered placeholder!</p>', (string) $element['#markup'], '#markup is overridden.');
     $expected_js_settings = ['foo' => 'bar', 'dynamic_animal' => $args[0]];
     $this->assertSame($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; both the original JavaScript setting and the one added by the placeholder #lazy_builder callback exist.');
     $this->assertPlaceholderRenderCache($placeholder_cid_parts, $bubbled_cache_contexts, $placeholder_expected_render_cache_array);
     // GET request: validate cached data.
     $cached = $this->memoryCache->get('placeholder_test_GET');
     // There are three edge cases, where the shape of the render cache item for
     // the parent (with CID 'placeholder_test_GET') is vastly different. These
     // are the cases where:
     // - the placeholder is uncacheable (because it has no #cache[keys]), and;
     // - cacheability metadata that meets auto_placeholder_conditions is bubbled
     $has_uncacheable_lazy_builder = !isset($test_element['placeholder']['#cache']['keys']) && isset($test_element['placeholder']['#lazy_builder']);
     // Edge cases: always where both bubbling of an auto-placeholdering
     // condition happens from within a #lazy_builder that is uncacheable.
     // - uncacheable + A5 (cache max-age)
     // @todo in https://www.drupal.org/node/2559847
     // - uncacheable + A6 (cache context)
     $edge_case_a6_uncacheable = $has_uncacheable_lazy_builder && $test_element['placeholder']['#lazy_builder'][0] === 'Drupal\\Tests\\Core\\Render\\PlaceholdersTest::callbackPerUser';
     // - uncacheable + A7 (cache tag)
     $edge_case_a7_uncacheable = $has_uncacheable_lazy_builder && $test_element['placeholder']['#lazy_builder'][0] === 'Drupal\\Tests\\Core\\Render\\PlaceholdersTest::callbackTagCurrentTemperature';
     // The redirect-cacheable edge case: a high-cardinality cache context is
     // bubbled from a #lazy_builder callback for an uncacheable placeholder. The
     // element containing the uncacheable placeholder has cache keys set, and
     // due to the bubbled cache contexts it creates a cache redirect.
     if ($edge_case_a6_uncacheable) {
         $cached_element = $cached->data;
         $expected_redirect = ['#cache_redirect' => TRUE, '#cache' => ['keys' => ['placeholder_test_GET'], 'contexts' => ['user'], 'tags' => [], 'max-age' => Cache::PERMANENT, 'bin' => 'render']];
         $this->assertEquals($expected_redirect, $cached_element);
         // Follow the redirect.
         $cached_element = $this->memoryCache->get('placeholder_test_GET:' . implode(':', $bubbled_cache_contexts))->data;
         $expected_element = ['#markup' => '<p>#cache enabled, GET</p><p>This is a rendered placeholder!</p>', '#attached' => ['drupalSettings' => ['foo' => 'bar', 'dynamic_animal' => $args[0]]], '#cache' => ['contexts' => $bubbled_cache_contexts, 'tags' => [], 'max-age' => Cache::PERMANENT]];
         $this->assertEquals($expected_element, $cached_element, 'The parent is render cached with a redirect in ase a cache context is bubbled from an uncacheable child (no #cache[keys]) with a #lazy_builder.');
     } elseif ($edge_case_a7_uncacheable) {
         $cached_element = $cached->data;
         $expected_element = ['#markup' => '<p>#cache enabled, GET</p><p>This is a rendered placeholder!</p>', '#attached' => ['drupalSettings' => ['foo' => 'bar', 'dynamic_animal' => $args[0]]], '#cache' => ['contexts' => [], 'tags' => $bubbled_cache_tags, 'max-age' => Cache::PERMANENT]];
         $this->assertEquals($expected_element, $cached_element, 'The correct data is cached: the stored #markup and #attached properties are not affected by placeholder #lazy_builder callbacks.');
     } else {
         $cached_element = $cached->data;
         $expected_element = ['#markup' => '<p>#cache enabled, GET</p>' . $expected_placeholder_markup, '#attached' => ['drupalSettings' => ['foo' => 'bar'], 'placeholders' => [$expected_placeholder_markup => ['#lazy_builder' => ['Drupal\\Tests\\Core\\Render\\PlaceholdersTest::callback', $args]]]], '#cache' => ['contexts' => [], 'tags' => $bubbled_cache_tags, 'max-age' => Cache::PERMANENT]];
         $expected_element['#attached']['placeholders'][$expected_placeholder_markup] = $expected_placeholder_render_array;
         $this->assertEquals($expected_element, $cached_element, 'The correct data is cached: the stored #markup and #attached properties are not affected by placeholder #lazy_builder callbacks.');
     }
     // GET request: #cache enabled, cache hit.
     $element = $test_element;
     $element['#cache'] = ['keys' => ['placeholder_test_GET']];
     $element['#prefix'] = '<p>#cache enabled, GET</p>';
     $output = $this->renderer->renderRoot($element);
     $this->assertSame('<p>#cache enabled, GET</p><p>This is a rendered placeholder!</p>', (string) $output, 'Output is overridden.');
     $this->assertFalse(isset($element['#printed']), 'Cache hit');
     $this->assertSame('<p>#cache enabled, GET</p><p>This is a rendered placeholder!</p>', (string) $element['#markup'], '#markup is overridden.');
     $expected_js_settings = ['foo' => 'bar', 'dynamic_animal' => $args[0]];
     $this->assertSame($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; both the original JavaScript setting and the one added by the placeholder #lazy_builder callback exist.');
 }
 /**
  * Trims the field down to the specified length.
  *
  * @param array $alter
  *   The alter array of options to use.
  *     - max_length: Maximum length of the string, the rest gets truncated.
  *     - word_boundary: Trim only on a word boundary.
  *     - ellipsis: Show an ellipsis (…) at the end of the trimmed string.
  *     - html: Make sure that the html is correct.
  *
  * @param string $value
  *   The string which should be trimmed.
  *
  * @return string
  *   The trimmed string.
  */
 public static function trimText($alter, $value)
 {
     if (drupal_strlen($value) > $alter['max_length']) {
         $value = drupal_substr($value, 0, $alter['max_length']);
         if (!empty($alter['word_boundary'])) {
             $regex = "(.*)\\b.+";
             if (function_exists('mb_ereg')) {
                 mb_regex_encoding('UTF-8');
                 $found = mb_ereg($regex, $value, $matches);
             } else {
                 $found = preg_match("/{$regex}/us", $value, $matches);
             }
             if ($found) {
                 $value = $matches[1];
             }
         }
         // Remove scraps of HTML entities from the end of a strings
         $value = rtrim(preg_replace('/(?:<(?!.+>)|&(?!.+;)).*$/us', '', $value));
         if (!empty($alter['ellipsis'])) {
             $value .= t('…');
         }
     }
     if (!empty($alter['html'])) {
         $value = Html::normalize($value);
     }
     return $value;
 }
Exemple #5
0
    /**
     * Tests the HTML corrector filter.
     *
     * @todo This test could really use some validity checking function.
     */
    function testHtmlCorrectorFilter()
    {
        // Tag closing.
        $f = Html::normalize('<p>text');
        $this->assertEqual($f, '<p>text</p>', 'HTML corrector -- tag closing at the end of input.');
        $f = Html::normalize('<p>text<p><p>text');
        $this->assertEqual($f, '<p>text</p><p></p><p>text</p>', 'HTML corrector -- tag closing.');
        $f = Html::normalize("<ul><li>e1<li>e2");
        $this->assertEqual($f, "<ul><li>e1</li><li>e2</li></ul>", 'HTML corrector -- unclosed list tags.');
        $f = Html::normalize('<div id="d">content');
        $this->assertEqual($f, '<div id="d">content</div>', 'HTML corrector -- unclosed tag with attribute.');
        // XHTML slash for empty elements.
        $f = Html::normalize('<hr><br>');
        $this->assertEqual($f, '<hr /><br />', 'HTML corrector -- XHTML closing slash.');
        $f = Html::normalize('<P>test</P>');
        $this->assertEqual($f, '<p>test</p>', 'HTML corrector -- Convert uppercased tags to proper lowercased ones.');
        $f = Html::normalize('<P>test</p>');
        $this->assertEqual($f, '<p>test</p>', 'HTML corrector -- Convert uppercased tags to proper lowercased ones.');
        $f = Html::normalize('test<hr />');
        $this->assertEqual($f, 'test<hr />', 'HTML corrector -- Let proper XHTML pass through.');
        $f = Html::normalize('test<hr/>');
        $this->assertEqual($f, 'test<hr />', 'HTML corrector -- Let proper XHTML pass through, but ensure there is a single space before the closing slash.');
        $f = Html::normalize('test<hr    />');
        $this->assertEqual($f, 'test<hr />', 'HTML corrector -- Let proper XHTML pass through, but ensure there are not too many spaces before the closing slash.');
        $f = Html::normalize('<span class="test" />');
        $this->assertEqual($f, '<span class="test"></span>', 'HTML corrector -- Convert XHTML that is properly formed but that would not be compatible with typical HTML user agents.');
        $f = Html::normalize('test1<br class="test">test2');
        $this->assertEqual($f, 'test1<br class="test" />test2', 'HTML corrector -- Automatically close single tags.');
        $f = Html::normalize('line1<hr>line2');
        $this->assertEqual($f, 'line1<hr />line2', 'HTML corrector -- Automatically close single tags.');
        $f = Html::normalize('line1<HR>line2');
        $this->assertEqual($f, 'line1<hr />line2', 'HTML corrector -- Automatically close single tags.');
        $f = Html::normalize('<img src="http://example.com/test.jpg">test</img>');
        $this->assertEqual($f, '<img src="http://example.com/test.jpg" />test', 'HTML corrector -- Automatically close single tags.');
        $f = Html::normalize('<br></br>');
        $this->assertEqual($f, '<br />', "HTML corrector -- Transform empty tags to a single closed tag if the tag's content model is EMPTY.");
        $f = Html::normalize('<div></div>');
        $this->assertEqual($f, '<div></div>', "HTML corrector -- Do not transform empty tags to a single closed tag if the tag's content model is not EMPTY.");
        $f = Html::normalize('<p>line1<br/><hr/>line2</p>');
        $this->assertEqual($f, '<p>line1<br /></p><hr />line2', 'HTML corrector -- Move non-inline elements outside of inline containers.');
        $f = Html::normalize('<p>line1<div>line2</div></p>');
        $this->assertEqual($f, '<p>line1</p><div>line2</div>', 'HTML corrector -- Move non-inline elements outside of inline containers.');
        $f = Html::normalize('<p>test<p>test</p>\\n');
        $this->assertEqual($f, '<p>test</p><p>test</p>\\n', 'HTML corrector -- Auto-close improperly nested tags.');
        $f = Html::normalize('<p>Line1<br><STRONG>bold stuff</b>');
        $this->assertEqual($f, '<p>Line1<br /><strong>bold stuff</strong></p>', 'HTML corrector -- Properly close unclosed tags, and remove useless closing tags.');
        $f = Html::normalize('test <!-- this is a comment -->');
        $this->assertEqual($f, 'test <!-- this is a comment -->', 'HTML corrector -- Do not touch HTML comments.');
        $f = Html::normalize('test <!--this is a comment-->');
        $this->assertEqual($f, 'test <!--this is a comment-->', 'HTML corrector -- Do not touch HTML comments.');
        $f = Html::normalize('test <!-- comment <p>another
    <strong>multiple</strong> line
    comment</p> -->');
        $this->assertEqual($f, 'test <!-- comment <p>another
    <strong>multiple</strong> line
    comment</p> -->', 'HTML corrector -- Do not touch HTML comments.');
        $f = Html::normalize('test <!-- comment <p>another comment</p> -->');
        $this->assertEqual($f, 'test <!-- comment <p>another comment</p> -->', 'HTML corrector -- Do not touch HTML comments.');
        $f = Html::normalize('test <!--break-->');
        $this->assertEqual($f, 'test <!--break-->', 'HTML corrector -- Do not touch HTML comments.');
        $f = Html::normalize('<p>test\\n</p>\\n');
        $this->assertEqual($f, '<p>test\\n</p>\\n', 'HTML corrector -- New-lines are accepted and kept as-is.');
        $f = Html::normalize('<p>دروبال');
        $this->assertEqual($f, '<p>دروبال</p>', 'HTML corrector -- Encoding is correctly kept.');
        $f = Html::normalize('<script>alert("test")</script>');
        $this->assertEqual($f, '<script>
<!--//--><![CDATA[// ><!--
alert("test")
//--><!]]>
</script>', 'HTML corrector -- CDATA added to script element');
        $f = Html::normalize('<p><script>alert("test")</script></p>');
        $this->assertEqual($f, '<p><script>
<!--//--><![CDATA[// ><!--
alert("test")
//--><!]]>
</script></p>', 'HTML corrector -- CDATA added to a nested script element');
        $f = Html::normalize('<p><style> /* Styling */ body {color:red}</style></p>');
        $this->assertEqual($f, '<p><style>
<!--/*--><![CDATA[/* ><!--*/
 /* Styling */ body {color:red}
/*--><!]]>*/
</style></p>', 'HTML corrector -- CDATA added to a style element.');
        $filtered_data = Html::normalize('<p><style>
/*<![CDATA[*/
/* Styling */
body {color:red}
/*]]>*/
</style></p>');
        $this->assertEqual($filtered_data, '<p><style>
<!--/*--><![CDATA[/* ><!--*/

/*<![CDATA[*/
/* Styling */
body {color:red}
/*]]]]><![CDATA[>*/

/*--><!]]>*/
</style></p>', format_string('HTML corrector -- Existing cdata section @pattern_name properly escaped', array('@pattern_name' => '/*<![CDATA[*/')));
        $filtered_data = Html::normalize('<p><style>
  <!--/*--><![CDATA[/* ><!--*/
  /* Styling */
  body {color:red}
  /*--><!]]>*/
</style></p>');
        $this->assertEqual($filtered_data, '<p><style>
<!--/*--><![CDATA[/* ><!--*/

  <!--/*--><![CDATA[/* ><!--*/
  /* Styling */
  body {color:red}
  /*--><!]]]]><![CDATA[>*/

/*--><!]]>*/
</style></p>', format_string('HTML corrector -- Existing cdata section @pattern_name properly escaped', array('@pattern_name' => '<!--/*--><![CDATA[/* ><!--*/')));
        $filtered_data = Html::normalize('<p><script>
<!--//--><![CDATA[// ><!--
  alert("test");
//--><!]]>
</script></p>');
        $this->assertEqual($filtered_data, '<p><script>
<!--//--><![CDATA[// ><!--

<!--//--><![CDATA[// ><!--
  alert("test");
//--><!]]]]><![CDATA[>

//--><!]]>
</script></p>', format_string('HTML corrector -- Existing cdata section @pattern_name properly escaped', array('@pattern_name' => '<!--//--><![CDATA[// ><!--')));
        $filtered_data = Html::normalize('<p><script>
// <![CDATA[
  alert("test");
// ]]>
</script></p>');
        $this->assertEqual($filtered_data, '<p><script>
<!--//--><![CDATA[// ><!--

// <![CDATA[
  alert("test");
// ]]]]><![CDATA[>

//--><!]]>
</script></p>', format_string('HTML corrector -- Existing cdata section @pattern_name properly escaped', array('@pattern_name' => '// <![CDATA[')));
    }
Exemple #6
0
 /**
  * Tests post-render cache-integrated 'render_cache_placeholder' element.
  */
 function testDrupalRenderRenderCachePlaceholder()
 {
     $context = array('bar' => $this->randomContextValue());
     $callback = 'common_test_post_render_cache_placeholder';
     $placeholder = drupal_render_cache_generate_placeholder($callback, $context);
     $this->assertIdentical($placeholder, Html::normalize($placeholder), 'Placeholder unaltered by Html::normalize() which is used by FilterHtmlCorrector.');
     $test_element = array('#post_render_cache' => array($callback => array($context)), '#markup' => $placeholder, '#prefix' => '<foo>', '#suffix' => '</foo>');
     $expected_output = '<foo><bar>' . $context['bar'] . '</bar></foo>';
     // #cache disabled.
     drupal_static_reset('_drupal_add_js');
     $element = $test_element;
     $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 = $test_element;
     $element['#cache'] = array('cid' => 'render_cache_placeholder_test_GET');
     $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.
     $expected_token = $element['#post_render_cache']['common_test_post_render_cache_placeholder'][0]['token'];
     $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 markup');
     $token = '';
     if ($nodes->length) {
         $token = $nodes->item(0)->getAttribute('token');
     }
     $this->assertIdentical($token, $expected_token, 'The tokens are identical');
     // 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: 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 = $test_element;
     $element['#cache'] = array('cid' => 'render_cache_placeholder_test_GET');
     $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);
 }
 /**
  * Transforms an HTML string into plain text, preserving its structure.
  *
  * The output will be suitable for use as 'format=flowed; delsp=yes' text
  * (RFC 3676) and can be passed directly to MailManagerInterface::mail() for sending.
  *
  * We deliberately use LF rather than CRLF, see MailManagerInterface::mail().
  *
  * This function provides suitable alternatives for the following tags:
  * <a> <em> <i> <strong> <b> <br> <p> <blockquote> <ul> <ol> <li> <dl> <dt>
  * <dd> <h1> <h2> <h3> <h4> <h5> <h6> <hr>
  *
  * @param string $string
  *   The string to be transformed.
  * @param array $allowed_tags
  *   (optional) If supplied, a list of tags that will be transformed. If
  *   omitted, all supported tags are transformed.
  *
  * @return string
  *   The transformed string.
  */
 public static function htmlToText($string, $allowed_tags = NULL)
 {
     // Cache list of supported tags.
     if (empty(static::$supportedTags)) {
         static::$supportedTags = array('a', 'em', 'i', 'strong', 'b', 'br', 'p', 'blockquote', 'ul', 'ol', 'li', 'dl', 'dt', 'dd', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr');
     }
     // Make sure only supported tags are kept.
     $allowed_tags = isset($allowed_tags) ? array_intersect(static::$supportedTags, $allowed_tags) : static::$supportedTags;
     // Make sure tags, entities and attributes are well-formed and properly
     // nested.
     $string = Html::normalize(Xss::filter($string, $allowed_tags));
     // Apply inline styles.
     $string = preg_replace('!</?(em|i)((?> +)[^>]*)?>!i', '/', $string);
     $string = preg_replace('!</?(strong|b)((?> +)[^>]*)?>!i', '*', $string);
     // Replace inline <a> tags with the text of link and a footnote.
     // 'See <a href="http://drupal.org">the Drupal site</a>' becomes
     // 'See the Drupal site [1]' with the URL included as a footnote.
     static::htmlToMailUrls(NULL, TRUE);
     $pattern = '@(<a[^>]+?href="([^"]*)"[^>]*?>(.+?)</a>)@i';
     $string = preg_replace_callback($pattern, 'static::htmlToMailUrls', $string);
     $urls = static::htmlToMailUrls();
     $footnotes = '';
     if (count($urls)) {
         $footnotes .= "\n";
         for ($i = 0, $max = count($urls); $i < $max; $i++) {
             $footnotes .= '[' . ($i + 1) . '] ' . $urls[$i] . "\n";
         }
     }
     // Split tags from text.
     $split = preg_split('/<([^>]+?)>/', $string, -1, PREG_SPLIT_DELIM_CAPTURE);
     // Note: PHP ensures the array consists of alternating delimiters and
     // literals and begins and ends with a literal (inserting $null as
     // required).
     // Odd/even counter (tag or no tag).
     $tag = FALSE;
     // Case conversion function.
     $casing = NULL;
     $output = '';
     // All current indentation string chunks.
     $indent = array();
     // Array of counters for opened lists.
     $lists = array();
     foreach ($split as $value) {
         // Holds a string ready to be formatted and output.
         $chunk = NULL;
         // Process HTML tags (but don't output any literally).
         if ($tag) {
             list($tagname) = explode(' ', strtolower($value), 2);
             switch ($tagname) {
                 // List counters.
                 case 'ul':
                     array_unshift($lists, '*');
                     break;
                 case 'ol':
                     array_unshift($lists, 1);
                     break;
                 case '/ul':
                 case '/ol':
                     array_shift($lists);
                     // Ensure blank new-line.
                     $chunk = '';
                     break;
                     // Quotation/list markers, non-fancy headers.
                 // Quotation/list markers, non-fancy headers.
                 case 'blockquote':
                     // Format=flowed indentation cannot be mixed with lists.
                     $indent[] = count($lists) ? ' "' : '>';
                     break;
                 case 'li':
                     $indent[] = isset($lists[0]) && is_numeric($lists[0]) ? ' ' . $lists[0]++ . ') ' : ' * ';
                     break;
                 case 'dd':
                     $indent[] = '    ';
                     break;
                 case 'h3':
                     $indent[] = '.... ';
                     break;
                 case 'h4':
                     $indent[] = '.. ';
                     break;
                 case '/blockquote':
                     if (count($lists)) {
                         // Append closing quote for inline quotes (immediately).
                         $output = rtrim($output, "> \n") . "\"\n";
                         // Ensure blank new-line.
                         $chunk = '';
                     }
                     // Fall-through.
                 // Fall-through.
                 case '/li':
                 case '/dd':
                     array_pop($indent);
                     break;
                 case '/h3':
                 case '/h4':
                     array_pop($indent);
                 case '/h5':
                 case '/h6':
                     // Ensure blank new-line.
                     $chunk = '';
                     break;
                     // Fancy headers.
                 // Fancy headers.
                 case 'h1':
                     $indent[] = '======== ';
                     $casing = 'drupal_strtoupper';
                     break;
                 case 'h2':
                     $indent[] = '-------- ';
                     $casing = 'drupal_strtoupper';
                     break;
                 case '/h1':
                 case '/h2':
                     $casing = NULL;
                     // Pad the line with dashes.
                     $output = static::htmlToTextPad($output, $tagname == '/h1' ? '=' : '-', ' ');
                     array_pop($indent);
                     // Ensure blank new-line.
                     $chunk = '';
                     break;
                     // Horizontal rulers.
                 // Horizontal rulers.
                 case 'hr':
                     // Insert immediately.
                     $output .= static::wrapMail('', implode('', $indent)) . "\n";
                     $output = static::htmlToTextPad($output, '-');
                     break;
                     // Paragraphs and definition lists.
                 // Paragraphs and definition lists.
                 case '/p':
                 case '/dl':
                     // Ensure blank new-line.
                     $chunk = '';
                     break;
             }
         } else {
             // Convert inline HTML text to plain text; not removing line-breaks or
             // white-space, since that breaks newlines when sanitizing plain-text.
             $value = trim(decode_entities($value));
             if (drupal_strlen($value)) {
                 $chunk = $value;
             }
         }
         // See if there is something waiting to be output.
         if (isset($chunk)) {
             // Apply any necessary case conversion.
             if (isset($casing)) {
                 $chunk = $casing($chunk);
             }
             $line_endings = Settings::get('mail_line_endings', PHP_EOL);
             // Format it and apply the current indentation.
             $output .= static::wrapMail($chunk, implode('', $indent)) . $line_endings;
             // Remove non-quotation markers from indentation.
             $indent = array_map('\\Drupal\\Core\\Mail\\MailFormatHelper::htmlToTextClean', $indent);
         }
         $tag = !$tag;
     }
     return $output . $footnotes;
 }
 /**
  * Transforms placeholders to BigPipe placeholders, either no-JS or JS.
  *
  * @param array $placeholders
  *   The placeholders to process.
  *
  * @return array
  *   The BigPipe placeholders.
  */
 protected function doProcessPlaceholders(array $placeholders)
 {
     $overridden_placeholders = [];
     foreach ($placeholders as $placeholder => $placeholder_elements) {
         // BigPipe uses JavaScript and the DOM to find the placeholder to replace.
         // This means finding the placeholder to replace must be efficient. Most
         // placeholders are HTML, which we can find efficiently thanks to the
         // querySelector API. But some placeholders are HTML attribute values or
         // parts thereof, and potentially even plain text in DOM text nodes. For
         // BigPipe's JavaScript to find those placeholders, it would need to
         // iterate over all DOM text nodes. This is highly inefficient. Therefore,
         // the BigPipe placeholder strategy only converts HTML placeholders into
         // BigPipe placeholders. The other placeholders need to be replaced on the
         // server, not via BigPipe.
         // @see \Drupal\Core\Access\RouteProcessorCsrf::renderPlaceholderCsrfToken()
         // @see \Drupal\Core\Form\FormBuilder::renderFormTokenPlaceholder()
         // @see \Drupal\Core\Form\FormBuilder::renderPlaceholderFormAction()
         if ($placeholder[0] !== '<' || $placeholder !== Html::normalize($placeholder)) {
             $overridden_placeholders[$placeholder] = static::createBigPipeNoJsPlaceholder($placeholder, $placeholder_elements, TRUE);
         } else {
             // If the current request/session doesn't have JavaScript, fall back to
             // no-JS BigPipe.
             if ($this->requestStack->getCurrentRequest()->cookies->has(static::NOJS_COOKIE)) {
                 $overridden_placeholders[$placeholder] = static::createBigPipeNoJsPlaceholder($placeholder, $placeholder_elements, FALSE);
             } else {
                 $overridden_placeholders[$placeholder] = static::createBigPipeJsPlaceholder($placeholder, $placeholder_elements);
             }
             $overridden_placeholders[$placeholder]['#cache']['contexts'][] = 'cookies:' . static::NOJS_COOKIE;
         }
     }
     return $overridden_placeholders;
 }
 /**
  * @covers ::render
  * @covers ::doRender
  * @covers \Drupal\Core\Render\RenderCache::get
  * @covers \Drupal\Core\Render\RenderCache::set
  * @covers \Drupal\Core\Render\RenderCache::createCacheID
  *
  * @dataProvider providerPlaceholders
  */
 public function testCacheableParent($test_element, $args, array $expected_placeholder_render_array, $placeholder_cid_parts, array $placeholder_expected_render_cache_array)
 {
     $element = $test_element;
     $this->setupMemoryCache();
     $this->setUpRequest('GET');
     $token = hash('crc32b', serialize($expected_placeholder_render_array));
     $expected_placeholder_markup = '<drupal-render-placeholder callback="Drupal\\Tests\\Core\\Render\\PlaceholdersTest::callback" arguments="0=' . $args[0] . '" token="' . $token . '"></drupal-render-placeholder>';
     $this->assertSame($expected_placeholder_markup, Html::normalize($expected_placeholder_markup), 'Placeholder unaltered by Html::normalize() which is used by FilterHtmlCorrector.');
     // GET request: #cache enabled, cache miss.
     $element['#cache'] = ['keys' => ['placeholder_test_GET']];
     $element['#prefix'] = '<p>#cache enabled, GET</p>';
     $output = $this->renderer->renderRoot($element);
     $this->assertSame('<p>#cache enabled, GET</p><p>This is a rendered placeholder!</p>', (string) $output, 'Output is overridden.');
     $this->assertTrue(isset($element['#printed']), 'No cache hit');
     $this->assertSame('<p>#cache enabled, GET</p><p>This is a rendered placeholder!</p>', (string) $element['#markup'], '#markup is overridden.');
     $expected_js_settings = ['foo' => 'bar', 'dynamic_animal' => $args[0]];
     $this->assertSame($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; both the original JavaScript setting and the one added by the placeholder #lazy_builder callback exist.');
     $this->assertPlaceholderRenderCache($placeholder_cid_parts, $placeholder_expected_render_cache_array);
     // GET request: validate cached data.
     $cached_element = $this->memoryCache->get('placeholder_test_GET')->data;
     $expected_element = ['#markup' => '<p>#cache enabled, GET</p>' . $expected_placeholder_markup, '#attached' => ['drupalSettings' => ['foo' => 'bar'], 'placeholders' => [$expected_placeholder_markup => ['#lazy_builder' => ['Drupal\\Tests\\Core\\Render\\PlaceholdersTest::callback', $args]]]], '#cache' => ['contexts' => [], 'tags' => [], 'max-age' => Cache::PERMANENT]];
     $expected_element['#attached']['placeholders'][$expected_placeholder_markup] = $expected_placeholder_render_array;
     $this->assertEquals($cached_element, $expected_element, 'The correct data is cached: the stored #markup and #attached properties are not affected by placeholder #lazy_builder callbacks.');
     // GET request: #cache enabled, cache hit.
     $element = $test_element;
     $element['#cache'] = ['keys' => ['placeholder_test_GET']];
     $element['#prefix'] = '<p>#cache enabled, GET</p>';
     $output = $this->renderer->renderRoot($element);
     $this->assertSame('<p>#cache enabled, GET</p><p>This is a rendered placeholder!</p>', (string) $output, 'Output is overridden.');
     $this->assertFalse(isset($element['#printed']), 'Cache hit');
     $this->assertSame('<p>#cache enabled, GET</p><p>This is a rendered placeholder!</p>', (string) $element['#markup'], '#markup is overridden.');
     $expected_js_settings = ['foo' => 'bar', 'dynamic_animal' => $args[0]];
     $this->assertSame($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; both the original JavaScript setting and the one added by the placeholder #lazy_builder callback exist.');
 }
 /**
  * Filters an HTML string to prevent XSS vulnerabilities.
  *
  * Like \Drupal\Component\Utility\Xss::filterAdmin(), but with a shorter list
  * of allowed tags.
  *
  * Used for items entered by administrators, like field descriptions, allowed
  * values, where some (mainly inline) mark-up may be desired (so
  * \Drupal\Component\Utility\String::checkPlain() is not acceptable).
  *
  * @param string $string
  *   The string with raw HTML in it.
  *
  * @return \Drupal\Component\Utility\SafeMarkup
  *   An XSS safe version of $string, or an empty string if $string is not
  *   valid UTF-8.
  */
 public function fieldFilterXss($string)
 {
     return SafeMarkup::set(Html::normalize(Xss::filter($string, $this->allowedTags())));
 }
 /**
  * {@inheritdoc}
  */
 public function process($text, $langcode)
 {
     return new FilterProcessResult(Html::normalize($text));
 }
 /**
  * Tests #post_render_cache placeholders.
  *
  * @covers ::render
  * @covers ::doRender
  * @covers ::cacheGet
  * @covers ::processPostRenderCache
  * @covers ::generateCachePlaceholder
  */
 public function testPlaceholder()
 {
     $this->setupMemoryCache();
     $context = ['bar' => $this->randomContextValue(), 'token' => \Drupal\Component\Utility\Crypt::randomBytesBase64(55)];
     $callback = __NAMESPACE__ . '\\PostRenderCache::placeholder';
     $placeholder = \Drupal::service('renderer')->generateCachePlaceholder($callback, $context);
     $this->assertSame($placeholder, Html::normalize($placeholder), 'Placeholder unaltered by Html::normalize() which is used by FilterHtmlCorrector.');
     $test_element = ['#post_render_cache' => [$callback => [$context]], '#markup' => $placeholder, '#prefix' => '<pre>', '#suffix' => '</pre>'];
     $expected_output = '<pre><bar>' . $context['bar'] . '</bar></pre>';
     // #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'];
     $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($output, $expected_output, 'Placeholder was replaced in output');
     $this->assertSame($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; JavaScript setting is added to page.');
     // GET request: validate cached data.
     $expected_token = $context['token'];
     $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 markup');
     $token = '';
     if ($nodes->length) {
         $token = $nodes->item(0)->getAttribute('token');
     }
     $this->assertSame($token, $expected_token, 'The tokens are identical');
     // 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: 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'];
     $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.');
 }