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