diff --git a/src/wp-includes/formatting.php b/src/wp-includes/formatting.php index 234d71a2a175a..e2d02d1b8f966 100644 --- a/src/wp-includes/formatting.php +++ b/src/wp-includes/formatting.php @@ -3220,34 +3220,81 @@ function _split_str_by_whitespace( $text, $goal ) { * @return string HTML A element with the added rel attribute. */ function wp_rel_callback( $matches, $rel ) { - $text = $matches[1]; - $atts = wp_kses_hair( $matches[1], wp_allowed_protocols() ); + _deprecated_function( + __FUNCTION__, + '{WP_VERSION}', + 'wp_include_in_all_a_rel()' + ); + return wp_include_in_all_a_rel( $matches[0], $rel ); +} - if ( ! empty( $atts['href'] ) && wp_is_internal_link( $atts['href']['value'] ) ) { - $rel = trim( str_replace( 'nofollow', '', $rel ) ); +/** + * Ensures that all A elements in the given HTML contain + * the provided and unique “rel” keywords. + * + * Example: + * + * `` === wp_include_in_all_a_rel( '', 'nofollow' ); + * `` === wp_include_in_all_a_rel( '', 'nofollow' ); + * `` === wp_include_in_all_a_rel( '', 'nofollow' ); + * `` === wp_include_in_all_a_rel( '`, 'a a a b b c' ); + * + * @since {WP_VERSION} + * + * @param string $html Add the given `rel` keywords to every `A` tag in this HTML. + * @param string $space_separated_rel_keywords Each of these keywords will be present in the final HTML. + * @return string Modified HTML with all `A` tags containing the given `rel` keywords. + */ +function wp_include_in_all_a_rel( $html, $space_separated_rel_keywords ) { + if ( empty( $html ) || empty( $space_separated_rel_keywords ) ) { + return $html; } - if ( ! empty( $atts['rel'] ) ) { - $parts = array_map( 'trim', explode( ' ', $atts['rel']['value'] ) ); - $rel_array = array_map( 'trim', explode( ' ', $rel ) ); - $parts = array_unique( array_merge( $parts, $rel_array ) ); - $rel = implode( ' ', $parts ); - unset( $atts['rel'] ); + /* + * It’s not necessary to add the `nofollow` guard to internal links; + * these are used to only check and remove `nofollow` when adding it. + */ + $without_nofollow = $space_separated_rel_keywords; + $adding_no_follow = false; - $html = ''; - foreach ( $atts as $name => $value ) { - if ( isset( $value['vless'] ) && 'y' === $value['vless'] ) { - $html .= $name . ' '; + /* + * Although this could falsely match on longer tokens like `nofollowers`, + * it’s safe to check generously since the parsing will ensure that only + * `nofollow` is removed; only a bit of unnecessary processing will occur. + */ + if ( str_contains( $without_nofollow, 'nofollow' ) ) { + $tokens = WP_HTML_Attribute::from_unordered_set_of_space_separated_tokens( $without_nofollow ); + $without_nofollow = ''; + + foreach ( $tokens as $token ) { + if ( 'nofollow' === $token ) { + $adding_no_follow = true; } else { - $html .= "{$name}=\"" . esc_attr( $value['value'] ) . '" '; + $without_nofollow .= " {$token}"; } } - $text = trim( $html ); } - $rel_attr = $rel ? ' rel="' . esc_attr( $rel ) . '"' : ''; + // Update the `rel` attributes in every `A` element. + $processor = new WP_HTML_Tag_Processor( $html ); + while ( $processor->next_tag( 'A' ) ) { + $rel = $processor->get_attribute( 'rel' ); + $rel = is_string( $rel ) ? $rel : ''; - return ""; + $href = $adding_no_follow ? $processor->get_attribute( 'href' ) : null; + $skip_nofollow = is_string( $href ) && wp_is_internal_link( $href ); + + $combined = $skip_nofollow + ? "{$rel} {$without_nofollow}" + : "{$rel} {$space_separated_rel_keywords}"; + + $tokens = WP_HTML_Attribute::from_unordered_set_of_space_separated_tokens( $combined ); + $new_rel = empty( $tokens ) ? false : implode( ' ', $tokens ); + + $processor->set_attribute( 'rel', $new_rel ); + } + + return $processor->get_updated_html(); } /** @@ -3261,13 +3308,7 @@ function wp_rel_callback( $matches, $rel ) { function wp_rel_nofollow( $text ) { // This is a pre-save filter, so text is already escaped. $text = stripslashes( $text ); - $text = preg_replace_callback( - '||i', - static function ( $matches ) { - return wp_rel_callback( $matches, 'nofollow' ); - }, - $text - ); + $text = wp_include_in_all_a_rel( $text, 'nofollow' ); return wp_slash( $text ); } @@ -3281,6 +3322,11 @@ static function ( $matches ) { * @return string HTML A Element with `rel="nofollow"`. */ function wp_rel_nofollow_callback( $matches ) { + _deprecated_function( + __FUNCTION__, + '{WP_VERSION}', + 'wp_include_in_all_a_rel()' + ); return wp_rel_callback( $matches, 'nofollow' ); } @@ -3295,13 +3341,7 @@ function wp_rel_nofollow_callback( $matches ) { function wp_rel_ugc( $text ) { // This is a pre-save filter, so text is already escaped. $text = stripslashes( $text ); - $text = preg_replace_callback( - '||i', - static function ( $matches ) { - return wp_rel_callback( $matches, 'nofollow ugc' ); - }, - $text - ); + $text = wp_include_in_all_a_rel( $text, 'nofollow ugc' ); return wp_slash( $text ); } diff --git a/src/wp-includes/html-api/class-wp-html-attribute.php b/src/wp-includes/html-api/class-wp-html-attribute.php new file mode 100644 index 0000000000000..fad2aadd09782 --- /dev/null +++ b/src/wp-includes/html-api/class-wp-html-attribute.php @@ -0,0 +1,64 @@ + A set of space-separated tokens is a string containing zero or more + * > words (known as tokens) separated by one or more ASCII whitespace, + * > where words consist of any string of one or more characters, none + * > of which are ASCII whitespace. + * + * > An unordered set of unique space-separated tokens is a set of + * > space-separated tokens where none of the tokens are duplicated. + * + * > How tokens in a set of space-separated tokens are to be compared + * > (e.g. case-sensitively or not) is defined on a per-set basis. + * + * @see https://html.spec.whatwg.org/#unordered-set-of-unique-space-separated-tokens + * + * @since {WP_VERSION} + * + * @param string $attribute_value HTML-decoded attribute value to parse. + * @param string $case_sensitivity Optional. Constrain uniqueness with 'case-sensitive' + * or 'case-insensitive'. Default 'case-sensitive'. + * @return string[] Set of unique tokens parsed from attribute value. + */ + public static function from_unordered_set_of_space_separated_tokens( $attribute_value, $case_sensitivity = 'case-sensitive' ) { + if ( empty( $attribute_value ) ) { + return array(); + } + + if ( 'case-insensitive' === $case_sensitivity ) { + $attribute_value = strtolower( $attribute_value ); + } + + $tokens = array(); + $uniques = ' '; + $at = 0; + $end = strlen( $attribute_value ); + while ( $at < $end ) { + $at += strspn( $attribute_value, " \t\f\r\n", $at ); + + $word_length = strcspn( $attribute_value, " \t\f\r\n", $at ); + $word = substr( $attribute_value, $at, $word_length ); + + if ( 0 < $word_length && ! str_contains( $uniques, " {$word} " ) ) { + $uniques .= "{$word} "; + $tokens[] = $word; + } + + $at += $word_length; + } + + return $tokens; + } +} diff --git a/src/wp-includes/kses.php b/src/wp-includes/kses.php index 28bbce222a214..53bb51ec7acf6 100644 --- a/src/wp-includes/kses.php +++ b/src/wp-includes/kses.php @@ -1385,149 +1385,50 @@ function wp_kses_attr_check( &$name, &$value, &$whole, $vless, $element, $allowe * attribute defined first (`foo='bar' foo='baz'` will result in `foo='bar'`). * * @since 1.0.0 + * @since 6.9.0 Rebuilt on 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 ) { - $attrarr = array(); - $mode = 0; - $attrname = ''; - $uris = wp_kses_uri_attributes(); + $attributes = array(); + $uris = wp_kses_uri_attributes(); // Loop through the whole attribute list. - while ( strlen( $attr ) !== 0 ) { - $working = 0; // Was the last operation successful? + $processor = new WP_HTML_Tag_Processor( "" ); + $processor->next_token(); - switch ( $mode ) { - case 0: - if ( preg_match( '/^([_a-zA-Z][-_a-zA-Z0-9:.]*)/', $attr, $match ) ) { - $attrname = $match[1]; - $working = 1; - $mode = 1; - $attr = preg_replace( '/^[_a-zA-Z][-_a-zA-Z0-9:.]*/', '', $attr ); - } - - break; - - case 1: - if ( preg_match( '/^\s*=\s*/', $attr ) ) { // Equals sign. - $working = 1; - $mode = 2; - $attr = preg_replace( '/^\s*=\s*/', '', $attr ); - break; - } - - if ( preg_match( '/^\s+/', $attr ) ) { // Valueless. - $working = 1; - $mode = 0; - - if ( false === array_key_exists( $attrname, $attrarr ) ) { - $attrarr[ $attrname ] = array( - 'name' => $attrname, - 'value' => '', - 'whole' => $attrname, - 'vless' => 'y', - ); - } - - $attr = preg_replace( '/^\s+/', '', $attr ); - } - - break; - - case 2: - if ( preg_match( '%^"([^"]*)"(\s+|/?$)%', $attr, $match ) ) { - // "value" - $thisval = $match[1]; - 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( - 'name' => $attrname, - 'value' => $thisval, - 'whole' => "$attrname=\"$thisval\"", - 'vless' => 'n', - ); - } - - $working = 1; - $mode = 0; - $attr = preg_replace( '/^"[^"]*"(\s+|$)/', '', $attr ); - break; - } - - if ( preg_match( "%^'([^']*)'(\s+|/?$)%", $attr, $match ) ) { - // 'value' - $thisval = $match[1]; - 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( - 'name' => $attrname, - 'value' => $thisval, - 'whole' => "$attrname='$thisval'", - 'vless' => 'n', - ); - } - - $working = 1; - $mode = 0; - $attr = preg_replace( "/^'[^']*'(\s+|$)/", '', $attr ); - break; - } - - if ( preg_match( "%^([^\s\"']+)(\s+|/?$)%", $attr, $match ) ) { - // value - $thisval = $match[1]; - 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( - 'name' => $attrname, - 'value' => $thisval, - 'whole' => "$attrname=\"$thisval\"", - 'vless' => 'n', - ); - } - - // We add quotes to conform to W3C's HTML spec. - $working = 1; - $mode = 0; - $attr = preg_replace( "%^[^\s\"']+(\s+|$)%", '', $attr ); - } + foreach ( $processor->get_attribute_names_with_prefix( '' ) 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 ); + } - break; - } // End switch. + // Reconstruct and normalize the attribute value. + $syntax_characters = array( + '&' => '&', + '<' => '<', + '>' => '>', + "'" => ''', + '"' => '"', + ); - if ( 0 === $working ) { // Not well-formed, remove and try again. - $attr = wp_kses_html_error( $attr ); - $mode = 0; - } - } // End while. + $recoded = $is_bool ? '' : strtr( $value, $syntax_characters ); + $whole = $is_bool ? $name : "{$name}=\"{$recoded}\""; - 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' => '', - 'whole' => $attrname, - 'vless' => 'y', + // @todo What security issue need review on the names? + $attributes[ $name ] = array( + 'name' => $name, + 'value' => $recoded, + 'whole' => $whole, + 'vless' => $is_bool ? 'y' : 'n', ); } - return $attrarr; + return $attributes; } /** diff --git a/src/wp-settings.php b/src/wp-settings.php index 60ffc307c5f6e..1f9f7aad0d4ed 100644 --- a/src/wp-settings.php +++ b/src/wp-settings.php @@ -266,6 +266,7 @@ require ABSPATH . WPINC . '/html-api/class-wp-html-stack-event.php'; require ABSPATH . WPINC . '/html-api/class-wp-html-processor-state.php'; require ABSPATH . WPINC . '/html-api/class-wp-html-processor.php'; +require ABSPATH . WPINC . '/html-api/class-wp-html-attribute.php'; require ABSPATH . WPINC . '/class-wp-http.php'; require ABSPATH . WPINC . '/class-wp-http-streams.php'; require ABSPATH . WPINC . '/class-wp-http-curl.php'; diff --git a/tests/phpunit/tests/comment.php b/tests/phpunit/tests/comment.php index e1d3fd0613e4b..a8f15e1deaec3 100644 --- a/tests/phpunit/tests/comment.php +++ b/tests/phpunit/tests/comment.php @@ -168,7 +168,12 @@ public function test_update_comment_from_unprivileged_user_by_privileged_user() wp_set_current_user( 0 ); $comment = get_comment( $comment_id ); - $this->assertEqualHTML( 'click', $comment->comment_content, '', 'Comment: ' . $comment->comment_content ); + $this->assertEqualHTML( + 'click', + $comment->comment_content, + '', + 'Comment: ' . $comment->comment_content + ); } /** diff --git a/tests/phpunit/tests/formatting/wpRelNofollow.php b/tests/phpunit/tests/formatting/wpRelNofollow.php index 41cf835313444..83a9c00c23e95 100644 --- a/tests/phpunit/tests/formatting/wpRelNofollow.php +++ b/tests/phpunit/tests/formatting/wpRelNofollow.php @@ -12,8 +12,8 @@ class Tests_Formatting_wpRelNofollow extends WP_UnitTestCase { */ public function test_add_no_follow() { $content = '

This is some cool Code

'; - $expected = '

This is some cool Code

'; - $this->assertSame( $expected, wp_rel_nofollow( $content ) ); + $expected = '

This is some cool Code

'; + $this->assertEqualHTML( $expected, stripslashes( wp_rel_nofollow( $content ) ) ); } /** @@ -21,8 +21,8 @@ public function test_add_no_follow() { */ public function test_convert_no_follow() { $content = '

This is some cool Code

'; - $expected = '

This is some cool Code

'; - $this->assertSame( $expected, wp_rel_nofollow( $content ) ); + $expected = '

This is some cool Code

'; + $this->assertEqualHTML( $expected, stripslashes( wp_rel_nofollow( $content ) ) ); } /** @@ -30,7 +30,7 @@ public function test_convert_no_follow() { * @dataProvider data_wp_rel_nofollow */ public function test_wp_rel_nofollow( $input, $output, $expect_deprecation = false ) { - $this->assertSame( wp_slash( $output ), wp_rel_nofollow( $input ) ); + $this->assertEqualHTML( $output, stripslashes( wp_rel_nofollow( $input ) ) ); } public function data_wp_rel_nofollow() { @@ -80,7 +80,7 @@ public function data_wp_rel_nofollow() { public function test_append_no_follow_with_valueless_attribute() { $content = '

This is some cool Code

'; - $expected = '

This is some cool Code

'; - $this->assertSame( $expected, wp_rel_nofollow( $content ) ); + $expected = '

This is some cool Code

'; + $this->assertEqualHTML( $expected, stripslashes( wp_rel_nofollow( $content ) ) ); } } diff --git a/tests/phpunit/tests/kses.php b/tests/phpunit/tests/kses.php index 61baf0d0a1863..e3bd074511d2c 100644 --- a/tests/phpunit/tests/kses.php +++ b/tests/phpunit/tests/kses.php @@ -17,7 +17,7 @@ class Tests_Kses extends WP_UnitTestCase { public function test_wp_filter_post_kses_address( $content, $expected ) { global $allowedposttags; - $this->assertSame( $expected, wp_kses( $content, $allowedposttags ) ); + $this->assertEqualHTML( $expected, wp_kses( $content, $allowedposttags ) ); } /** @@ -65,7 +65,7 @@ public function data_wp_filter_post_kses_address() { public function test_wp_filter_post_kses_a( $content, $expected ) { global $allowedposttags; - $this->assertSame( $expected, wp_kses( $content, $allowedposttags ) ); + $this->assertEqualHTML( $expected, wp_kses( $content, $allowedposttags ) ); } /** @@ -120,7 +120,7 @@ public function data_wp_filter_post_kses_a() { * @param string $expected Expected output following KSES parsing. */ public function test_wp_kses_video( $source, $context, $expected ) { - $this->assertSame( $expected, wp_kses( $source, $context ) ); + $this->assertEqualHTML( $expected, wp_kses( $source, $context ) ); } /** @@ -171,7 +171,7 @@ public function data_wp_kses_video() { public function test_wp_filter_post_kses_abbr( $content, $expected ) { global $allowedposttags; - $this->assertSame( $expected, wp_kses( $content, $allowedposttags ) ); + $this->assertEqualHTML( $expected, wp_kses( $content, $allowedposttags ) ); } /** @@ -232,7 +232,7 @@ public function test_feed_links() { CLICK ME EOF; - $this->assertSame( $expected, wp_kses( $content, $allowedposttags ) ); + $this->assertEqualHTML( $expected, wp_kses( $content, $allowedposttags ) ); } public function test_wp_kses_bad_protocol() { @@ -546,8 +546,8 @@ public function test_hyphenated_tag() { $expect_stripped_content = 'Alot of hyphens.'; $expect_valid_content = 'Alot of hyphens.'; - $this->assertSame( $expect_stripped_content, wp_kses_post( $content ) ); - $this->assertSame( $expect_valid_content, wp_kses( $content, $custom_tags ) ); + $this->assertEqualHTML( $expect_stripped_content, wp_kses_post( $content ) ); + $this->assertEqualHTML( $expect_valid_content, wp_kses( $content, $custom_tags ) ); } /** @@ -613,7 +613,7 @@ public static function data_normalize_entities(): array { * @dataProvider data_normalize_entities */ public function test_wp_kses_normalize_entities( string $input, string $expected ) { - $this->assertSame( $expected, wp_kses_normalize_entities( $input ) ); + $this->assertEqualHTML( $expected, wp_kses_normalize_entities( $input ) ); } /** @@ -625,7 +625,7 @@ public function test_wp_kses_normalize_entities( string $input, string $expected public function test_ctrl_removal( $content, $expected ) { global $allowedposttags; - return $this->assertSame( $expected, wp_kses( $content, $allowedposttags ) ); + return $this->assertEqualHTML( $expected, wp_kses( $content, $allowedposttags ) ); } public function data_ctrl_removal() { @@ -662,7 +662,7 @@ public function data_ctrl_removal() { public function test_slash_zero_removal( $content, $expected ) { global $allowedposttags; - return $this->assertSame( $expected, wp_kses( $content, $allowedposttags ) ); + return $this->assertEqualHTML( $expected, wp_kses( $content, $allowedposttags ) ); } public function data_slash_zero_removal() { @@ -917,7 +917,7 @@ public function test_bdo_tag_allowed() { $content = '

This is a BDO tag. Weird, right?

'; - $this->assertSame( $content, wp_kses( $content, $allowedposttags ) ); + $this->assertEqualHTML( $content, wp_kses( $content, $allowedposttags ) ); } /** @@ -928,7 +928,7 @@ public function test_ruby_tag_allowed() { $content = ': Star, Étoile.'; - $this->assertSame( $content, wp_kses( $content, $allowedposttags ) ); + $this->assertEqualHTML( $content, wp_kses( $content, $allowedposttags ) ); } /** @@ -939,7 +939,7 @@ public function test_ol_reversed_attribute_allowed() { $content = '
  1. Item 1
  2. Item 2
  3. Item 3
'; - $this->assertSame( $content, wp_kses( $content, $allowedposttags ) ); + $this->assertEqualHTML( $content, wp_kses( $content, $allowedposttags ) ); } /** @@ -949,7 +949,7 @@ public function test_wp_kses_attr_no_attributes_allowed_with_empty_array() { $element = 'foo'; $attribute = 'title="foo" class="bar"'; - $this->assertSame( "<{$element}>", wp_kses_attr( $element, $attribute, array( 'foo' => array() ), array() ) ); + $this->assertEqualHTML( "<{$element}>", wp_kses_attr( $element, $attribute, array( 'foo' => array() ), array() ) ); } /** @@ -959,7 +959,7 @@ public function test_wp_kses_attr_no_attributes_allowed_with_true() { $element = 'foo'; $attribute = 'title="foo" class="bar"'; - $this->assertSame( "<{$element}>", wp_kses_attr( $element, $attribute, array( 'foo' => true ), array() ) ); + $this->assertEqualHTML( "<{$element}>", wp_kses_attr( $element, $attribute, array( 'foo' => true ), array() ) ); } /** @@ -969,7 +969,7 @@ public function test_wp_kses_attr_single_attribute_is_allowed() { $element = 'foo'; $attribute = 'title="foo" class="bar"'; - $this->assertSame( "<{$element} title=\"foo\">", wp_kses_attr( $element, $attribute, array( 'foo' => array( 'title' => true ) ), array() ) ); + $this->assertEqualHTML( "<{$element} title=\"foo\">", wp_kses_attr( $element, $attribute, array( 'foo' => array( 'title' => true ) ), array() ) ); } /** @@ -979,7 +979,7 @@ public function test_wp_kses_attr_no_attributes_allowed_with_false() { $element = 'foo'; $attribute = 'title="foo" class="bar"'; - $this->assertSame( "<{$element}>", wp_kses_attr( $element, $attribute, array( 'foo' => false ), array() ) ); + $this->assertEqualHTML( "<{$element}>", wp_kses_attr( $element, $attribute, array( 'foo' => false ), array() ) ); } /** @@ -1423,7 +1423,7 @@ public function test_wp_kses_attr_data_attribute_is_allowed() { $test = '
Pens and pencils
'; $expected = '
Pens and pencils
'; - $this->assertSame( $expected, wp_kses_post( $test ) ); + $this->assertEqualHTML( $expected, wp_kses_post( $test ) ); } /** @@ -1435,7 +1435,7 @@ public function test_wp_kses_attr_data_attribute_hypens_allowed() { $test = '
Pens and pencils
'; $expected = '
Pens and pencils
'; - $this->assertSame( $expected, wp_kses_post( $test ) ); + $this->assertEqualHTML( $expected, wp_kses_post( $test ) ); } /** @@ -1456,7 +1456,7 @@ public function test_wildcard_requires_hyphen_after_prefix() { $actual = wp_kses( $content, $allowed_html ); - $this->assertSame( $expected, $actual ); + $this->assertEqualHTML( $expected, $actual ); } /** @@ -1476,7 +1476,7 @@ public function test_wildcard_allows_two_hyphens() { $actual = wp_kses( $content, $allowed_html ); - $this->assertSame( $expected, $actual ); + $this->assertEqualHTML( $expected, $actual ); } /** @@ -1761,7 +1761,7 @@ public function test_wp_kses_img_tag_standard_attributes() { $html = implode( ' ', $html ); - $this->assertSame( $html, wp_kses_post( $html ) ); + $this->assertEqualHTML( $html, wp_kses_post( $html ) ); } /** @@ -1779,7 +1779,7 @@ public function test_wp_kses_main_tag_standard_attributes() { $html = implode( ' ', $test ); - $this->assertSame( $html, wp_kses_post( $html ) ); + $this->assertEqualHTML( $html, wp_kses_post( $html ) ); } /** @@ -1793,7 +1793,7 @@ public function test_wp_kses_main_tag_standard_attributes() { * @param string $expected The expected result from KSES. */ public function test_wp_kses_object_tag_allowed( $html, $expected ) { - $this->assertSame( $expected, wp_kses_post( $html ) ); + $this->assertEqualHTML( $expected, wp_kses_post( $html ) ); } /** @@ -1904,7 +1904,7 @@ public function data_wp_kses_object_tag_allowed() { */ public function test_wp_kses_object_data_url_with_port_number_allowed( $html, $expected ) { add_filter( 'upload_dir', array( $this, 'wp_kses_upload_dir_filter' ), 10, 2 ); - $this->assertSame( $expected, wp_kses_post( $html ) ); + $this->assertEqualHTML( $expected, wp_kses_post( $html ) ); } /** @@ -1970,7 +1970,7 @@ public function test_wp_kses_object_added_in_html_filter() { remove_filter( 'wp_kses_allowed_html', array( $this, 'filter_wp_kses_object_added_in_html_filter' ) ); - $this->assertSame( $html, $filtered_html ); + $this->assertEqualHTML( $html, $filtered_html ); } public function filter_wp_kses_object_added_in_html_filter( $tags, $context ) { @@ -2001,9 +2001,10 @@ public function filter_wp_kses_object_added_in_html_filter( $tags, $context ) { * @param string $expected_output How `wp_kses()` ought to transform the comment. */ public function test_wp_kses_preserves_html_comments( $html_comment, $expected_output ) { - $this->assertSame( + $this->assertEqualHTML( $expected_output, wp_kses( $html_comment, array() ), + '', 'Failed to properly preserve HTML comment.' ); } @@ -2033,7 +2034,7 @@ public static function data_html_containing_various_kinds_of_html_comments() { * @param array $allowed_html The allowed HTML to pass to KSES. */ public function test_wp_kses_allowed_values_list( $content, $expected, $allowed_html ) { - $this->assertSame( $expected, wp_kses( $content, $allowed_html ) ); + $this->assertEqualHTML( $expected, wp_kses( $content, $allowed_html ) ); } /** @@ -2091,7 +2092,7 @@ static function ( $datum ) { * @param array $allowed_html The allowed HTML to pass to KSES. */ public function test_wp_kses_required_attribute( $content, $expected, $allowed_html ) { - $this->assertSame( $expected, wp_kses( $content, $allowed_html ) ); + $this->assertEqualHTML( $expected, wp_kses( $content, $allowed_html ) ); } /** @@ -2312,7 +2313,7 @@ public function data_kses_globals_are_defined() { public function test_target_attribute_preserved_in_context( $context, $input, $expected ) { $allowed = wp_kses_allowed_html( $context ); $this->assertTrue( isset( $allowed['a']['target'] ), "Target attribute not allowed in {$context}" ); - $this->assertEquals( $expected, wp_kses( $input, $context ) ); + $this->assertEqualHTML( $expected, wp_kses( $input, $context ) ); } /** diff --git a/tests/phpunit/tests/media.php b/tests/phpunit/tests/media.php index 80774902a0dc7..fe3f20b345e0c 100644 --- a/tests/phpunit/tests/media.php +++ b/tests/phpunit/tests/media.php @@ -208,14 +208,34 @@ public function test_img_caption_shortcode_with_old_format_and_class() { } public function test_new_img_caption_shortcode_with_html_caption() { + $mark = "\u{203B}"; + + $this->assertStringNotContainsString( + self::HTML_CONTENT, + $mark, + 'Test caption content should not contain the mark surround it: check test setup.' + ); + $result = img_caption_shortcode( array( 'width' => 20, - 'caption' => self::HTML_CONTENT, + 'caption' => $mark . self::HTML_CONTENT . $mark, ) ); - $this->assertSame( 1, substr_count( $result, self::HTML_CONTENT ) ); + $result_chunks = explode( $mark, $result ); + $this->assertSame( + 3, + count( $result_chunks ), + 'Expected to find embedded caption inside marks, but failed to do so.' + ); + + $this->assertEqualHTML( + self::HTML_CONTENT, + $result_chunks[1], + '', + 'Should have embedded the caption inside the image output.' + ); } public function test_new_img_caption_shortcode_new_format() { diff --git a/tests/phpunit/tests/oembed/filterResult.php b/tests/phpunit/tests/oembed/filterResult.php index d2c1c8614115a..10dbe0e4ea017 100644 --- a/tests/phpunit/tests/oembed/filterResult.php +++ b/tests/phpunit/tests/oembed/filterResult.php @@ -9,26 +9,57 @@ public function test_filter_oembed_result_trusted_malicious_iframe() { $actual = wp_filter_oembed_result( $html, (object) array( 'type' => 'rich' ), 'https://www.youtube.com/watch?v=72xdCU__XCk' ); - $this->assertSame( $html, $actual ); + $this->assertEqualHTML( $html, $actual ); } public function test_filter_oembed_result_with_untrusted_provider() { $html = '

'; $actual = wp_filter_oembed_result( $html, (object) array( 'type' => 'rich' ), 'http://example.com/sample-page/' ); - $matches = array(); - preg_match( '|src=".*#\?secret=([\w\d]+)" data-secret="([\w\d]+)"|', $actual, $matches ); + $processor = new WP_HTML_Tag_Processor( $actual ); - $this->assertArrayHasKey( 1, $matches ); - $this->assertArrayHasKey( 2, $matches ); - $this->assertSame( $matches[1], $matches[2] ); + $this->assertTrue( + $processor->next_tag( 'IFRAME' ), + 'Failed to find expected IFRAME element in filtered output.' + ); + + $src = $processor->get_attribute( 'src' ); + $this->assertIsString( + $src, + isset( $src ) + ? 'Expected "src" attribute on IFRAME with string value but found boolean attribute instead.' + : 'Failed to find expected "src" attribute on IFRAME element.' + ); + + $query_string = parse_url( $src, PHP_URL_FRAGMENT ); + $this->assertStringStartsWith( + '?', + $query_string, + 'Should have found URL fragment in "src" attribute resembling a query string.' + ); + + $query_string = substr( $query_string, 1 ); + $query_args = array(); + parse_str( $query_string, $query_args ); + + $this->assertArrayHasKey( + 'secret', + $query_args, + 'Failed to find expected query arg "secret" in IFRAME "src" attribute.' + ); + + $this->assertSame( + $query_args['secret'], + $processor->get_attribute( 'data-secret' ), + 'Expected to find identical copy of secret from IFRAME "src" in the "data-secret" attribute.' + ); } public function test_filter_oembed_result_only_one_iframe_is_allowed() { $html = '

'; $actual = wp_filter_oembed_result( $html, (object) array( 'type' => 'rich' ), '' ); - $this->assertSame( '', $actual ); + $this->assertEqualHTML( '', $actual ); } public function test_filter_oembed_result_with_newlines() { @@ -41,7 +72,7 @@ public function test_filter_oembed_result_with_newlines() { $actual = wp_filter_oembed_result( $html, (object) array( 'type' => 'rich' ), '' ); - $this->assertSame( '', $actual ); + $this->assertEqualHTML( '', $actual ); } public function test_filter_oembed_result_without_iframe() { @@ -60,18 +91,48 @@ public function test_filter_oembed_result_secret_param_available() { $html = ''; $actual = wp_filter_oembed_result( $html, (object) array( 'type' => 'rich' ), '' ); - $matches = array(); - preg_match( '|src="https://wordpress.org#\?secret=([\w\d]+)" data-secret="([\w\d]+)"|', $actual, $matches ); + $processor = new WP_HTML_Tag_Processor( $actual ); - $this->assertArrayHasKey( 1, $matches ); - $this->assertArrayHasKey( 2, $matches ); - $this->assertSame( $matches[1], $matches[2] ); + $this->assertTrue( + $processor->next_tag( 'IFRAME' ), + 'Failed to find expected IFRAME element in filtered output.' + ); + + $src = $processor->get_attribute( 'src' ); + $this->assertMatchesRegularExpression( + '~^https://wordpress.org~', + $src, + 'Failed to find expected "src" attribute on IFRAME element.' + ); + + $query_string = parse_url( $src, PHP_URL_FRAGMENT ); + $this->assertStringStartsWith( + '?', + $query_string, + 'Should have found URL fragment in "src" attribute resembling a query string.' + ); + + $query_string = substr( $query_string, 1 ); + $query_args = array(); + parse_str( $query_string, $query_args ); + + $this->assertArrayHasKey( + 'secret', + $query_args, + 'Failed to find expected query arg "secret" in IFRAME "src" attribute.' + ); + + $this->assertSame( + $query_args['secret'], + $processor->get_attribute( 'data-secret' ), + 'Expected to find identical copy of secret from IFRAME "src" in the "data-secret" attribute.' + ); } public function test_filter_oembed_result_wrong_type_provided() { $actual = wp_filter_oembed_result( 'some string', (object) array( 'type' => 'link' ), '' ); - $this->assertSame( 'some string', $actual ); + $this->assertEqualHTML( 'some string', $actual ); } public function test_filter_oembed_result_invalid_result() { @@ -83,14 +144,14 @@ public function test_filter_oembed_result_blockquote_adds_style_to_iframe() { $html = '
'; $actual = wp_filter_oembed_result( $html, (object) array( 'type' => 'rich' ), '' ); - $this->assertSame( '
', $actual ); + $this->assertEqualHTML( '
', $actual ); } public function test_filter_oembed_result_allowed_html() { $html = '
'; $actual = wp_filter_oembed_result( $html, (object) array( 'type' => 'rich' ), '' ); - $this->assertSame( '
', $actual ); + $this->assertEqualHTML( '
', $actual ); } public function data_wp_filter_pre_oembed_custom_result() { @@ -124,7 +185,7 @@ public function test_wp_filter_pre_oembed_custom_result( $html, $expected ) { 'html' => $html, ); $actual = _wp_oembed_get_object()->data2html( $data, 'https://untrusted.localhost' ); - $this->assertSame( $expected, $actual ); + $this->assertEqualHTML( $expected, $actual ); } /** @@ -134,6 +195,6 @@ public function test_filter_feed_content() { $html = '
'; $actual = _oembed_filter_feed_content( wp_filter_oembed_result( $html, (object) array( 'type' => 'rich' ), '' ) ); - $this->assertSame( '
', $actual ); + $this->assertEqualHTML( '
', $actual ); } } diff --git a/tests/phpunit/tests/oembed/filterTitleAttributes.php b/tests/phpunit/tests/oembed/filterTitleAttributes.php index 7f35cac8ee48b..29d22f838af79 100644 --- a/tests/phpunit/tests/oembed/filterTitleAttributes.php +++ b/tests/phpunit/tests/oembed/filterTitleAttributes.php @@ -67,7 +67,7 @@ public function data_filter_oembed_iframe_title_attribute() { public function test_oembed_iframe_title_attribute( $html, $oembed_data, $url, $expected ) { $actual = wp_filter_oembed_iframe_title_attribute( $html, (object) $oembed_data, $url ); - $this->assertSame( $expected, $actual ); + $this->assertEqualHTML( $expected, $actual ); } public function test_filter_oembed_iframe_title_attribute() { @@ -84,7 +84,7 @@ public function test_filter_oembed_iframe_title_attribute() { remove_filter( 'oembed_iframe_title_attribute', array( $this, '_filter_oembed_iframe_title_attribute' ) ); - $this->assertSame( '', $actual ); + $this->assertEqualHTML( '', $actual ); } public function test_filter_oembed_iframe_title_attribute_does_not_modify_other_tags() { @@ -101,7 +101,7 @@ public function test_filter_oembed_iframe_title_attribute_does_not_modify_other_ remove_filter( 'oembed_iframe_title_attribute', array( $this, '_filter_oembed_iframe_title_attribute' ) ); - $this->assertSame( '

Baz

', $actual ); + $this->assertEqualHTML( '

Baz

', $actual ); } public function _filter_oembed_iframe_title_attribute() { diff --git a/tests/phpunit/tests/post/filtering.php b/tests/phpunit/tests/post/filtering.php index 5947a29d43cfc..a44ad873001fb 100644 --- a/tests/phpunit/tests/post/filtering.php +++ b/tests/phpunit/tests/post/filtering.php @@ -35,7 +35,7 @@ public function test_post_content_unknown_tag() { $id = self::factory()->post->create( array( 'post_content' => $content ) ); $post = get_post( $id ); - $this->assertSame( $expected, $post->post_content ); + $this->assertEqualHTML( $expected, $post->post_content ); } // A simple test to make sure unbalanced tags are fixed. @@ -52,7 +52,7 @@ public function test_post_content_unbalanced_tag() { $id = self::factory()->post->create( array( 'post_content' => $content ) ); $post = get_post( $id ); - $this->assertSame( $expected, $post->post_content ); + $this->assertEqualHTML( $expected, $post->post_content ); } // Test KSES filtering of disallowed attribute. @@ -69,7 +69,7 @@ public function test_post_content_disallowed_attr() { $id = self::factory()->post->create( array( 'post_content' => $content ) ); $post = get_post( $id ); - $this->assertSame( $expected, $post->post_content ); + $this->assertEqualHTML( $expected, $post->post_content ); } /** @@ -89,7 +89,7 @@ public function test_post_content_xhtml_empty_elem() { $id = self::factory()->post->create( array( 'post_content' => $content ) ); $post = get_post( $id ); - $this->assertSame( $expected, $post->post_content ); + $this->assertEqualHTML( $expected, $post->post_content ); } // Make sure unbalanced tags are untouched when the balance option is off. @@ -109,6 +109,6 @@ public function test_post_content_nobalance_nextpage_more() { $id = self::factory()->post->create( array( 'post_content' => $content ) ); $post = get_post( $id ); - $this->assertSame( $content, $post->post_content ); + $this->assertEqualHTML( $content, $post->post_content ); } }