diff --git a/src/wp-includes/formatting.php b/src/wp-includes/formatting.php index f7d248294c..80e70879a1 100644 --- a/src/wp-includes/formatting.php +++ b/src/wp-includes/formatting.php @@ -3148,9 +3148,25 @@ function wp_rel_ugc( $text ) { */ function wp_targeted_link_rel( $text ) { // Don't run (more expensive) regex if no links with targets. - if ( stripos( $text, 'target' ) !== false && stripos( $text, ']*target\s*=[^>]*)>|i', 'wp_targeted_link_rel_callback', $text ); + if ( stripos( $text, 'target' ) === false || stripos( $text, ']*target\s*=[^>]*)>|i', 'wp_targeted_link_rel_callback', $part ); + } + + $text = ''; + for ( $i = 0; $i < count( $html_parts ); $i++ ) { + $text .= $html_parts[ $i ]; + if ( isset( $extra_parts[ $i ] ) ) { + $text .= $extra_parts[ $i ]; } } @@ -3169,8 +3185,17 @@ function wp_targeted_link_rel( $text ) { * @return string HTML A Element with rel noreferrer noopener in addition to any existing values */ function wp_targeted_link_rel_callback( $matches ) { - $link_html = $matches[1]; - $rel_match = array(); + $link_html = $matches[1]; + $original_link_html = $link_html; + + // Consider the html escaped if there are no unescaped quotes + $is_escaped = ! preg_match( '/(^|[^\\\\])[\'"]/', $link_html ); + if ( $is_escaped ) { + // Replace only the quotes so that they are parsable by wp_kses_hair, leave the rest as is + $link_html = preg_replace( '/\\\\([\'"])/', '$1', $link_html ); + } + + $atts = wp_kses_hair( $link_html, wp_allowed_protocols() ); /** * Filters the rel values that are added to links with `target` attribute. @@ -3182,35 +3207,21 @@ function wp_targeted_link_rel_callback( $matches ) { */ $rel = apply_filters( 'wp_targeted_link_rel', 'noopener noreferrer', $link_html ); - // Avoid additional regex if the filter removes rel values. - if ( ! $rel ) { - return ""; + // Return early if no rel values to be added or if no actual target attribute + if ( ! $rel || ! isset( $atts['target'] ) ) { + return ""; } - // Value with delimiters, spaces around are optional. - $attr_regex = '|rel\s*=\s*?(\\\\{0,1}["\'])(.*?)\\1|i'; - preg_match( $attr_regex, $link_html, $rel_match ); - - if ( empty( $rel_match[0] ) ) { - // No delimiters, try with a single value and spaces, because `rel = va"lue` is totally fine... - $attr_regex = '|rel\s*=(\s*)([^\s]*)|i'; - preg_match( $attr_regex, $link_html, $rel_match ); + if ( isset( $atts['rel'] ) ) { + $all_parts = preg_split( '/\s/', "{$atts['rel']['value']} $rel", -1, PREG_SPLIT_NO_EMPTY ); + $rel = implode( ' ', array_unique( $all_parts ) ); } - if ( ! empty( $rel_match[0] ) ) { - $parts = preg_split( '|\s+|', strtolower( $rel_match[2] ) ); - $parts = array_map( 'esc_attr', $parts ); - $needed = explode( ' ', $rel ); - $parts = array_unique( array_merge( $parts, $needed ) ); - $delimiter = trim( $rel_match[1] ) ? $rel_match[1] : '"'; - $rel = 'rel=' . $delimiter . trim( implode( ' ', $parts ) ) . $delimiter; - $link_html = str_replace( $rel_match[0], $rel, $link_html ); - } elseif ( preg_match( '|target\s*=\s*?\\\\"|', $link_html ) ) { - $link_html .= " rel=\\\"$rel\\\""; - } elseif ( preg_match( '#(target|href)\s*=\s*?\'#', $link_html ) ) { - $link_html .= " rel='$rel'"; - } else { - $link_html .= " rel=\"$rel\""; + $atts['rel']['whole'] = 'rel="' . esc_attr( $rel ) . '"'; + $link_html = join( ' ', array_column( $atts, 'whole' ) ); + + if ( $is_escaped ) { + $link_html = preg_replace( '/[\'"]/', '\\\\$0', $link_html ); } return ""; diff --git a/tests/phpunit/tests/formatting/WPTargetedLinkRel.php b/tests/phpunit/tests/formatting/WPTargetedLinkRel.php index 8be254ac44..a7bcf91f2a 100644 --- a/tests/phpunit/tests/formatting/WPTargetedLinkRel.php +++ b/tests/phpunit/tests/formatting/WPTargetedLinkRel.php @@ -38,7 +38,7 @@ class Tests_Targeted_Link_Rel extends WP_UnitTestCase { public function test_rel_with_single_quote_delimiter() { $content = '

Links: Existing rel

'; - $expected = '

Links: Existing rel

'; + $expected = '

Links: Existing rel

'; $this->assertEquals( $expected, wp_targeted_link_rel( $content ) ); } @@ -54,12 +54,6 @@ class Tests_Targeted_Link_Rel extends WP_UnitTestCase { $this->assertEquals( $expected, wp_targeted_link_rel( $content ) ); } - public function test_rel_value_spaced_and_no_delimiter_and_values_to_escape() { - $content = '

Links: Existing rel

'; - $expected = '

Links: Existing rel

'; - $this->assertEquals( $expected, wp_targeted_link_rel( $content ) ); - } - public function test_escaped_quotes() { $content = '

Links: Existing rel

'; $expected = '

Links: Existing rel

'; @@ -114,17 +108,13 @@ class Tests_Targeted_Link_Rel extends WP_UnitTestCase { } /** - * Ensure correct quotes are used when relation attribute (rel) is missing. + * Ensure the content of style and script tags are not processed * * @ticket 47244 */ - public function test_wp_targeted_link_rel_should_use_correct_quotes() { - $content = '

Links: No rel<\/a><\/p>'; - $expected = '

Links: No rel<\/a><\/p>'; - $this->assertEquals( $expected, wp_targeted_link_rel( $content ) ); - - $content = '

Links: No rel<\/a><\/p>'; - $expected = '

Links: No rel<\/a><\/p>'; + public function test_wp_targeted_link_rel_skips_style_and_scripts() { + $content = '

Links: here aq

'; + $expected = '

Links: here aq

'; $this->assertEquals( $expected, wp_targeted_link_rel( $content ) ); } @@ -139,4 +129,10 @@ class Tests_Targeted_Link_Rel extends WP_UnitTestCase { $this->assertEquals( $expected, wp_targeted_link_rel( $content ) ); } + public function test_wp_targeted_link_rel_tab_separated_values_are_split() { + $content = "

Links: No rel

"; + $expected = '

Links: No rel

'; + $this->assertEquals( $expected, wp_targeted_link_rel( $content ) ); + } + }