: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
* 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
* @return string[] HTML attribute names whose value contains a URL.
function wp_kses_uri_attributes() {
* 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
* @param string[] $uri_attributes HTML attribute names whose value contains a URL.
$uri_attributes = apply_filters( 'wp_kses_uri_attributes', $uri_attributes );
* Callback for `wp_kses_split()`.
* @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
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
* 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.
* @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 `<div>` 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, '<' ) ) {
* 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][^>]*>|<![a-z][^>]*>)$~', $content ) ) {
* Since the pattern matches `</…>` and also `<!…>`, this will
* preserve the type of the cleaned-up token in the output.
$content = substr( $content, 2, -1 );
$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 = str_replace( array( '<!--', '-->' ), '', $content );
while ( ( $newstring = wp_kses( $content, $allowed_html, $allowed_protocols ) ) !== $content ) {
// Prevent multiple dashes in comments.
$content = preg_replace( '/--+/', '-', $content );
// Prevent three dashes closing a comment.
$content = preg_replace( '/-$/', '', $content );
return "<!--{$content}-->";
// It's seriously malformed.
if ( ! preg_match( '%^<\s*(/\s*)?([a-zA-Z0-9-]+)([^>]*)>?$%', $content, $matches ) ) {
$slash = trim( $matches[1] );
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 ) ] ) ) {
// No attributes are allowed for closing elements.
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 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?
if ( preg_match( '%\s*/\s*$%', $attr ) ) {
// 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>";
$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.
if ( empty( $xhtml_slash ) ) {
$stripped_tag = "<$element>";
// Go through $attrarr, and save the allowed attributes for this element in $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.
unset( $required_attrs[ strtolower( $arreach['name'] ) ] );
// This attribute was required, but didn't pass the check. The entire tag is not allowed.
// If some required attributes weren't set, the entire tag is not allowed.
if ( ! empty( $required_attrs ) ) {
// Remove any "<" or ">" characters.
$attr2 = preg_replace( '/[<>]/', '', $attr2 );
return "<$element$attr2$xhtml_slash>";
* Determines whether an attribute is allowed.
* @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 ] ) ) {
$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-*'];
if ( 'style' === $name_low ) {
$new_value = safecss_filter_attr( $value );
if ( empty( $new_value ) ) {
$whole = str_replace( $value, $new_value, $whole );
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 ) ) {
* Builds an attribute list from string containing attributes.
* This function does a lot of work. It parses an attribute list into an array
* with attribute data, and tries to do the right thing even if it gets weird
* input. It will add quotes around attribute values that don't have any quotes
* or apostrophes around them, to make it easier to produce HTML code that will
* conform to W3C's HTML specification. It will also remove bad URL protocols
* from attribute values. It also reduces duplicate attributes by using the
* attribute defined first (`foo='bar' foo='baz'` will result in `foo='bar'`).
* @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 ) {
$uris = wp_kses_uri_attributes();
// Loop through the whole attribute list.
while ( strlen( $attr ) !== 0 ) {
$working = 0; // Was the last operation successful?
if ( preg_match( '/^([_a-zA-Z][-_a-zA-Z0-9:.]*)/', $attr, $match ) ) {
$attr = preg_replace( '/^[_a-zA-Z][-_a-zA-Z0-9:.]*/', '', $attr );
if ( preg_match( '/^\s*=\s*/', $attr ) ) { // Equals sign.
$attr = preg_replace( '/^\s*=\s*/', '', $attr );
if ( preg_match( '/^\s+/', $attr ) ) { // Valueless.
if ( false === array_key_exists( $attrname, $attrarr ) ) {
$attrarr[ $attrname ] = array(
$attr = preg_replace( '/^\s+/', '', $attr );
if ( preg_match( '%^"([^"]*)"(\s+|/?$)%', $attr, $match ) ) {
if ( in_array( strtolower( $attrname ), $uris, true ) ) {
$thisval = wp_kses_bad_protocol( $thisval, $allowed_protocols );
if ( false === array_key_exists( $attrname, $attrarr ) ) {
$attrarr[ $attrname ] = array(
'whole' => "$attrname=\"$thisval\"",
$attr = preg_replace( '/^"[^"]*"(\s+|$)/', '', $attr );
if ( preg_match( "%^'([^']*)'(\s+|/?$)%", $attr, $match ) ) {
if ( in_array( strtolower( $attrname ), $uris, true ) ) {
$thisval = wp_kses_bad_protocol( $thisval, $allowed_protocols );
if ( false === array_key_exists( $attrname, $attrarr ) ) {
$attrarr[ $attrname ] = array(
'whole' => "$attrname='$thisval'",
$attr = preg_replace( "/^'[^']*'(\s+|$)/", '', $attr );
if ( preg_match( "%^([^\s\"']+)(\s+|/?$)%", $attr, $match ) ) {
if ( in_array( strtolower( $attrname ), $uris, true ) ) {
$thisval = wp_kses_bad_protocol( $thisval, $allowed_protocols );
if ( false === array_key_exists( $attrname, $attrarr ) ) {
$attrarr[ $attrname ] = array(
'whole' => "$attrname=\"$thisval\"",
// We add quotes to conform to W3C's HTML spec.