Verified Commit 0347262d authored by Andrei Mateescu's avatar Andrei Mateescu
Browse files

task: #3226806 Move filter implementations from filter.module to plugin classes

By: claudiu.cristea
By: nicxvan
By: longwave
By: joachim
By: amateescu
By: larowlan
parent 597ab5ec
Loading
Loading
Loading
Loading
Loading
+85 −262
Original line number Diff line number Diff line
@@ -365,151 +365,35 @@ function template_preprocess_filter_tips(&$variables): void {
 *   www.example.com.
 * Each type must be processed separately, as there is no one regular
 * expression that could possibly match all of the cases in one pass.
 *
 * @deprecated in drupal:11.4.0 and is removed from drupal:13.0.0. The logic has
 *   been included in \Drupal\filter\Plugin\Filter\FilterUrl::process() and no
 *   replacement is provided.
 *
 * @see https://www.drupal.org/node/3566774
 */
function _filter_url($text, $filter) {
  // Store the current text in case any of the preg_* functions fail.
  $saved_text = $text;

  // Tags to skip and not recurse into.
  $ignore_tags = 'a|script|style|code|pre';

  // Pass length to regexp callback.
  _filter_url_trim(NULL, $filter->settings['filter_url_length']);

  // Create an array which contains the regexps for each type of link.
  // The key to the regexp is the name of a function that is used as
  // callback function to process matches of the regexp. The callback function
  // is to return the replacement for the match. The array is used and
  // matching/replacement done below inside some loops.
  $tasks = [];

  // Prepare protocols pattern for absolute URLs.
  // \Drupal\Component\Utility\UrlHelper::stripDangerousProtocols() will replace
  // any bad protocols with HTTP, so we need to support the identical list.
  // While '//' is technically optional for MAILTO only, we cannot cleanly
  // differ between protocols here without hard-coding MAILTO, so '//' is
  // optional for all protocols.
  // @see \Drupal\Component\Utility\UrlHelper::stripDangerousProtocols()
  $protocols = \Drupal::getContainer()->getParameter('filter_protocols');
  $protocols = implode(':(?://)?|', $protocols) . ':(?://)?';

  $valid_url_path_characters = "[\p{L}\p{M}\p{N}!\*\';:=\+,\.\$\/%#\[\]\-_~@&]";

  // Allow URL paths to contain balanced parens
  // 1. Used in Wikipedia URLs like /Primer_(film)
  // 2. Used in IIS sessions like /S(dfd346)/
  $valid_url_balanced_parens = '\(' . $valid_url_path_characters . '+\)';

  // Valid end-of-path characters (so /foo. does not gobble the period).
  // 1. Allow =&# for empty URL parameters and other URL-join artifacts
  $valid_url_ending_characters = '[\p{L}\p{M}\p{N}:_+~#=/]|(?:' . $valid_url_balanced_parens . ')';

  $valid_url_query_chars = '[a-zA-Z0-9!?\*\'@\(\);:&=\+\$\/%#\[\]\-_\.,~|]';
  $valid_url_query_ending_chars = '[a-zA-Z0-9_&=#\/]';

  // Full path
  // and allow @ in a URL, but only in the middle. Catch things like
  // http://example.com/@user/
  $valid_url_path = '(?:(?:' . $valid_url_path_characters . '*(?:' . $valid_url_balanced_parens . $valid_url_path_characters . '*)*' . $valid_url_ending_characters . ')|(?:@' . $valid_url_path_characters . '+\/))';

  // Prepare domain name pattern.
  // The ICANN seems to be on track towards accepting more diverse top level
  // domains (TLDs), so this pattern has been "future-proofed" to allow for
  // TLDs of length 2-64.
  $domain = '(?:[\p{L}\p{M}\p{N}._+-]+\.)?[\p{L}\p{M}]{2,64}\b';
  // Mail domains differ from the generic domain pattern, specifically:
  // A . character must be present in the string that follows the @ character.
  $email_domain = '(?:[\p{L}\p{M}\p{N}._+-]+\.)+[\p{L}\p{M}]{2,64}\b';
  $ip = '(?:[0-9]{1,3}\.){3}[0-9]{1,3}';
  $auth = '[\p{L}\p{M}\p{N}:%_+*~#?&=.,/;-]+@';
  $trail = '(' . $valid_url_path . '*)?(\\?' . $valid_url_query_chars . '*' . $valid_url_query_ending_chars . ')?';

  // Match absolute URLs.
  $url_pattern = "(?:$auth)?(?:$domain|$ip)/?(?:$trail)?";
  $pattern = "`((?:$protocols)(?:$url_pattern))`u";
  $tasks['_filter_url_parse_full_links'] = $pattern;

  // Match email addresses.
  $url_pattern = "[\p{L}\p{M}\p{N}._+-]{1,254}@(?:$email_domain)";
  $pattern = "`($url_pattern)`u";
  $tasks['_filter_url_parse_email_links'] = $pattern;

  // Match www domains.
  $url_pattern = "www\.(?:$domain)/?(?:$trail)?";
  $pattern = "`($url_pattern)`u";
  $tasks['_filter_url_parse_partial_links'] = $pattern;

  // Each type of URL needs to be processed separately. The text is joined and
  // re-split after each task, since all injected HTML tags must be correctly
  // protected before the next task.
  foreach ($tasks as $task => $pattern) {
    // HTML comments need to be handled separately, as they may contain HTML
    // markup, especially a '>'. Therefore, remove all comment contents and add
    // them back later.
    _filter_url_escape_comments('', TRUE);
    $text = is_null($text) ? '' : preg_replace_callback('`<!--(.*?)-->`s', '_filter_url_escape_comments', $text);

    // Split at all tags; ensures that no tags or attributes are processed.
    $chunks = is_null($text) ? [''] : preg_split('/(<.+?>)/is', $text, -1, PREG_SPLIT_DELIM_CAPTURE);

    // Do not attempt to convert links into URLs if preg_split() fails.
    if ($chunks !== FALSE) {
      // PHP ensures that the array consists of alternating delimiters and
      // literals, and begins and ends with a literal (inserting NULL as
      // required). Therefore, the first chunk is always text:
      $chunk_type = 'text';
      // If a tag of $ignore_tags is found, it is stored in $open_tag and only
      // removed when the closing tag is found. Until the closing tag is found,
      // no replacements are made.
      $open_tag = '';
      for ($i = 0; $i < count($chunks); $i++) {
        if ($chunk_type == 'text') {
          // Only process this text if there are no unclosed $ignore_tags.
          if ($open_tag == '') {
            // If there is a match, inject a link into this chunk via the
            // callback function contained in $task.
            $chunks[$i] = preg_replace_callback($pattern, $task, $chunks[$i]);
          }
          // Text chunk is done, so next chunk must be a tag.
          $chunk_type = 'tag';
        }
        else {
          // Only process this tag if there are no unclosed $ignore_tags.
          if ($open_tag == '') {
            // Check whether this tag is contained in $ignore_tags.
            if (preg_match("`<($ignore_tags)(?:\s|>)`i", $chunks[$i], $matches)) {
              $open_tag = $matches[1];
            }
          }
          // Otherwise, check whether this is the closing tag for $open_tag.
          else {
            if (preg_match("`<\/$open_tag>`i", $chunks[$i], $matches)) {
              $open_tag = '';
            }
          }
          // Tag chunk is done, so next chunk must be text.
          $chunk_type = 'text';
        }
      }

      $text = implode($chunks);
    }

    // Revert to the original comment contents.
    _filter_url_escape_comments('', FALSE);
    $text = $text ? preg_replace_callback('`<!--(.*?)-->`', '_filter_url_escape_comments', $text) : $text;
  }

  // If there is no text at this point revert to the previous text.
  return strlen((string) $text) > 0 ? $text : $saved_text;
  @trigger_error(__FUNCTION__ . '() is deprecated in drupal:11.4.0 and is removed from drupal:13.0.0. The logic has been included in \Drupal\filter\Plugin\Filter\FilterUrl::process() and no replacement is provided. See https://www.drupal.org/node/3566774', E_USER_DEPRECATED);
  $langcode = \Drupal::languageManager()->getCurrentLanguage()->getId();
  return \Drupal::service('plugin.manager.filter')
    ->createInstance('filter_url', ['settings' => $filter->settings])
    ->process($text, $langcode)
    ->getProcessedText();
}

/**
 * Makes links out of absolute URLs.
 *
 * Callback for preg_replace_callback() within _filter_url().
 *
 * @deprecated in drupal:11.4.0 and is removed from drupal:13.0.0. The logic has
 *   been included in \Drupal\filter\Plugin\Filter\FilterUrl::parseFullLinks()
 *   and no replacement is provided.
 *
 * @see https://www.drupal.org/node/3566774
 */
function _filter_url_parse_full_links($match) {
  @trigger_error(__FUNCTION__ . '() is deprecated in drupal:11.4.0 and is removed from drupal:13.0.0. The logic has been included in \Drupal\filter\Plugin\Filter\FilterUrl::parseFullLinks() and no replacement is provided. See https://www.drupal.org/node/3566774', E_USER_DEPRECATED);
  // The $i:th parenthesis in the regexp contains the URL.
  $i = 1;

@@ -523,8 +407,15 @@ function _filter_url_parse_full_links($match) {
 * Makes links out of email addresses.
 *
 * Callback for preg_replace_callback() within _filter_url().
 *
 * @deprecated in drupal:11.4.0 and is removed from drupal:13.0.0. The logic has
 *   been included in \Drupal\filter\Plugin\Filter\FilterUrl::parseEmailLinks()
 *   and no replacement is provided.
 *
 * @see https://www.drupal.org/node/3566774
 */
function _filter_url_parse_email_links($match) {
  @trigger_error(__FUNCTION__ . '() is deprecated in drupal:11.4.0 and is removed from drupal:13.0.0. The logic has been included in \Drupal\filter\Plugin\Filter\FilterUrl::parseEmailLinks() and no replacement is provided. See https://www.drupal.org/node/3566774', E_USER_DEPRECATED);
  // The $i:th parenthesis in the regexp contains the URL.
  $i = 0;

@@ -538,8 +429,16 @@ function _filter_url_parse_email_links($match) {
 * Makes links out of domain names starting with "www.".
 *
 * Callback for preg_replace_callback() within _filter_url().
 *
 * @deprecated in drupal:11.4.0 and is removed from drupal:13.0.0. The logic has
 *   been included in
 *   \Drupal\filter\Plugin\Filter\FilterUrl::parsePartialLinks() and no
 *   replacement is provided.
 *
 * @see https://www.drupal.org/node/3566774
 */
function _filter_url_parse_partial_links($match) {
  @trigger_error(__FUNCTION__ . '() is deprecated in drupal:11.4.0 and is removed from drupal:13.0.0. The logic has been included in \Drupal\filter\Plugin\Filter\FilterUrl::parsePartialLinks() and no replacement is provided. See https://www.drupal.org/node/3566774', E_USER_DEPRECATED);
  // The $i:th parenthesis in the regexp contains the URL.
  $i = 1;

@@ -561,8 +460,16 @@ function _filter_url_parse_partial_links($match) {
 *   (optional) A Boolean indicating whether to escape (TRUE) or unescape
 *   comments (FALSE). Defaults to NULL, indicating neither. If TRUE, statically
 *   cached $comments are reset.
 *
 * @deprecated in drupal:11.4.0 and is removed from drupal:13.0.0. The logic has
 *   been split between \Drupal\filter\Plugin\Filter\FilterUrl::escapeComments()
 *   and \Drupal\filter\Plugin\Filter\FilterUrl::unescapeComments(), and no
 *   replacement is provided.
 *
 * @see https://www.drupal.org/node/3566774
 */
function _filter_url_escape_comments($match, $escape = NULL) {
  @trigger_error(__FUNCTION__ . '() is deprecated in drupal:11.4.0 and is removed from drupal:13.0.0. The logic has been split between \Drupal\filter\Plugin\Filter\FilterUrl::escapeComments() and \Drupal\filter\Plugin\Filter\FilterUrl::unescapeComments(), and no replacement is provided. See https://www.drupal.org/node/3566774', E_USER_DEPRECATED);
  static $mode, $comments = [];

  if (isset($escape)) {
@@ -591,8 +498,15 @@ function _filter_url_escape_comments($match, $escape = NULL) {

/**
 * Shortens a long URL to a given length ending with an ellipsis.
 *
 * @deprecated in drupal:11.4.0 and is removed from drupal:13.0.0. The logic has
 *   been included in \Drupal\filter\Plugin\Filter\FilterUrl::trimUrl() and no
 *   replacement is provided.
 *
 * @see https://www.drupal.org/node/3566774
 */
function _filter_url_trim($text, $length = NULL) {
  @trigger_error(__FUNCTION__ . '() is deprecated in drupal:11.4.0 and is removed from drupal:13.0.0. The logic has been included in \Drupal\filter\Plugin\Filter\FilterUrl::trimUrl() and no replacement is provided. See https://www.drupal.org/node/3566774', E_USER_DEPRECATED);
  static $_length;
  if ($length !== NULL) {
    $_length = $length;
@@ -609,147 +523,56 @@ function _filter_url_trim($text, $length = NULL) {
 * Converts line breaks into <p> and <br> in an intelligent fashion.
 *
 * Based on: http://photomatt.net/scripts/autop
 *
 * @deprecated in drupal:11.4.0 and is removed from drupal:13.0.0. The logic has
 *   been included in \Drupal\filter\Plugin\Filter\FilterAutoP::process() and no
 *   replacement is provided.
 *
 * @see https://www.drupal.org/node/3566774
 */
function _filter_autop($text) {
  // All block level tags.
  $block = '(?:table|thead|tfoot|caption|col|colgroup|tbody|tr|td|th|div|dl|dd|dt|ul|ol|li|pre|select|option|form|map|area|blockquote|address|math|input|p|h[1-6]|fieldset|legend|hr|article|aside|details|figcaption|figure|footer|header|hgroup|menu|nav|section|summary)';

  // Split at opening and closing PRE, SCRIPT, STYLE, OBJECT, IFRAME tags
  // and comments. We don't apply any processing to the contents of these tags
  // to avoid messing up code. We look for matched pairs and allow basic
  // nesting. For example:
  // "processed<pre>ignored<script>ignored</script>ignored</pre>processed".
  $chunks = preg_split('@(<!--.*?-->|</?(?:pre|script|style|object|iframe|drupal-media|svg|!--)[^>]*>)@i', $text, -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).
  $ignore = FALSE;
  $ignore_tag = '';
  $output = '';
  foreach ($chunks as $i => $chunk) {
    if ($i % 2) {
      if (str_starts_with($chunk, '<!--')) {
        // Nothing to do, this is a comment.
        $output .= $chunk;
        continue;
      }
      // Opening or closing tag?
      $open = ($chunk[1] != '/');
      [$tag] = preg_split('/[ >]/', substr($chunk, 2 - $open), 2);
      if (!$ignore) {
        if ($open) {
          $ignore = TRUE;
          $ignore_tag = $tag;
        }
      }
      // Only allow a matching tag to close it.
      elseif (!$open && $ignore_tag == $tag) {
        $ignore = FALSE;
        $ignore_tag = '';
      }
    }
    elseif (!$ignore) {
      // Skip if the next chunk starts with Twig theme debug.
      // @see twig_render_template()
      if (isset($chunks[$i + 1]) && $chunks[$i + 1] === '<!-- THEME DEBUG -->') {
        $chunk = rtrim($chunk, "\n");
        $output .= $chunk;
        continue;
      }

      // Skip if the preceding chunk was the end of a Twig theme debug.
      // @see twig_render_template()
      if (isset($chunks[$i - 1])) {
        if (
          str_starts_with($chunks[$i - 1], '<!-- BEGIN OUTPUT from ')
          || str_starts_with($chunks[$i - 1], '<!-- 💡 BEGIN CUSTOM TEMPLATE OUTPUT from ')
        ) {
          $chunk = ltrim($chunk, "\n");
          $output .= $chunk;
          continue;
        }
      }

      // Just to make things a little easier, pad the end.
      $chunk = preg_replace('|\n*$|', '', $chunk) . "\n\n";
      $chunk = preg_replace('|<br />\s*<br />|', "\n\n", $chunk);
      // Space things out a little.
      $chunk = preg_replace('!(<' . $block . '[^>]*>)!', "\n$1", $chunk);
      // Space things out a little.
      $chunk = preg_replace('!(</' . $block . '>)!', "$1\n\n", $chunk);
      // Take care of duplicates.
      $chunk = preg_replace("/\n\n+/", "\n\n", $chunk);
      $chunk = preg_replace('/^\n|\n\s*\n$/', '', $chunk);
      // Make paragraphs, including one at the end.
      $chunk = '<p>' . preg_replace('/\n\s*\n\n?(.)/', "</p>\n<p>$1", $chunk) . "</p>\n";
      // Problem with nested lists.
      $chunk = preg_replace("|<p>(<li.+?)</p>|", "$1", $chunk);
      $chunk = preg_replace('|<p><blockquote([^>]*)>|i', "<blockquote$1><p>", $chunk);
      $chunk = str_replace('</blockquote></p>', '</p></blockquote>', $chunk);
      // Under certain strange conditions it could create a P of entirely
      // whitespace.
      $chunk = preg_replace('|<p>\s*</p>\n?|', '', $chunk);
      $chunk = preg_replace('!<p>\s*(</?' . $block . '[^>]*>)!', "$1", $chunk);
      $chunk = preg_replace('!(</?' . $block . '[^>]*>)\s*</p>!', "$1", $chunk);
      // Make line breaks.
      $chunk = preg_replace('|(?<!<br />)\s*\n|', "<br />\n", $chunk);
      $chunk = preg_replace('!(</?' . $block . '[^>]*>)\s*<br />!', "$1", $chunk);
      $chunk = preg_replace('!<br />(\s*</?(?:p|li|div|dl|dd|dt|th|pre|td|ul|ol)>)!', '$1', $chunk);
      $chunk = preg_replace('/&([^#])(?![A-Za-z0-9]{1,8};)/', '&amp;$1', $chunk);
    }
    $output .= $chunk;
  }
  return $output;
  @trigger_error(__FUNCTION__ . '() is deprecated in drupal:11.4.0 and is removed from drupal:13.0.0. The logic has been included in \Drupal\filter\Plugin\Filter\FilterAutoP::process() and no replacement is provided. See https://www.drupal.org/node/3566774', E_USER_DEPRECATED);
  $langcode = \Drupal::languageManager()->getCurrentLanguage()->getId();
  return \Drupal::service('plugin.manager.filter')
    ->createInstance('filter_autop')
    ->process($text, $langcode)
    ->getProcessedText();
}

/**
 * Escapes all HTML tags, so they will be visible instead of being effective.
 *
 * @deprecated in drupal:11.4.0 and is removed from drupal:13.0.0. The logic has
 *   been included in \Drupal\filter\Plugin\Filter\FilterHtmlEscape::process()
 *   and no replacement is provided.
 *
 * @see https://www.drupal.org/node/3566774
 */
function _filter_html_escape($text) {
  return trim(Html::escape($text));
  @trigger_error(__FUNCTION__ . '() is deprecated in drupal:11.4.0 and is removed from drupal:13.0.0. The logic has been included in \Drupal\filter\Plugin\Filter\FilterHtmlEscape::process() and no replacement is provided. See https://www.drupal.org/node/3566774', E_USER_DEPRECATED);
  $langcode = \Drupal::languageManager()->getCurrentLanguage()->getId();
  return \Drupal::service('plugin.manager.filter')
    ->createInstance('filter_html_escape')
    ->process($text, $langcode)
    ->getProcessedText();
}

/**
 * Process callback for local image filter.
 *
 * @deprecated in drupal:11.4.0 and is removed from drupal:13.0.0. The logic is
 *   included in \Drupal\filter\Plugin\Filter\FilterHtmlImageSecure::process()
 *   and no replacement is provided.
 *
 * @see https://www.drupal.org/node/3566774
 */
function _filter_html_image_secure_process($text) {
  // Find the path (e.g. '/') to Drupal root.
  $base_path = base_path();
  $base_path_length = mb_strlen($base_path);

  // Find the directory on the server where index.php resides.
  $local_dir = \Drupal::root() . '/';

  $html_dom = Html::load($text);
  $images = $html_dom->getElementsByTagName('img');

  /** @var \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator */
  $file_url_generator = \Drupal::service('file_url_generator');
  foreach ($images as $image) {
    $src = $image->getAttribute('src');
    // Transform absolute image URLs to relative image URLs: prevent problems on
    // multisite set-ups and prevent mixed content errors.
    $image->setAttribute('src', $file_url_generator->transformRelative($src));

    // Verify that $src starts with $base_path.
    // This also ensures that external images cannot be referenced.
    $src = $image->getAttribute('src');
    if (mb_substr($src, 0, $base_path_length) === $base_path) {
      // Remove the $base_path to get the path relative to the Drupal root.
      // Ensure the path refers to an actual image by prefixing the image source
      // with the Drupal root and running getimagesize() on it.
      $local_image_path = $local_dir . mb_substr($src, $base_path_length);
      $local_image_path = rawurldecode($local_image_path);
      if (@getimagesize($local_image_path)) {
        // The image has the right path. Erroneous images are dealt with below.
        continue;
      }
    }
    // Allow modules and themes to replace an invalid image with an error
    // indicator. See filter_filter_secure_image_alter().
    \Drupal::moduleHandler()->alter('filter_secure_image', $image);
  }
  $text = Html::serialize($html_dom);
  return $text;
  @trigger_error(__FUNCTION__ . '() is deprecated in drupal:11.4.0 and is removed from drupal:13.0.0. The logic is included in \Drupal\filter\Plugin\Filter\FilterHtmlImageSecure::process() and no replacement is provided. See https://www.drupal.org/node/3566774', E_USER_DEPRECATED);
  $langcode = \Drupal::languageManager()->getCurrentLanguage()->getId();
  return \Drupal::service('plugin.manager.filter')
    ->createInstance('filter_html_image_secure')
    ->process($text, $langcode)
    ->getProcessedText();
}

/**
+1 −1
Original line number Diff line number Diff line
@@ -69,7 +69,7 @@ public function help($route_name, RouteMatchInterface $route_match): ?string {
   *
   * Formats an image DOM element that has an invalid source.
   *
   * @see _filter_html_image_secure_process()
   * @see \Drupal\filter\Plugin\Filter\FilterHtmlImageSecure::process()
   */
  #[Hook('filter_secure_image_alter')]
  public function filterSecureImageAlter(&$image): void {
+89 −1
Original line number Diff line number Diff line
@@ -22,7 +22,95 @@ class FilterAutoP extends FilterBase {
   * {@inheritdoc}
   */
  public function process($text, $langcode) {
    return new FilterProcessResult(_filter_autop($text));
    // All block level tags.
    $block = '(?:table|thead|tfoot|caption|col|colgroup|tbody|tr|td|th|div|dl|dd|dt|ul|ol|li|pre|select|option|form|map|area|blockquote|address|math|input|p|h[1-6]|fieldset|legend|hr|article|aside|details|figcaption|figure|footer|header|hgroup|menu|nav|section|summary)';

    // Split at opening and closing PRE, SCRIPT, STYLE, OBJECT, IFRAME tags and
    // comments. We don't apply any processing to the contents of these tags to
    // avoid messing up code. We look for matched pairs and allow basic nesting.
    // For example,
    // "processed<pre>ignored<script>ignored</script>ignored</pre>processed".
    $chunks = preg_split('@(<!--.*?-->|</?(?:pre|script|style|object|iframe|drupal-media|svg|!--)[^>]*>)@i', $text, -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).
    $ignore = FALSE;
    $ignore_tag = '';
    $output = '';
    foreach ($chunks as $i => $chunk) {
      if ($i % 2) {
        if (str_starts_with($chunk, '<!--')) {
          // Nothing to do, this is a comment.
          $output .= $chunk;
          continue;
        }
        // Opening or closing tag?
        $open = ($chunk[1] != '/');
        [$tag] = preg_split('/[ >]/', substr($chunk, 2 - $open), 2);
        if (!$ignore) {
          if ($open) {
            $ignore = TRUE;
            $ignore_tag = $tag;
          }
        }
        // Only allow a matching tag to close it.
        elseif (!$open && $ignore_tag == $tag) {
          $ignore = FALSE;
          $ignore_tag = '';
        }
      }
      elseif (!$ignore) {
        // Skip if the next chunk starts with Twig theme debug.
        // @see twig_render_template()
        if (isset($chunks[$i + 1]) && $chunks[$i + 1] === '<!-- THEME DEBUG -->') {
          $chunk = rtrim($chunk, "\n");
          $output .= $chunk;
          continue;
        }

        // Skip if the preceding chunk was the end of a Twig theme debug.
        // @see \Drupal\Core\Template\TwigThemeEngine::renderTemplate()
        if (isset($chunks[$i - 1])) {
          if (
            str_starts_with($chunks[$i - 1], '<!-- BEGIN OUTPUT from ')
            || str_starts_with($chunks[$i - 1], '<!-- 💡 BEGIN CUSTOM TEMPLATE OUTPUT from ')
          ) {
            $chunk = ltrim($chunk, "\n");
            $output .= $chunk;
            continue;
          }
        }

        // Just to make things a little easier, pad the end.
        $chunk = preg_replace('|\n*$|', '', $chunk) . "\n\n";
        $chunk = preg_replace('|<br />\s*<br />|', "\n\n", $chunk);
        // Space things out a little.
        $chunk = preg_replace('!(<' . $block . '[^>]*>)!', "\n$1", $chunk);
        // Space things out a little.
        $chunk = preg_replace('!(</' . $block . '>)!', "$1\n\n", $chunk);
        // Take care of duplicates.
        $chunk = preg_replace("/\n\n+/", "\n\n", $chunk);
        $chunk = preg_replace('/^\n|\n\s*\n$/', '', $chunk);
        // Make paragraphs, including one at the end.
        $chunk = '<p>' . preg_replace('/\n\s*\n\n?(.)/', "</p>\n<p>$1", $chunk) . "</p>\n";
        // Problem with nested lists.
        $chunk = preg_replace("|<p>(<li.+?)</p>|", "$1", $chunk);
        $chunk = preg_replace('|<p><blockquote([^>]*)>|i', "<blockquote$1><p>", $chunk);
        $chunk = str_replace('</blockquote></p>', '</p></blockquote>', $chunk);
        // Under certain strange conditions it could create a P of entirely
        // whitespace.
        $chunk = preg_replace('|<p>\s*</p>\n?|', '', $chunk);
        $chunk = preg_replace('!<p>\s*(</?' . $block . '[^>]*>)!', "$1", $chunk);
        $chunk = preg_replace('!(</?' . $block . '[^>]*>)\s*</p>!', "$1", $chunk);
        // Make line breaks.
        $chunk = preg_replace('|(?<!<br />)\s*\n|', "<br />\n", $chunk);
        $chunk = preg_replace('!(</?' . $block . '[^>]*>)\s*<br />!', "$1", $chunk);
        $chunk = preg_replace('!<br />(\s*</?(?:p|li|div|dl|dd|dt|th|pre|td|ul|ol)>)!', '$1', $chunk);
        $chunk = preg_replace('/&([^#])(?![A-Za-z0-9]{1,8};)/', '&amp;$1', $chunk);
      }
      $output .= $chunk;
    }

    return new FilterProcessResult($output);
  }

  /**
+2 −1
Original line number Diff line number Diff line
@@ -2,6 +2,7 @@

namespace Drupal\filter\Plugin\Filter;

use Drupal\Component\Utility\Html;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\filter\Attribute\Filter;
use Drupal\filter\FilterProcessResult;
@@ -23,7 +24,7 @@ class FilterHtmlEscape extends FilterBase {
   * {@inheritdoc}
   */
  public function process($text, $langcode) {
    return new FilterProcessResult(_filter_html_escape($text));
    return new FilterProcessResult(trim(Html::escape($text)));
  }

  /**
+86 −2

File changed.

Preview size limit exceeded, changes collapsed.

Loading