filter.module

Same filename and directory in other branches
  1. 7.x modules/filter/filter.module
  2. 9 core/modules/filter/filter.module
  3. 8.9.x core/modules/filter/filter.module
  4. 10 core/modules/filter/filter.module

File

core/modules/filter/filter.module

View source
<?php


/**
 * @file
 */

use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Template\Attribute;
use Drupal\filter\FilterFormatInterface;
use Drupal\user\Entity\Role;
use Drupal\user\RoleInterface;

/**
 * Retrieves a list of enabled text formats, ordered by weight.
 *
 * @param \Drupal\Core\Session\AccountInterface|null $account
 *   (optional) If provided, only those formats that are allowed for this user
 *   account will be returned. All enabled formats will be returned otherwise.
 *   Defaults to NULL.
 *
 * @return \Drupal\filter\FilterFormatInterface[]
 *   An array of text format objects, keyed by the format ID and ordered by
 *   weight.
 *
 * @see filter_formats_reset()
 */
function filter_formats(?AccountInterface $account = NULL) {
  $formats =& drupal_static(__FUNCTION__, []);
  // All available formats are cached for performance.
  if (!isset($formats['all'])) {
    $language_interface = \Drupal::languageManager()->getCurrentLanguage();
    if ($cache = \Drupal::cache()->get("filter_formats:{$language_interface->getId()}")) {
      $formats['all'] = $cache->data;
    }
    else {
      $formats['all'] = \Drupal::entityTypeManager()->getStorage('filter_format')
        ->loadByProperties([
        'status' => TRUE,
      ]);
      uasort($formats['all'], 'Drupal\\Core\\Config\\Entity\\ConfigEntityBase::sort');
      \Drupal::cache()->set("filter_formats:{$language_interface->getId()}", $formats['all'], Cache::PERMANENT, \Drupal::entityTypeManager()->getDefinition('filter_format')
        ->getListCacheTags());
    }
  }
  // If no user was specified, return all formats.
  if (!isset($account)) {
    return $formats['all'];
  }
  // Build a list of user-specific formats.
  $account_id = $account->id();
  if (!isset($formats['user'][$account_id])) {
    $formats['user'][$account_id] = [];
    foreach ($formats['all'] as $format) {
      if ($format->access('use', $account)) {
        $formats['user'][$account_id][$format->id()] = $format;
      }
    }
  }
  return $formats['user'][$account_id];
}

/**
 * Resets the text format caches.
 *
 * @see filter_formats()
 */
function filter_formats_reset() : void {
  drupal_static_reset('filter_formats');
}

/**
 * Retrieves a list of roles that are allowed to use a given text format.
 *
 * @param \Drupal\filter\FilterFormatInterface $format
 *   An object representing the text format.
 *
 * @return array
 *   An array of role names, keyed by role ID.
 */
function filter_get_roles_by_format(FilterFormatInterface $format) : array {
  // Handle the fallback format upfront (all roles have access to this format).
  if ($format->isFallbackFormat()) {
    return array_map(fn(RoleInterface $role) => $role->label(), Role::loadMultiple());
  }
  // Do not list any roles if the permission does not exist.
  $permission = $format->getPermissionName();
  if (empty($permission)) {
    return [];
  }
  $roles = array_filter(Role::loadMultiple(), fn(RoleInterface $role) => $role->hasPermission($permission));
  return array_map(fn(RoleInterface $role) => $role->label(), $roles);
}

/**
 * Retrieves a list of text formats that are allowed for a given role.
 *
 * @param string $rid
 *   The user role ID to retrieve text formats for.
 *
 * @return \Drupal\filter\FilterFormatInterface[]
 *   An array of text format objects that are allowed for the role, keyed by
 *   the text format ID and ordered by weight.
 */
function filter_get_formats_by_role($rid) : array {
  $formats = [];
  foreach (filter_formats() as $format) {
    $roles = filter_get_roles_by_format($format);
    if (isset($roles[$rid])) {
      $formats[$format->id()] = $format;
    }
  }
  return $formats;
}

