filter.module
Same filename in other branches
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() {
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) {
// 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) {
$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) {
$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) {
$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) {
$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) {
$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};)/', '&$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.