diff -r 7b1b88e27a20 -r 48c4eec2b7e6 wp/wp-includes/kses.php --- a/wp/wp-includes/kses.php Thu Sep 29 08:06:27 2022 +0200 +++ b/wp/wp-includes/kses.php Fri Sep 05 18:40:08 2025 +0200 @@ -36,6 +36,13 @@ * Using `CUSTOM_TAGS` is not recommended and should be considered deprecated. The * {@see 'wp_kses_allowed_html'} filter is more powerful and supplies context. * + * When using this constant, make sure to set all of these globals to arrays: + * + * - `$allowedposttags` + * - `$allowedtags` + * - `$allowedentitynames` + * - `$allowedxmlentitynames` + * * @see wp_kses_allowed_html() * @since 1.2.0 * @@ -685,6 +692,33 @@ $allowedposttags = array_map( '_wp_add_global_attributes', $allowedposttags ); } else { + $required_kses_globals = array( + 'allowedposttags', + 'allowedtags', + 'allowedentitynames', + 'allowedxmlentitynames', + ); + $missing_kses_globals = array(); + + foreach ( $required_kses_globals as $global_name ) { + if ( ! isset( $GLOBALS[ $global_name ] ) || ! is_array( $GLOBALS[ $global_name ] ) ) { + $missing_kses_globals[] = '$' . $global_name . ''; + } + } + + if ( $missing_kses_globals ) { + _doing_it_wrong( + 'wp_kses_allowed_html', + sprintf( + /* translators: 1: CUSTOM_TAGS, 2: Global variable names. */ + __( 'When using the %1$s constant, make sure to set these globals to an array: %2$s.' ), + 'CUSTOM_TAGS', + implode( ', ', $missing_kses_globals ) + ), + '6.2.0' + ); + } + $allowedtags = wp_kses_array_lc( $allowedtags ); $allowedposttags = wp_kses_array_lc( $allowedposttags ); } @@ -702,23 +736,24 @@ * * @since 1.0.0 * - * @param string $string Text content to filter. + * @param string $content Text 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. + * @param string[] $allowed_protocols Optional. Array of allowed URL protocols. + * Defaults to the result of wp_allowed_protocols(). * @return string Filtered content containing only the allowed HTML. */ -function wp_kses( $string, $allowed_html, $allowed_protocols = array() ) { +function wp_kses( $content, $allowed_html, $allowed_protocols = array() ) { if ( empty( $allowed_protocols ) ) { $allowed_protocols = wp_allowed_protocols(); } - $string = wp_kses_no_null( $string, array( 'slash_zero' => 'keep' ) ); - $string = wp_kses_normalize_entities( $string ); - $string = wp_kses_hook( $string, $allowed_html, $allowed_protocols ); + $content = wp_kses_no_null( $content, array( 'slash_zero' => 'keep' ) ); + $content = wp_kses_normalize_entities( $content ); + $content = wp_kses_hook( $content, $allowed_html, $allowed_protocols ); - return wp_kses_split( $string, $allowed_html, $allowed_protocols ); + return wp_kses_split( $content, $allowed_html, $allowed_protocols ); } /** @@ -728,43 +763,45 @@ * * @since 4.2.3 * - * @param string $string The 'whole' attribute, including name and value. + * @param string $attr The 'whole' attribute, including name and value. * @param string $element The HTML element name to which the attribute belongs. * @return string Filtered attribute. */ -function wp_kses_one_attr( $string, $element ) { +function wp_kses_one_attr( $attr, $element ) { $uris = wp_kses_uri_attributes(); $allowed_html = wp_kses_allowed_html( 'post' ); $allowed_protocols = wp_allowed_protocols(); - $string = wp_kses_no_null( $string, array( 'slash_zero' => 'keep' ) ); + $attr = wp_kses_no_null( $attr, array( 'slash_zero' => 'keep' ) ); // Preserve leading and trailing whitespace. $matches = array(); - preg_match( '/^\s*/', $string, $matches ); + preg_match( '/^\s*/', $attr, $matches ); $lead = $matches[0]; - preg_match( '/\s*$/', $string, $matches ); + preg_match( '/\s*$/', $attr, $matches ); $trail = $matches[0]; if ( empty( $trail ) ) { - $string = substr( $string, strlen( $lead ) ); + $attr = substr( $attr, strlen( $lead ) ); } else { - $string = substr( $string, strlen( $lead ), -strlen( $trail ) ); + $attr = substr( $attr, strlen( $lead ), -strlen( $trail ) ); } // Parse attribute name and value from input. - $split = preg_split( '/\s*=\s*/', $string, 2 ); + $split = preg_split( '/\s*=\s*/', $attr, 2 ); $name = $split[0]; - if ( count( $split ) == 2 ) { + if ( count( $split ) === 2 ) { $value = $split[1]; - // Remove quotes surrounding $value. - // Also guarantee correct quoting in $string for this one attribute. + /* + * Remove quotes surrounding $value. + * Also guarantee correct quoting in $attr for this one attribute. + */ if ( '' === $value ) { $quote = ''; } else { $quote = $value[0]; } if ( '"' === $quote || "'" === $quote ) { - if ( substr( $value, -1 ) != $quote ) { + if ( ! str_ends_with( $value, $quote ) ) { return ''; } $value = substr( $value, 1, -1 ); @@ -780,18 +817,18 @@ $value = wp_kses_bad_protocol( $value, $allowed_protocols ); } - $string = "$name=$quote$value$quote"; - $vless = 'n'; + $attr = "$name=$quote$value$quote"; + $vless = 'n'; } else { $value = ''; $vless = 'y'; } // Sanitize attribute by name. - wp_kses_attr_check( $name, $value, $string, $vless, $element, $allowed_html ); + wp_kses_attr_check( $name, $value, $attr, $vless, $element, $allowed_html ); // Restore whitespace. - return $lead . $string . $trail; + return $lead . $attr . $trail; } /** @@ -887,26 +924,26 @@ * * @since 1.0.0 * - * @param string $string Content to filter through KSES. + * @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( $string, $allowed_html, $allowed_protocols ) { +function wp_kses_hook( $content, $allowed_html, $allowed_protocols ) { /** * Filters content to be run through KSES. * * @since 2.3.0 * - * @param string $string Content to filter through KSES. + * @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', $string, $allowed_html, $allowed_protocols ); + return apply_filters( 'pre_kses', $content, $allowed_html, $allowed_protocols ); } /** @@ -926,25 +963,40 @@ * 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 $string Content to filter. + * @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( $string, $allowed_html, $allowed_protocols ) { +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; - return preg_replace_callback( '%(|$))|(<[^>]*(>|$)|>)%', '_wp_kses_split_callback', $string ); + $token_pattern = <<|$)) # - Normative HTML comments. + | + ]*> # - 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 ); } /** @@ -1008,13 +1060,13 @@ * or a context name such as 'post'. * @global string[] $pass_allowed_protocols Array of allowed URL protocols. * - * @param array $match preg_replace regexp matches + * @param array $matches preg_replace regexp matches * @return string */ -function _wp_kses_split_callback( $match ) { +function _wp_kses_split_callback( $matches ) { global $pass_allowed_html, $pass_allowed_protocols; - return wp_kses_split2( $match[0], $pass_allowed_html, $pass_allowed_protocols ); + return wp_kses_split2( $matches[0], $pass_allowed_html, $pass_allowed_protocols ); } /** @@ -1032,40 +1084,90 @@ * @access private * @ignore * @since 1.0.0 + * @since 6.6.0 Recognize additional forms of invalid HTML which convert into comments. * - * @param string $string Content to filter. + * @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( $string, $allowed_html, $allowed_protocols ) { - $string = wp_kses_stripslashes( $string ); +function wp_kses_split2( $content, $allowed_html, $allowed_protocols ) { + $content = wp_kses_stripslashes( $content ); - // It matched a ">" character. - if ( '<' !== substr( $string, 0, 1 ) ) { + /* + * 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 '>'; } - // Allow HTML comments. - if ( '' ), '', $string ); - while ( ( $newstring = wp_kses( $string, $allowed_html, $allowed_protocols ) ) != $string ) { - $string = $newstring; + /* + * 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( '~^(?:]*>|]*>)$~', $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 ( '' === $string ) { + + if ( '' === $content ) { return ''; } + // Prevent multiple dashes in comments. - $string = preg_replace( '/--+/', '-', $string ); + $content = preg_replace( '/--+/', '-', $content ); // Prevent three dashes closing a comment. - $string = preg_replace( '/-$/', '', $string ); - return ""; + $content = preg_replace( '/-$/', '', $content ); + + return ""; } // It's seriously malformed. - if ( ! preg_match( '%^<\s*(/\s*)?([a-zA-Z0-9-]+)([^>]*)>?$%', $string, $matches ) ) { + if ( ! preg_match( '%^<\s*(/\s*)?([a-zA-Z0-9-]+)([^>]*)>?$%', $content, $matches ) ) { return ''; } @@ -1142,7 +1244,7 @@ // Check if there are attributes that are required. $required_attrs = array_filter( $allowed_html[ $element_low ], - function( $required_attr_limits ) { + static function ( $required_attr_limits ) { return isset( $required_attr_limits['required'] ) && true === $required_attr_limits['required']; } ); @@ -1222,11 +1324,10 @@ * `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, - * double hyphens `--` are not accepted by WordPress. + * Note: the attribute name should only contain `A-Za-z0-9_-` chars. */ - if ( strpos( $name_low, 'data-' ) === 0 && ! empty( $allowed_attr['data-*'] ) - && preg_match( '/^data(?:-[a-z0-9_]+)+$/', $name_low, $match ) + 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 @@ -1295,7 +1396,7 @@ // Loop through the whole attribute list. - while ( strlen( $attr ) != 0 ) { + while ( strlen( $attr ) !== 0 ) { $working = 0; // Was the last operation successful? switch ( $mode ) { @@ -1320,6 +1421,7 @@ if ( preg_match( '/^\s+/', $attr ) ) { // Valueless. $working = 1; $mode = 0; + if ( false === array_key_exists( $attrname, $attrarr ) ) { $attrarr[ $attrname ] = array( 'name' => $attrname, @@ -1328,6 +1430,7 @@ 'vless' => 'y', ); } + $attr = preg_replace( '/^\s+/', '', $attr ); } @@ -1349,6 +1452,7 @@ 'vless' => 'n', ); } + $working = 1; $mode = 0; $attr = preg_replace( '/^"[^"]*"(\s+|$)/', '', $attr ); @@ -1370,6 +1474,7 @@ 'vless' => 'n', ); } + $working = 1; $mode = 0; $attr = preg_replace( "/^'[^']*'(\s+|$)/", '', $attr ); @@ -1391,6 +1496,7 @@ 'vless' => 'n', ); } + // We add quotes to conform to W3C's HTML spec. $working = 1; $mode = 0; @@ -1400,15 +1506,17 @@ break; } // End switch. - if ( 0 == $working ) { // Not well-formed, remove and try again. + if ( 0 === $working ) { // Not well-formed, remove and try again. $attr = wp_kses_html_error( $attr ); $mode = 0; } } // End while. - if ( 1 == $mode && false === array_key_exists( $attrname, $attrarr ) ) { - // Special case, for when the attribute list ends with a valueless - // attribute like "selected". + if ( 1 === $mode && false === array_key_exists( $attrname, $attrarr ) ) { + /* + * Special case, for when the attribute list ends with a valueless + * attribute like "selected". + */ $attrarr[ $attrname ] = array( 'name' => $attrname, 'value' => '', @@ -1488,34 +1596,37 @@ return array(); } - // phpcs:disable Squiz.Strings.ConcatenationSpacing.PaddingFound -- don't remove regex indentation $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. - // phpcs:enable + '(?: + [_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. + /* + * 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)+$%"; - $extraction = "%$regex%"; + $validation = "/^($regex)+$/x"; + $extraction = "/$regex/x"; if ( 1 === preg_match( $validation, $attr ) ) { preg_match_all( $extraction, $attr, $attrarr ); @@ -1605,7 +1716,7 @@ * If the given value is an "n" or an "N", the attribute must have a value. */ - if ( strtolower( $checkvalue ) != $vless ) { + if ( strtolower( $checkvalue ) !== $vless ) { $ok = false; } break; @@ -1646,24 +1757,33 @@ * * @since 1.0.0 * - * @param string $string Content to filter bad protocols from. + * @param string $content Content to filter bad protocols from. * @param string[] $allowed_protocols Array of allowed URL protocols. * @return string Filtered content. */ -function wp_kses_bad_protocol( $string, $allowed_protocols ) { - $string = wp_kses_no_null( $string ); +function wp_kses_bad_protocol( $content, $allowed_protocols ) { + $content = wp_kses_no_null( $content ); + + // Short-circuit if the string starts with `https://` or `http://`. Most common cases. + if ( + ( str_starts_with( $content, 'https://' ) && in_array( 'https', $allowed_protocols, true ) ) || + ( str_starts_with( $content, 'http://' ) && in_array( 'http', $allowed_protocols, true ) ) + ) { + return $content; + } + $iterations = 0; do { - $original_string = $string; - $string = wp_kses_bad_protocol_once( $string, $allowed_protocols ); - } while ( $original_string != $string && ++$iterations < 6 ); + $original_content = $content; + $content = wp_kses_bad_protocol_once( $content, $allowed_protocols ); + } while ( $original_content !== $content && ++$iterations < 6 ); - if ( $original_string != $string ) { + if ( $original_content !== $content ) { return ''; } - return $string; + return $content; } /** @@ -1673,21 +1793,21 @@ * * @since 1.0.0 * - * @param string $string Content to filter null characters from. + * @param string $content Content to filter null characters from. * @param array $options Set 'slash_zero' => 'keep' when '\0' is allowed. Default is 'remove'. * @return string Filtered content. */ -function wp_kses_no_null( $string, $options = null ) { +function wp_kses_no_null( $content, $options = null ) { if ( ! isset( $options['slash_zero'] ) ) { $options = array( 'slash_zero' => 'remove' ); } - $string = preg_replace( '/[\x00-\x08\x0B\x0C\x0E-\x1F]/', '', $string ); + $content = preg_replace( '/[\x00-\x08\x0B\x0C\x0E-\x1F]/', '', $content ); if ( 'remove' === $options['slash_zero'] ) { - $string = preg_replace( '/\\\\+0+/', '', $string ); + $content = preg_replace( '/\\\\+0+/', '', $content ); } - return $string; + return $content; } /** @@ -1698,11 +1818,11 @@ * * @since 1.0.0 * - * @param string $string String to strip slashes from. + * @param string $content String to strip slashes from. * @return string Fixed string with quoted slashes. */ -function wp_kses_stripslashes( $string ) { - return preg_replace( '%\\\\"%', '"', $string ); +function wp_kses_stripslashes( $content ) { + return preg_replace( '%\\\\"%', '"', $content ); } /** @@ -1737,11 +1857,11 @@ * * @since 1.0.0 * - * @param string $string + * @param string $attr * @return string */ -function wp_kses_html_error( $string ) { - return preg_replace( '/^("[^"]*("|$)|\'[^\']*(\'|$)|\S)*\s*/', '', $string ); +function wp_kses_html_error( $attr ) { + return preg_replace( '/^("[^"]*("|$)|\'[^\']*(\'|$)|\S)*\s*/', '', $attr ); } /** @@ -1752,30 +1872,31 @@ * * @since 1.0.0 * - * @param string $string Content to check for bad protocols. + * @param string $content Content to check for bad protocols. * @param string[] $allowed_protocols Array of allowed URL protocols. * @param int $count Depth of call recursion to this function. * @return string Sanitized content. */ -function wp_kses_bad_protocol_once( $string, $allowed_protocols, $count = 1 ) { - $string = preg_replace( '/(�*58(?![;0-9])|�*3a(?![;a-f0-9]))/i', '$1;', $string ); - $string2 = preg_split( '/:|�*58;|�*3a;|:/i', $string, 2 ); - if ( isset( $string2[1] ) && ! preg_match( '%/\?%', $string2[0] ) ) { - $string = trim( $string2[1] ); - $protocol = wp_kses_bad_protocol_once2( $string2[0], $allowed_protocols ); +function wp_kses_bad_protocol_once( $content, $allowed_protocols, $count = 1 ) { + $content = preg_replace( '/(�*58(?![;0-9])|�*3a(?![;a-f0-9]))/i', '$1;', $content ); + $content2 = preg_split( '/:|�*58;|�*3a;|:/i', $content, 2 ); + + if ( isset( $content2[1] ) && ! preg_match( '%/\?%', $content2[0] ) ) { + $content = trim( $content2[1] ); + $protocol = wp_kses_bad_protocol_once2( $content2[0], $allowed_protocols ); if ( 'feed:' === $protocol ) { if ( $count > 2 ) { return ''; } - $string = wp_kses_bad_protocol_once( $string, $allowed_protocols, ++$count ); - if ( empty( $string ) ) { - return $string; + $content = wp_kses_bad_protocol_once( $content, $allowed_protocols, ++$count ); + if ( empty( $content ) ) { + return $content; } } - $string = $protocol . $string; + $content = $protocol . $content; } - return $string; + return $content; } /** @@ -1789,26 +1910,26 @@ * @ignore * @since 1.0.0 * - * @param string $string URI scheme to check against the list of allowed protocols. + * @param string $scheme URI scheme to check against the list of allowed protocols. * @param string[] $allowed_protocols Array of allowed URL protocols. * @return string Sanitized content. */ -function wp_kses_bad_protocol_once2( $string, $allowed_protocols ) { - $string2 = wp_kses_decode_entities( $string ); - $string2 = preg_replace( '/\s/', '', $string2 ); - $string2 = wp_kses_no_null( $string2 ); - $string2 = strtolower( $string2 ); +function wp_kses_bad_protocol_once2( $scheme, $allowed_protocols ) { + $scheme = wp_kses_decode_entities( $scheme ); + $scheme = preg_replace( '/\s/', '', $scheme ); + $scheme = wp_kses_no_null( $scheme ); + $scheme = strtolower( $scheme ); $allowed = false; foreach ( (array) $allowed_protocols as $one_protocol ) { - if ( strtolower( $one_protocol ) == $string2 ) { + if ( strtolower( $one_protocol ) === $scheme ) { $allowed = true; break; } } if ( $allowed ) { - return "$string2:"; + return "$scheme:"; } else { return ''; } @@ -1826,25 +1947,25 @@ * @since 1.0.0 * @since 5.5.0 Added `$context` parameter. * - * @param string $string Content to normalize entities. + * @param string $content Content to normalize entities. * @param string $context Context for normalization. Can be either 'html' or 'xml'. * Default 'html'. * @return string Content with normalized entities. */ -function wp_kses_normalize_entities( $string, $context = 'html' ) { +function wp_kses_normalize_entities( $content, $context = 'html' ) { // Disarm all entities by converting & to & - $string = str_replace( '&', '&', $string ); + $content = str_replace( '&', '&', $content ); // Change back the allowed entities in our list of allowed entities. if ( 'xml' === $context ) { - $string = preg_replace_callback( '/&([A-Za-z]{2,8}[0-9]{0,2});/', 'wp_kses_xml_named_entities', $string ); + $content = preg_replace_callback( '/&([A-Za-z]{2,8}[0-9]{0,2});/', 'wp_kses_xml_named_entities', $content ); } else { - $string = preg_replace_callback( '/&([A-Za-z]{2,8}[0-9]{0,2});/', 'wp_kses_named_entities', $string ); + $content = preg_replace_callback( '/&([A-Za-z]{2,8}[0-9]{0,2});/', 'wp_kses_named_entities', $content ); } - $string = preg_replace_callback( '/&#(0*[0-9]{1,7});/', 'wp_kses_normalize_entities2', $string ); - $string = preg_replace_callback( '/&#[Xx](0*[0-9A-Fa-f]{1,6});/', 'wp_kses_normalize_entities3', $string ); + $content = preg_replace_callback( '/&#(0*[0-9]{1,7});/', 'wp_kses_normalize_entities2', $content ); + $content = preg_replace_callback( '/&#[Xx](0*[0-9A-Fa-f]{1,6});/', 'wp_kses_normalize_entities3', $content ); - return $string; + return $content; } /** @@ -1923,6 +2044,7 @@ } $i = $matches[1]; + if ( valid_unicode( $i ) ) { $i = str_pad( ltrim( $i, '0' ), 3, '0', STR_PAD_LEFT ); $i = "&#$i;"; @@ -1952,6 +2074,7 @@ } $hexchars = $matches[1]; + return ( ! valid_unicode( hexdec( $hexchars ) ) ) ? "&#x$hexchars;" : '&#x' . ltrim( $hexchars, '0' ) . ';'; } @@ -1964,10 +2087,13 @@ * @return bool Whether or not the codepoint is a valid Unicode codepoint. */ function valid_unicode( $i ) { - return ( 0x9 == $i || 0xa == $i || 0xd == $i || - ( 0x20 <= $i && $i <= 0xd7ff ) || - ( 0xe000 <= $i && $i <= 0xfffd ) || - ( 0x10000 <= $i && $i <= 0x10ffff ) ); + $i = (int) $i; + + return ( 0x9 === $i || 0xa === $i || 0xd === $i || + ( 0x20 <= $i && $i <= 0xd7ff ) || + ( 0xe000 <= $i && $i <= 0xfffd ) || + ( 0x10000 <= $i && $i <= 0x10ffff ) + ); } /** @@ -1979,14 +2105,14 @@ * * @since 1.0.0 * - * @param string $string Content to change entities. + * @param string $content Content to change entities. * @return string Content after decoded entities. */ -function wp_kses_decode_entities( $string ) { - $string = preg_replace_callback( '/&#([0-9]+);/', '_wp_kses_decode_entities_chr', $string ); - $string = preg_replace_callback( '/&#[Xx]([0-9A-Fa-f]+);/', '_wp_kses_decode_entities_chr_hexdec', $string ); +function wp_kses_decode_entities( $content ) { + $content = preg_replace_callback( '/&#([0-9]+);/', '_wp_kses_decode_entities_chr', $content ); + $content = preg_replace_callback( '/&#[Xx]([0-9A-Fa-f]+);/', '_wp_kses_decode_entities_chr_hexdec', $content ); - return $string; + return $content; } /** @@ -1996,11 +2122,11 @@ * @access private * @ignore * - * @param array $match preg match + * @param array $matches preg match * @return string */ -function _wp_kses_decode_entities_chr( $match ) { - return chr( $match[1] ); +function _wp_kses_decode_entities_chr( $matches ) { + return chr( $matches[1] ); } /** @@ -2010,11 +2136,11 @@ * @access private * @ignore * - * @param array $match preg match + * @param array $matches preg match * @return string */ -function _wp_kses_decode_entities_chr_hexdec( $match ) { - return chr( hexdec( $match[1] ) ); +function _wp_kses_decode_entities_chr_hexdec( $matches ) { + return chr( hexdec( $matches[1] ) ); } /** @@ -2081,7 +2207,7 @@ ) { unset( $decoded_data['isGlobalStylesUserThemeJSON'] ); - $data_to_encode = WP_Theme_JSON::remove_insecure_properties( $decoded_data ); + $data_to_encode = WP_Theme_JSON::remove_insecure_properties( $decoded_data, 'custom' ); $data_to_encode['isGlobalStylesUserThemeJSON'] = true; return wp_slash( wp_json_encode( $data_to_encode ) ); @@ -2224,10 +2350,21 @@ * @since 5.1.0 Added support for `text-transform`. * @since 5.2.0 Added support for `background-position` and `grid-template-columns`. * @since 5.3.0 Added support for `grid`, `flex` and `column` layout properties. - * Extend `background-*` support of individual properties. + * Extended `background-*` support for individual properties. * @since 5.3.1 Added support for gradient backgrounds. * @since 5.7.1 Added support for `object-position`. * @since 5.8.0 Added support for `calc()` and `var()` values. + * @since 6.1.0 Added support for `min()`, `max()`, `minmax()`, `clamp()`, + * nested `var()` values, and assigning values to CSS variables. + * Added support for `object-fit`, `gap`, `column-gap`, `row-gap`, and `flex-wrap`. + * Extended `margin-*` and `padding-*` support for logical properties. + * @since 6.2.0 Added support for `aspect-ratio`, `position`, `top`, `right`, `bottom`, `left`, + * and `z-index` CSS properties. + * @since 6.3.0 Extended support for `filter` to accept a URL and added support for repeat(). + * Added support for `box-shadow`. + * @since 6.4.0 Added support for `writing-mode`. + * @since 6.5.0 Added support for `background-repeat`. + * @since 6.6.0 Added support for `grid-column`, `grid-row`, and `container-type`. * * @param string $css A string of CSS rules. * @param string $deprecated Not used. @@ -2259,6 +2396,7 @@ 'background-color', 'background-image', 'background-position', + 'background-repeat', 'background-size', 'background-attachment', 'background-blend-mode', @@ -2333,12 +2471,20 @@ 'margin-bottom', 'margin-left', 'margin-top', + 'margin-block-start', + 'margin-block-end', + 'margin-inline-start', + 'margin-inline-end', 'padding', 'padding-right', 'padding-bottom', 'padding-left', 'padding-top', + 'padding-block-start', + 'padding-block-end', + 'padding-inline-start', + 'padding-inline-end', 'flex', 'flex-basis', @@ -2346,16 +2492,23 @@ 'flex-flow', 'flex-grow', 'flex-shrink', + 'flex-wrap', + + 'gap', + 'column-gap', + 'row-gap', 'grid-template-columns', 'grid-auto-columns', 'grid-column-start', 'grid-column-end', + 'grid-column', 'grid-column-gap', 'grid-template-rows', 'grid-auto-rows', 'grid-row-start', 'grid-row-end', + 'grid-row', 'grid-row-gap', 'grid-gap', @@ -2371,9 +2524,24 @@ 'direction', 'float', 'list-style-type', + 'object-fit', 'object-position', 'overflow', 'vertical-align', + 'writing-mode', + + 'position', + 'top', + 'right', + 'bottom', + 'left', + 'z-index', + 'box-shadow', + 'aspect-ratio', + 'container-type', + + // Custom CSS properties. + '--*', ) ); @@ -2390,6 +2558,7 @@ 'background-image', 'cursor', + 'filter', 'list-style', 'list-style-image', @@ -2419,18 +2588,31 @@ $found = false; $url_attr = false; $gradient_attr = false; + $is_custom_var = false; - if ( strpos( $css_item, ':' ) === false ) { + if ( ! str_contains( $css_item, ':' ) ) { $found = true; } else { $parts = explode( ':', $css_item, 2 ); $css_selector = trim( $parts[0] ); + // Allow assigning values to CSS variables. + if ( in_array( '--*', $allowed_attr, true ) && preg_match( '/^--[a-zA-Z0-9-_]+$/', $css_selector ) ) { + $allowed_attr[] = $css_selector; + $is_custom_var = true; + } + if ( in_array( $css_selector, $allowed_attr, true ) ) { $found = true; $url_attr = in_array( $css_selector, $css_url_data_types, true ); $gradient_attr = in_array( $css_selector, $css_gradient_data_types, true ); } + + if ( $is_custom_var ) { + $css_value = trim( $parts[1] ); + $url_attr = str_starts_with( $css_value, 'url(' ); + $gradient_attr = str_contains( $css_value, '-gradient(' ); + } } if ( $found && $url_attr ) { @@ -2467,13 +2649,20 @@ } if ( $found ) { - // Allow CSS calc(). - $css_test_string = preg_replace( '/calc\(((?:\([^()]*\)?|[^()])*)\)/', '', $css_test_string ); - // Allow CSS var(). - $css_test_string = preg_replace( '/\(?var\(--[a-zA-Z0-9_-]*\)/', '', $css_test_string ); + /* + * Allow CSS functions like var(), calc(), etc. by removing them from the test string. + * Nested functions and parentheses are also removed, so long as the parentheses are balanced. + */ + $css_test_string = preg_replace( + '/\b(?:var|calc|min|max|minmax|clamp|repeat)(\((?:[^()]|(?1))*\))/', + '', + $css_test_string + ); - // Check for any CSS containing \ ( & } = or comments, - // except for url(), calc(), or var() usage checked above. + /* + * Disallow CSS containing \ ( & } = or comments, except for within url(), var(), calc(), etc. + * which were removed from the test string above. + */ $allow_css = ! preg_match( '%[\\\(&=}]|/\*%', $css_test_string ); /** @@ -2510,6 +2699,8 @@ * @since 3.5.0 * @since 5.0.0 Added support for `data-*` wildcard attributes. * @since 6.0.0 Added `dir`, `lang`, and `xml:lang` to global attributes. + * @since 6.3.0 Added `aria-controls`, `aria-current`, and `aria-expanded` attributes. + * @since 6.4.0 Added `aria-live` and `hidden` attributes. * * @access private * @ignore @@ -2519,14 +2710,19 @@ */ function _wp_add_global_attributes( $value ) { $global_attributes = array( + 'aria-controls' => true, + 'aria-current' => true, 'aria-describedby' => true, 'aria-details' => true, + 'aria-expanded' => true, + 'aria-hidden' => true, 'aria-label' => true, 'aria-labelledby' => true, - 'aria-hidden' => true, + 'aria-live' => true, 'class' => true, 'data-*' => true, 'dir' => true, + 'hidden' => true, 'id' => true, 'lang' => true, 'style' => true,