/**
 * Returns the ID of the default text format for a particular user.
 *
 * The default text format is the first available format that the user is
 * allowed to access, when the formats are ordered by weight. It should
 * generally be used as a default choice when presenting the user with a list
 * of possible text formats (for example, in a node creation form).
 *
 * Conversely, when existing content that does not have an assigned text format
 * needs to be filtered for display, the default text format is the wrong
 * choice, because it is not guaranteed to be consistent from user to user, and
 * some trusted users may have an unsafe text format set by default, which
 * should not be used on text of unknown origin. Instead, the fallback format
 * returned by filter_fallback_format() should be used, since that is intended
 * to be a safe, consistent format that is always available to all users.
 *
 * @param \Drupal\Core\Session\AccountInterface|null $account
 *   (optional) The user account to check. Defaults to the currently logged-in
 *   user. Defaults to NULL.
 *
 * @return string
 *   The ID of the user's default text format.
 *
 * @see filter_fallback_format()
 */
function filter_default_format(?AccountInterface $account = NULL) {
  if (!isset($account)) {
    $account = \Drupal::currentUser();
  }
  // Get a list of formats for this user, ordered by weight. The first one
  // available is the user's default format.
  $formats = filter_formats($account);
  $format = reset($formats);
  return $format->id();
}

/**
 * Returns the ID of the fallback text format that all users have access to.
 *
 * The fallback text format is a regular text format in every respect, except
 * it does not participate in the filter permission system and cannot be
 * disabled. It needs to exist because any user who has permission to create
 * formatted content must always have at least one text format they can use.
 *
 * Because the fallback format is available to all users, it should always be
 * configured securely. For example, when the Filter module is installed, this
 * format is initialized to output plain text. Installation profiles and site
 * administrators have the freedom to configure it further.
 *
 * Note that the fallback format is completely distinct from the default format,
 * which differs per user and is simply the first format which that user has
 * access to. The default and fallback formats are only guaranteed to be the
 * same for users who do not have access to any other format; otherwise, the
 * fallback format's weight determines its placement with respect to the user's
 * other formats.
 *
 * Any modules implementing a format deletion functionality must not delete this
 * format.
 *
 * @return string|null
 *   The ID of the fallback text format.
 *
 * @see hook_filter_format_disable()
 * @see filter_default_format()
 */
function filter_fallback_format() {
  // This variable is automatically set in the database for all installations
  // of Drupal. In the event that it gets disabled or deleted somehow, there
  // is no safe default to return, since we do not want to risk making an
  // existing (and potentially unsafe) text format on the site automatically
  // available to all users. Returning NULL at least guarantees that this
  // cannot happen.
  return \Drupal::config('filter.settings')->get('fallback_format');
}

/**
 * Runs all the enabled filters on a piece of text.
 *
 * Note: Because filters can inject JavaScript or execute PHP code, security is
 * vital here. When a user supplies a text format, you should validate it using
 * $format->access() before accepting/using it. This is normally done in the
 * validation stage of the Form API. You should for example never make a
 * preview of content in a disallowed format.
 *
 * Note: this function should only be used when filtering text for use elsewhere
 * than on a rendered HTML page. If this is part of an HTML page, then a
 * renderable array with a #type 'processed_text' element should be used instead
 * of this, because that will allow cacheability metadata to be set and bubbled
 * up and attachments to be associated (assets, placeholders, etc.). In other
 * words: if you are presenting the filtered text in an HTML page, the only way
 * this will be presented correctly, is by using the 'processed_text' element.
 *
 * @param string $text
 *   The text to be filtered.
 * @param string|null $format_id
 *   (optional) The machine name of the filter format to be used to filter the
 *   text. Defaults to the fallback format. See filter_fallback_format().
 * @param string $langcode
 *   (optional) The language code of the text to be filtered, e.g. 'en' for
 *   English. This allows filters to be language-aware so language-specific
 *   text replacement can be implemented. Defaults to an empty string.
 * @param array $filter_types_to_skip
 *   (optional) An array of filter types to skip, or an empty array (default)
 *   to skip no filter types. All of the format's filters will be applied,
 *   except for filters of the types that are marked to be skipped.
 *   FilterInterface::TYPE_HTML_RESTRICTOR is the only type that cannot be
 *   skipped.
 *
 * @return \Drupal\Component\Render\MarkupInterface
 *   The filtered text.
 *
 * @see \Drupal\filter\Plugin\FilterInterface::process()
 *
 * @ingroup sanitization
 */
function check_markup($text, $format_id = NULL, $langcode = '', $filter_types_to_skip = []) {
  $build = [
    '#type' => 'processed_text',
    '#text' => $text,
    '#format' => $format_id,
    '#filter_types_to_skip' => $filter_types_to_skip,
    '#langcode' => $langcode,
  ];
  return \Drupal::service('renderer')->renderInIsolation($build);
}

