th `unfiltered_html` are filtered through `wp_filter_post_kses`
* (or not at all), where arbitrary classes are already permitted, and
* narrowing their markup here would restrict what core allows them to post.
*
* @since 7.1.0
* @access private
*
* @param string $content Slashed comment content, already filtered by kses.
* @return string Slashed comment content with span classes reduced.
*/
function _wp_kses_sanitize_note_mention_classes( $content ): string {
if ( ! is_string( $content ) ) {
$content = '';
}
if ( false === has_filter( 'pre_comment_content', 'wp_filter_kses' ) ) {
return $content;
}
$processor = new WP_HTML_Tag_Processor( wp_unslash( $content ) );
while ( $processor->next_tag( 'SPAN' ) ) {
foreach ( $processor->class_list() as $token ) {
if ( 'wp-note-mention' !== $token && ! preg_match( '/^user-[1-9][0-9]*$/', $token ) ) {
// Removing the last class also removes the attribute itself.
$processor->remove_class( $token );
}
}
}
return wp_slash( $processor->get_updated_html() );
}
/**
* You add any KSES hooks here.
*
* There is currently only one KSES WordPress hook, {@see 'pre_kses'}, and it is called here.
* All parameters are passed to the hooks and expected to receive a string.
*
* @since 1.0.0
*
* @param string $content Content to filter through KSES.
* @param array[]|string $allowed_html An array of allowed HTML elements and attributes,
* or a context name such as 'post'. See wp_kses_allowed_html()
* for the list of accepted context names.
* @param string[] $allowed_protocols Array of allowed URL protocols.
* @return string Filtered content through {@see 'pre_kses'} hook.
*/
function wp_kses_hook( $content, $allowed_html, $allowed_protocols ) {
/**
* Filters content to be run through KSES.
*
* @since 2.3.0
*
* @param string $content Content to filter through KSES.
* @param array[]|string $allowed_html An array of allowed HTML elements and attributes,
* or a context name such as 'post'. See wp_kses_allowed_html()
* for the list of accepted context names.
* @param string[] $allowed_protocols Array of allowed URL protocols.
*/
return apply_filters( 'pre_kses', $content, $allowed_html, $allowed_protocols );
}
/**
* Returns the version number of KSES.
*
* @since 1.0.0
*
* @return string KSES version number.
*/
function wp_kses_version() {
return '0.2.2';
}
/**
* Searches for HTML tags, no matter how malformed.
*
* It also matches stray `>` characters.
*
* @since 1.0.0
* @since 6.6.0 Recognize additional forms of invalid HTML which convert into comments.
*
* @global array[]|string $pass_allowed_html An array of allowed HTML elements and attributes,
* or a context name such as 'post'.
* @global string[] $pass_allowed_protocols Array of allowed URL protocols.
*
* @param string $content Content to filter.
* @param array[]|string $allowed_html An array of allowed HTML elements and attributes,
* or a context name such as 'post'. See wp_kses_allowed_html()
* for the list of accepted context names.
* @param string[] $allowed_protocols Array of allowed URL protocols.
* @return string Content with fixed HTML tags
*/
function wp_kses_split( $content, $allowed_html, $allowed_protocols ) {
global $pass_allowed_html, $pass_allowed_protocols;
$pass_allowed_html = $allowed_html;
$pass_allowed_protocols = $allowed_protocols;
$token_pattern = <<|$)) # - Normative HTML comments.
|
[^a-zA-Z][^>]*> # - Closing tags with invalid tag names.
|
]*> # - Invalid markup declaration nodes. Not all invalid nodes
# are matched so as to avoid breaking legacy behaviors.
)
|
(<[^>]*(>|$)|>) # Tag-like spans of text.
~x
REGEX;
return preg_replace_callback( $token_pattern, '_wp_kses_split_callback', $content );
}
/**
* Returns an array of HTML attribute names whose value contains a URL.
*
* This function returns a list of all HTML attributes that must contain
* a URL according to the HTML specification.
*
* This list includes URI attributes both allowed and disallowed by KSES.
*
* @link https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes
*
* @since 5.0.1
*
* @return string[] HTML attribute names whose value contains a URL.
*/
function wp_kses_uri_attributes() {
$uri_attributes = array(
'action',
'archive',
'background',
'cite',
'classid',
'codebase',
'data',
'formaction',
'href',
'icon',
'longdesc',
'manifest',
'poster',
'profile',
'src',
'usemap',
'xmlns',
);
/**
* Filters the list of attributes that are required to contain a URL.
*
* Use this filter to add any `data-` attributes that are required to be
* validated as a URL.
*
* @since 5.0.1
*
* @param string[] $uri_attributes HTML attribute names whose value contains a URL.
*/
$uri_attributes = apply_filters( 'wp_kses_uri_attributes', $uri_attributes );
return $uri_attributes;
}
/**
* Callback for `wp_kses_split()`.
*
* @since 3.1.0
* @access private
* @ignore
*
* @global array[]|string $pass_allowed_html An array of allowed HTML elements and attributes,
* or a context name such as 'post'.
* @global string[] $pass_allowed_protocols Array of allowed URL protocols.
*
* @param array $matches preg_replace regexp matches
* @return string
*/
function _wp_kses_split_callback( $matches ) {
global $pass_allowed_html, $pass_allowed_protocols;
return wp_kses_split2( $matches[0], $pass_allowed_html, $pass_allowed_protocols );
}
/**
* Callback for `wp_kses_split()` for fixing malformed HTML tags.
*
* This function does a lot of work. It rejects some very malformed things like
* `<:::>`. It returns an empty string, if the element isn't allowed (look ma, no
* `strip_tags()`!). Otherwise it splits the tag into an element and an attribute
* list.
*
* After the tag is split into an element and an attribute list, it is run
* through another filter which will remove illegal attributes and once that is
* completed, will be returned.
*
* @access private
* @ignore
* @since 1.0.0
* @since 6.6.0 Recognize additional forms of invalid HTML which convert into comments.
*
* @param string $content Content to filter.
* @param array[]|string $allowed_html An array of allowed HTML elements and attributes,
* or a context name such as 'post'. See wp_kses_allowed_html()
* for the list of accepted context names.
* @param string[] $allowed_protocols Array of allowed URL protocols.
*
* @return string Fixed HTML element
*/
function wp_kses_split2( $content, $allowed_html, $allowed_protocols ) {
$content = wp_kses_stripslashes( $content );
/*
* The regex pattern used to split HTML into chunks attempts
* to split on HTML token boundaries. This function should
* thus receive chunks that _either_ start with meaningful
* syntax tokens, like a tag `
` or a comment ``.
*
* If the first character of the `$content` chunk _isn't_ one
* of these syntax elements, which always starts with `<`, then
* the match had to be for the final alternation of `>`. In such
* case, it's probably standing on its own and could be encoded
* with a character reference to remove ambiguity.
*
* In other words, if this chunk isn't from a match of a syntax
* token, it's just a plaintext greater-than (`>`) sign.
*/
if ( ! str_starts_with( $content, '<' ) ) {
return '>';
}
/*
* When certain invalid syntax constructs appear, the HTML parser
* shifts into what's called the "bogus comment state." This is a
* plaintext state that consumes everything until the nearest `>`
* and then transforms the entire span into an HTML comment.
*
* Preserve these comments and do not treat them like tags.
*
* @see https://html.spec.whatwg.org/#bogus-comment-state
*/
if ( 1 === preg_match( '~^(?:[^a-zA-Z][^>]*>|]*>)$~', $content ) ) {
/**
* Since the pattern matches `…>` and also ``, this will
* preserve the type of the cleaned-up token in the output.
*/
$opener = $content[1];
$content = substr( $content, 2, -1 );
do {
$prev = $content;
$content = wp_kses( $content, $allowed_html, $allowed_protocols );
} while ( $prev !== $content );
// Recombine the modified inner content with the original token structure.
return "<{$opener}{$content}>";
}
/*
* Normative HTML comments should be handled separately as their
* parsing rules differ from those for tags and text nodes.
*/
if ( str_starts_with( $content, '' ), '', $content );
while ( ( $newstring = wp_kses( $content, $allowed_html, $allowed_protocols ) ) !== $content ) {
$content = $newstring;
}
if ( '' === $content ) {
return '';
}
// Prevent multiple dashes in comments.
$content = preg_replace( '/--+/', '-', $content );
// Prevent three dashes closing a comment.
$content = preg_replace( '/-$/', '', $content );
return "";
}
// It's seriously malformed.
if ( ! preg_match( '%^<\s*(/\s*)?([a-zA-Z0-9-]+)([^>]*)>?$%', $content, $matches ) ) {
return '';
}
$slash = trim( $matches[1] );
$elem = $matches[2];
$attrlist = $matches[3];
if ( ! is_array( $allowed_html ) ) {
$allowed_html = wp_kses_allowed_html( $allowed_html );
}
// They are using a not allowed HTML element.
if ( ! isset( $allowed_html[ strtolower( $elem ) ] ) ) {
return '';
}
// No attributes are allowed for closing elements.
if ( '' !== $slash ) {
return "$elem>";
}
return wp_kses_attr( $elem, $attrlist, $allowed_html, $allowed_protocols );
}
/**
* Removes all attributes, if none are allowed for this element.
*
* If some are allowed it calls `wp_kses_hair()` to split them further, and then
* it builds up new HTML code from the data that `wp_kses_hair()` returns. It also
* removes `<` and `>` characters, if there are any left. One more thing it does
* is to check if the tag has a closing XHTML slash, and if it does, it puts one
* in the returned code as well.
*
* An array of allowed values can be defined for attributes. If the attribute value
* doesn't fall into the list, the attribute will be removed from the tag.
*
* Attributes can be marked as required. If a required attribute is not present,
* KSES will remove all attributes from the tag. As KSES doesn't match opening and
* closing tags, it's not possible to safely remove the tag itself, the safest
* fallback is to strip all attributes from the tag, instead.
*
* @since 1.0.0
* @since 5.9.0 Added support for an array of allowed values for attributes.
* Added support for required attributes.
*
* @param string $element HTML element/tag.
* @param string $attr HTML attributes from HTML element to closing HTML element tag.
* @param array[]|string $allowed_html An array of allowed HTML elements and attributes,
* or a context name such as 'post'. See wp_kses_allowed_html()
* for the list of accepted context names.
* @param string[] $allowed_protocols Array of allowed URL protocols.
* @return string Sanitized HTML element.
*/
function wp_kses_attr( $element, $attr, $allowed_html, $allowed_protocols ) {
if ( ! is_array( $allowed_html ) ) {
$allowed_html = wp_kses_allowed_html( $allowed_html );
}
// Is there a closing XHTML slash at the end of the attributes?
$xhtml_slash = '';
if ( preg_match( '%\s*/\s*$%', $attr ) ) {
$xhtml_slash = ' /';
}
// Are any attributes allowed at all for this element?
$element_low = strtolower( $element );
if ( empty( $allowed_html[ $element_low ] ) || true === $allowed_html[ $element_low ] ) {
return "<$element$xhtml_slash>";
}
// Split it.
$attrarr = wp_kses_hair( $attr, $allowed_protocols );
// Check if there are attributes that are required.
$required_attrs = array_filter(
$allowed_html[ $element_low ],
static function ( $required_attr_limits ) {
return isset( $required_attr_limits['required'] ) && true === $required_attr_limits['required'];
}
);
/*
* If a required attribute check fails, we can return nothing for a self-closing tag,
* but for a non-self-closing tag the best option is to return the element with attributes,
* as KSES doesn't handle matching the relevant closing tag.
*/
$stripped_tag = '';
if ( empty( $xhtml_slash ) ) {
$stripped_tag = "<$element>";
}
// Go through $attrarr, and save the allowed attributes for this element in $attr2.
$attr2 = '';
foreach ( $attrarr as $arreach ) {
// Check if this attribute is required.
$required = isset( $required_attrs[ strtolower( $arreach['name'] ) ] );
if ( wp_kses_attr_check( $arreach['name'], $arreach['value'], $arreach['whole'], $arreach['vless'], $element, $allowed_html ) ) {
$attr2 .= ' ' . $arreach['whole'];
// If this was a required attribute, we can mark it as found.
if ( $required ) {
unset( $required_attrs[ strtolower( $arreach['name'] ) ] );
}
} elseif ( $required ) {
// This attribute was required, but didn't pass the check. The entire tag is not allowed.
return $stripped_tag;
}
}
// If some required attributes weren't set, the entire tag is not allowed.
if ( ! empty( $required_attrs ) ) {
return $stripped_tag;
}
// Remove any "<" or ">" characters.
$attr2 = preg_replace( '/[<>]/', '', $attr2 );
return "<$element$attr2$xhtml_slash>";
}
/**
* Determines whether an attribute is allowed.
*
* @since 4.2.3
* @since 5.0.0 Added support for `data-*` wildcard attributes.
*
* @param string $name The attribute name. Passed by reference. Returns empty string when not allowed.
* @param string $value The attribute value. Passed by reference. Returns a filtered value.
* @param string $whole The `name=value` input. Passed by reference. Returns filtered input.
* @param string $vless Whether the attribute is valueless. Use 'y' or 'n'.
* @param string $element The name of the element to which this attribute belongs.
* @param array $allowed_html The full list of allowed elements and attributes.
* @return bool Whether or not the attribute is allowed.
*/
function wp_kses_attr_check( &$name, &$value, &$whole, $vless, $element, $allowed_html ) {
$name_low = strtolower( $name );
$element_low = strtolower( $element );
if ( ! isset( $allowed_html[ $element_low ] ) ) {
$name = '';
$value = '';
$whole = '';
return false;
}
$allowed_attr = $allowed_html[ $element_low ];
if ( ! isset( $allowed_attr[ $name_low ] ) || '' === $allowed_attr[ $name_low ] ) {
/*
* Allow `data-*` attributes.
*
* When specifying `$allowed_html`, the attribute name should be set as
* `data-*` (not to be mixed with the HTML 4.0 `data` attribute, see
* https://www.w3.org/TR/html40/struct/objects.html#adef-data).
*
* Note: the attribute name should only contain `A-Za-z0-9_-` chars.
*/
if ( str_starts_with( $name_low, 'data-' ) && ! empty( $allowed_attr['data-*'] )
&& preg_match( '/^data-[a-z0-9_-]+$/', $name_low, $match )
) {
/*
* Add the whole attribute name to the allowed attributes and set any restrictions
* for the `data-*` attribute values for the current element.
*/
$allowed_attr[ $match[0] ] = $allowed_attr['data-*'];
} else {
$name = '';
$value = '';
$whole = '';
return false;
}
}
if ( 'style' === $name_low ) {
$decoded_value = WP_HTML_Decoder::decode_attribute( $value );
$new_value = safecss_filter_attr( $decoded_value );
if ( empty( $new_value ) ) {
$name = '';
$value = '';
$whole = '';
return false;
}
if ( $new_value !== $decoded_value ) {
$encoded_value = esc_attr( $new_value );
$whole = str_replace( $value, $encoded_value, $whole );
$value = $encoded_value;
}
}
if ( is_array( $allowed_attr[ $name_low ] ) ) {
// There are some checks.
foreach ( $allowed_attr[ $name_low ] as $currkey => $currval ) {
if ( ! wp_kses_check_attr_val( $value, $vless, $currkey, $currval ) ) {
$name = '';
$value = '';
$whole = '';
return false;
}
}
}
return true;
}
/**
* Given a string of HTML attributes and values, parse into a structured attribute list.
*
* This function performs a number of transformations while parsing attribute strings:
* - It normalizes attribute values and surrounds them with double quotes.
* - It normalizes HTML character references inside attribute values.
* - It removes “bad” URL protocols from attribute values.
*
* Otherwise this reads the attributes as if they were part of an HTML tag. It performs
* these transformations to lower the risk of mis-parsing down the line and to perform
* URL sanitization in line with the rest of the `kses` subsystem. Importantly, it does
* not decode the attribute values, meaning that special HTML syntax characters will
* be left with character references in the `value` property.
*
* Example:
*
* $attrs = wp_kses_hair( 'class="is-wide" inert data-lazy=\'<img>\' =/🐮=/' );
* $attrs === array(
* 'class' => array( 'name' => 'class', 'value' => 'is-wide', 'whole' => 'class="is-wide"', 'vless' => 'n' ),
* 'inert' => array( 'name' => 'inert', 'value' => '', 'whole' => 'inert', 'vless' => 'y' ),
* 'data-lazy' => array( 'name' => 'data-lazy', 'value' => '<img>', 'whole' => 'data-lazy="<img>"', 'vless' => 'n' ),
* '=' => array( 'name' => '=', 'value' => '', 'whole' => '=', 'vless' => 'y' ),
* '🐮' => array( 'name' => '🐮', 'value' => '/', 'whole' => '🐮="/"', 'vless' => 'n' ),
* );
*
* @since 1.0.0
* @since 7.0.0 Reliably parses HTML via the HTML API.
*
* @param string $attr Attribute list from HTML element to closing HTML element tag.
* @param string[] $allowed_protocols Array of allowed URL protocols.
* @return array Array of attribute information after parsing.
*/
function wp_kses_hair( $attr, $allowed_protocols ) {
$attributes = array();
$uris = wp_kses_uri_attributes();
$processor = new WP_HTML_Tag_Processor( "" );
$processor->next_token();
$attribute_names = $processor->get_attribute_names_with_prefix( '' );
if ( null === $attribute_names || 0 === count( $attribute_names ) ) {
return $attributes;
}
$syntax_characters = array(
'&' => '&',
'<' => '<',
'>' => '>',
"'" => ''',
'"' => '"',
);
foreach ( $attribute_names as $name ) {
$value = $processor->get_attribute( $name );
$is_bool = true === $value;
if ( is_string( $value ) && in_array( $name, $uris, true ) ) {
$value = wp_kses_bad_protocol( $value, $allowed_protocols );
}
// Reconstruct and normalize the attribute value.
$recoded = $is_bool ? '' : strtr( $value, $syntax_characters );
$whole = $is_bool ? $name : "{$name}=\"{$recoded}\"";
$attributes[ $name ] = array(
'name' => $name,
'value' => $recoded,
'whole' => $whole,
'vless' => $is_bool ? 'y' : 'n',
);
}
return $attributes;
}
/**
* Finds all attributes of an HTML element.
*
* Does not modify input. May return "evil" output.
*
* Based on `wp_kses_split2()` and `wp_kses_attr()`.
*
* @since 4.2.3
*
* @param string $element HTML element.
* @return array|false List of attributes found in the element. Returns false on failure.
*/
function wp_kses_attr_parse( $element ) {
$valid = preg_match( '%^(<\s*)(/\s*)?([a-zA-Z0-9]+\s*)([^>]*)(>?)$%', $element, $matches );
if ( 1 !== $valid ) {
return false;
}
$begin = $matches[1];
$slash = $matches[2];
$elname = $matches[3];
$attr = $matches[4];
$end = $matches[5];
if ( '' !== $slash ) {
// Closing elements do not get parsed.
return false;
}
// Is there a closing XHTML slash at the end of the attributes?
if ( 1 === preg_match( '%\s*/\s*$%', $attr, $matches ) ) {
$xhtml_slash = $matches[0];
$attr = substr( $attr, 0, -strlen( $xhtml_slash ) );
} else {
$xhtml_slash = '';
}
// Split it.
$attrarr = wp_kses_hair_parse( $attr );
if ( false === $attrarr ) {
return false;
}
// Make sure all input is returned by adding front and back matter.
array_unshift( $attrarr, $begin . $slash . $elname );
array_push( $attrarr, $xhtml_slash . $end );
return $attrarr;
}
/**
* Builds an attribute list from string containing attributes.
*
* Does not modify input. May return "evil" output.
* In case of unexpected input, returns false instead of stripping things.
*
* Based on `wp_kses_hair()` but does not return a multi-dimensional array.
*
* @since 4.2.3
*
* @param string $attr Attribute list from HTML element to closing HTML element tag.
* @return array|false List of attributes found in $attr. Returns false on failure.
*/
function wp_kses_hair_parse( $attr ) {
if ( '' === $attr ) {
return array();
}
$regex =
'(?:
[_a-zA-Z][-_a-zA-Z0-9:.]* # Attribute name.
|
\[\[?[^\[\]]+\]\]? # Shortcode in the name position implies unfiltered_html.
)
(?: # Attribute value.
\s*=\s* # All values begin with "=".
(?:
"[^"]*" # Double-quoted.
|
\'[^\']*\' # Single-quoted.
|
[^\s"\']+ # Non-quoted.
(?:\s|$) # Must have a space.
)
|
(?:\s|$) # If attribute has no value, space is required.
)
\s* # Trailing space is optional except as mentioned above.
';
/*
* Although it is possible to reduce this procedure to a single regexp,
* we must run that regexp twice to get exactly the expected result.
*
* Note: do NOT remove the `x` modifiers as they are essential for the above regex!
*/
$validation = "/^($regex)+$/x";
$extraction = "/$regex/x";
if ( 1 === preg_match( $validation, $attr ) ) {
preg_match_all( $extraction, $attr, $attrarr );
return $attrarr[0];
} else {
return false;
}
}
/**
* Performs different checks for attribute values.
*
* The currently implemented checks are "maxlen", "minlen", "maxval", "minval",
* and "valueless".
*
* @since 1.0.0
*
* @param string $value Attribute value.
* @param string $vless Whether the attribute is valueless. Use 'y' or 'n'.
* @param string $checkname What $checkvalue is checking for.
* @param mixed $checkvalue What constraint the value should pass.
* @return bool Whether check passes.
*/
function wp_kses_check_attr_val( $value, $vless, $checkname, $checkvalue ) {
$ok = true;
switch ( strtolower( $checkname ) ) {
case 'maxlen':
/*
* The maxlen check makes sure that the attribute value has a length not
* greater than the given value. This can be used to avoid Buffer Overflows
* in WWW clients and various Internet servers.
*/
if ( strlen( $value ) > $checkvalue ) {
$ok = false;
}
break;
case 'minlen':
/*
* The minlen check makes sure that the attribute value has a length not
* smaller than the given value.
*/
if ( strlen( $value ) < $checkvalue ) {
$ok = false;
}
break;
case 'maxval':
/*
* The maxval check does two things: it checks that the attribute value is
* an integer from 0 and up, without an excessive amount of zeroes or
* whitespace (to avoid Buffer Overflows). It also checks that the attribute
* value is not greater than the given value.
* This check can be used to avoid Denial of Service attacks.
*/
if ( ! preg_match( '/^\s{0,6}[0-9]{1,6}\s{0,6}$/', $value ) ) {
$ok = false;
}
if ( $value > $checkvalue ) {
$ok = false;
}
break;
case 'minval':
/*
* The minval check makes sure that the attribute value is a positive integer,
* and that it is not smaller than the given value.
*/
if ( ! preg_match( '/^\s{0,6}[0-9]{1,6}\s{0,6}$/', $value ) ) {
$ok = false;
}
if ( $value < $checkvalue ) {
$ok = false;
}
break;
case 'valueless':
/*
* The valueless check makes sure if the attribute has a value
* (like ``) or not (`