diff --git a/wp-includes/formatting.php b/wp-includes/formatting.php
index 1c94f2906e..4ac2a685b7 100644
--- a/wp-includes/formatting.php
+++ b/wp-includes/formatting.php
@@ -3146,9 +3146,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, '/si';
+
+ preg_match_all( $script_and_style_regex, $text, $matches );
+ $extra_parts = $matches[0];
+ $html_parts = preg_split( $script_and_style_regex, $text );
+
+ foreach ( $html_parts as &$part ) {
+ $part = preg_replace_callback( '|]*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 ];
}
}
@@ -3167,8 +3183,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.
@@ -3180,35 +3205,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/wp-includes/version.php b/wp-includes/version.php
index 94505f9e8a..099c8c024f 100644
--- a/wp-includes/version.php
+++ b/wp-includes/version.php
@@ -13,7 +13,7 @@
*
* @global string $wp_version
*/
-$wp_version = '5.3.1-RC2-46897';
+$wp_version = '5.3.1-RC2-46898';
/**
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.