/**
 * Retrieves the filter tips.
 *
 * @param string $format_id
 *   The ID of the text format for which to retrieve tips, or -1 to return tips
 *   for all formats accessible to the current user.
 * @param bool $long
 *   (optional) Boolean indicating whether the long form of tips should be
 *   returned. Defaults to FALSE.
 *
 * @return array
 *   An associative array of filtering tips, keyed by filter name. Each
 *   filtering tip is an associative array with elements:
 *   - tip: Tip text.
 *   - id: Filter ID.
 */
function _filter_tips($format_id, $long = FALSE) : array {
  $formats = filter_formats(\Drupal::currentUser());
  $tips = [];
  // If only listing one format, extract it from the $formats array.
  if ($format_id != -1) {
    $formats = [
      $formats[$format_id],
    ];
  }
  foreach ($formats as $format) {
    foreach ($format->filters() as $name => $filter) {
      if ($filter->status) {
        $tip = $filter->tips($long);
        if (isset($tip)) {
          $tips[$format->label()][$name] = [
            'tip' => [
              '#markup' => $tip,
            ],
            'id' => $name,
          ];
        }
      }
    }
  }
  return $tips;
}

/**
 * Prepares variables for text format guideline templates.
 *
 * Default template: filter-guidelines.html.twig.
 *
 * @param array $variables
 *   An associative array containing:
 *   - format: An object representing a text format.
 */
function template_preprocess_filter_guidelines(&$variables) : void {
  $format = $variables['format'];
  $variables['tips'] = [
    '#theme' => 'filter_tips',
    '#tips' => _filter_tips($format->id(), FALSE),
  ];
  // Add format id for filter.js.
  $variables['attributes']['data-drupal-format-id'] = $format->id();
}

/**
 * Prepares variables for text format wrapper templates.
 *
 * Default template: text-format-wrapper.html.twig.
 *
 * @param array $variables
 *   An associative array containing:
 *   - attributes: An associative array containing properties of the element.
 */
function template_preprocess_text_format_wrapper(&$variables) : void {
  $variables['aria_description'] = FALSE;
  // Add element class and id for screen readers.
  if (isset($variables['attributes']['aria-describedby'])) {
    $variables['aria_description'] = TRUE;
    $variables['attributes']['id'] = $variables['attributes']['aria-describedby'];
    // Remove aria-describedby attribute as it shouldn't be visible here.
    unset($variables['attributes']['aria-describedby']);
  }
}

/**
 * Prepares variables for filter tips templates.
 *
 * Default template: filter-tips.html.twig.
 *
 * @param array $variables
 *   An associative array containing:
 *   - tips: An array containing descriptions and a CSS ID in the form of
 *     'module-name/filter-id' (only used when $long is TRUE) for each
 *     filter in one or more text formats. Example:
 *     @code
 *       [
 *         'Full HTML' => [
 *           0 => [
 *             'tip' => 'Web page addresses and email addresses turn into links automatically.',
 *             'id' => 'filter/2',
 *           ],
 *         ],
 *       ];
 *     @endcode
 *   - long: (optional) Whether the passed-in filter tips contain extended
 *     explanations, i.e. intended to be output on the path 'filter/tips'
 *     (TRUE), or are in a short format, i.e. suitable to be displayed below a
 *     form element. Defaults to FALSE.
 */
function template_preprocess_filter_tips(&$variables) : void {
  $tips = $variables['tips'];
  foreach ($variables['tips'] as $name => $tip_list) {
    foreach ($tip_list as $tip_key => $tip) {
      $tip_list[$tip_key]['attributes'] = new Attribute();
    }
    $variables['tips'][$name] = [
      'attributes' => new Attribute(),
      'name' => $name,
      'list' => $tip_list,
    ];
  }
  $variables['multiple'] = count($tips) > 1;
}

/**
 * @defgroup standard_filters Standard filters
 * @{
 * Filters implemented by the Filter module.
 */

/**
 * Converts text into hyperlinks automatically.
 *
 * This filter identifies and makes clickable three types of "links".
 * - URLs like http://example.com.
 * - Email addresses like name@example.com.
 * - Web addresses without the "http://" protocol defined, like
 *   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.
 */
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];
            }
          }
          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;
}

/**
 * Makes links out of absolute URLs.
 *
 * Callback for preg_replace_callback() within _filter_url().
 */
function _filter_url_parse_full_links($match) {
  // The $i:th parenthesis in the regexp contains the URL.
  $i = 1;
  $match[$i] = Html::decodeEntities($match[$i]);
  $caption = Html::escape(_filter_url_trim($match[$i]));
  $match[$i] = Html::escape($match[$i]);
  return '<a href="' . $match[$i] . '">' . $caption . '</a>';
}

/**
 * Makes links out of email addresses.
 *
 * Callback for preg_replace_callback() within _filter_url().
 */
function _filter_url_parse_email_links($match) {
  // The $i:th parenthesis in the regexp contains the URL.
  $i = 0;
  $match[$i] = Html::decodeEntities($match[$i]);
  $caption = Html::escape(_filter_url_trim($match[$i]));
  $match[$i] = Html::escape($match[$i]);
  return '<a href="mailto:' . $match[$i] . '">' . $caption . '</a>';
}

/**
 * Makes links out of domain names starting with "www.".
 *
 * Callback for preg_replace_callback() within _filter_url().
 */
function _filter_url_parse_partial_links($match) {
  // The $i:th parenthesis in the regexp contains the URL.
  $i = 1;
  $match[$i] = Html::decodeEntities($match[$i]);
  $caption = Html::escape(_filter_url_trim($match[$i]));
  $match[$i] = Html::escape($match[$i]);
  return '<a href="http://' . $match[$i] . '">' . $caption . '</a>';
}

/**
 * Escapes the contents of HTML comments.
 *
 * Callback for preg_replace_callback() within _filter_url().
 *
 * @param array $match
 *   An array containing matches to replace from preg_replace_callback(),
 *   whereas $match[1] is expected to contain the content to be filtered.
 * @param bool|null $escape
 *   (optional) A Boolean indicating whether to escape (TRUE) or unescape
 *   comments (FALSE). Defaults to NULL, indicating neither. If TRUE, statically
 *   cached $comments are reset.
 */
function _filter_url_escape_comments($match, $escape = NULL) {
  static $mode, $comments = [];
  if (isset($escape)) {
    $mode = $escape;
    if ($escape) {
      $comments = [];
    }
    return;
  }
  // Replace all HTML comments with a '<!-- [hash] -->' placeholder.
  if ($mode) {
    $content = $match[1];
    $hash = hash('sha256', $content);
    $comments[$hash] = $content;
    return "<!-- {$hash} -->";
  }
  else {
    $hash = $match[1];
    $hash = trim($hash);
    $content = $comments[$hash];
    return "<!--{$content}-->";
  }
}

/**
 * Shortens a long URL to a given length ending with an ellipsis.
 */
function _filter_url_trim($text, $length = NULL) {
  static $_length;
  if ($length !== NULL) {
    $_length = $length;
  }
  if (isset($text) && isset($_length)) {
    $text = Unicode::truncate($text, $_length, FALSE, TRUE);
  }
  return $text;
}

/**
 * Converts line breaks into <p> and <br> in an intelligent fashion.
 *
 * Based on: http://photomatt.net/scripts/autop
 */
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;
        }
      }
      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;
}

/**
 * Escapes all HTML tags, so they will be visible instead of being effective.
 */
function _filter_html_escape($text) {
  return trim(Html::escape($text));
}

/**
 * Process callback for local image filter.
 */
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;
}

/**
 * @} End of "defgroup standard_filters".
 */

Functions

Title Deprecated Summary
check_markup Runs all the enabled filters on a piece of text.
filter_default_format Returns the ID of the default text format for a particular user.
filter_fallback_format Returns the ID of the fallback text format that all users have access to.
filter_formats Retrieves a list of enabled text formats, ordered by weight.
filter_formats_reset Resets the text format caches.
filter_get_formats_by_role Retrieves a list of text formats that are allowed for a given role.
filter_get_roles_by_format Retrieves a list of roles that are allowed to use a given text format.
template_preprocess_filter_guidelines Prepares variables for text format guideline templates.
template_preprocess_filter_tips Prepares variables for filter tips templates.
template_preprocess_text_format_wrapper Prepares variables for text format wrapper templates.
_filter_autop Converts line breaks into <p> and <br> in an intelligent fashion.
_filter_html_escape Escapes all HTML tags, so they will be visible instead of being effective.
_filter_html_image_secure_process Process callback for local image filter.
_filter_tips Retrieves the filter tips.
_filter_url Converts text into hyperlinks automatically.
_filter_url_escape_comments Escapes the contents of HTML comments.
_filter_url_parse_email_links Makes links out of email addresses.
_filter_url_parse_full_links Makes links out of absolute URLs.
_filter_url_parse_partial_links Makes links out of domain names starting with "www.".
_filter_url_trim Shortens a long URL to a given length ending with an ellipsis.

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.