diff --git a/wp-includes/blocks.php b/wp-includes/blocks.php
index 829d916bfd..1ee3536f69 100644
--- a/wp-includes/blocks.php
+++ b/wp-includes/blocks.php
@@ -74,11 +74,11 @@ function has_blocks( $post = null ) {
* @since 5.0.0
* @see parse_blocks()
*
- * @param string $block_type Full Block type to look for.
+ * @param string $block_name Full Block type to look for.
* @param int|string|WP_Post|null $post Optional. Post content, post ID, or post object. Defaults to global $post.
* @return bool Whether the post content contains the specified block.
*/
-function has_block( $block_type, $post = null ) {
+function has_block( $block_name, $post = null ) {
if ( ! has_blocks( $post ) ) {
return false;
}
@@ -90,7 +90,30 @@ function has_block( $block_type, $post = null ) {
}
}
- return false !== strpos( $post, '', $serialized_block_name, $serialized_attributes );
+ }
+
+ return sprintf(
+ '%s',
+ $serialized_block_name,
+ $serialized_attributes,
+ $block_content,
+ $serialized_block_name
+ );
+}
+
+/**
+ * Returns the content of a block, including comment delimiters, serializing all
+ * attributes from the given parsed block.
+ *
+ * This should be used when preparing a block to be saved to post content.
+ * Prefer `render_block` when preparing a block for display. Unlike
+ * `render_block`, this does not evaluate a block's `render_callback`, and will
+ * instead preserve the markup as parsed.
+ *
+ * @since 5.3.1
+ *
+ * @param WP_Block_Parser_Block $block A single parsed block object.
+ * @return string String of rendered HTML.
+ */
+function serialize_block( $block ) {
+ $block_content = '';
+
+ $index = 0;
+ foreach ( $block['innerContent'] as $chunk ) {
+ $block_content .= is_string( $chunk ) ? $chunk : serialize_block( $block['innerBlocks'][ $index++ ] );
+ }
+
+ if ( ! is_array( $block['attrs'] ) ) {
+ $block['attrs'] = array();
+ }
+
+ return get_comment_delimited_block_content(
+ $block['blockName'],
+ $block['attrs'],
+ $block_content
+ );
+}
+
+/**
+ * Returns a joined string of the aggregate serialization of the given parsed
+ * blocks.
+ *
+ * @since 5.3.1
+ *
+ * @param WP_Block_Parser_Block[] $blocks Parsed block objects.
+ * @return string String of rendered HTML.
+ */
+function serialize_blocks( $blocks ) {
+ return implode( '', array_map( 'serialize_block', $blocks ) );
+}
+
+/**
+ * Filters and sanitizes block content to remove non-allowable HTML from
+ * parsed block attribute values.
+ *
+ * @since 5.3.1
+ *
+ * @param string $text Text that may contain block content.
+ * @param array[]|string $allowed_html An array of allowed HTML elements
+ * and attributes, or a context name
+ * such as 'post'.
+ * @param string[] $allowed_protocols Array of allowed URL protocols.
+ * @return string The filtered and sanitized content result.
+ */
+function filter_block_content( $text, $allowed_html = 'post', $allowed_protocols = array() ) {
+ $result = '';
+
+ $blocks = parse_blocks( $text );
+ foreach ( $blocks as $block ) {
+ $block = filter_block_kses( $block, $allowed_html, $allowed_protocols );
+ $result .= serialize_block( $block );
+ }
+
+ return $result;
+}
+
+/**
+ * Filters and sanitizes a parsed block to remove non-allowable HTML from block
+ * attribute values.
+ *
+ * @since 5.3.1
+ *
+ * @param WP_Block_Parser_Block $block The parsed block object.
+ * @param array[]|string $allowed_html An array of allowed HTML
+ * elements and attributes, or a
+ * context name such as 'post'.
+ * @param string[] $allowed_protocols Allowed URL protocols.
+ * @return array The filtered and sanitized block object result.
+ */
+function filter_block_kses( $block, $allowed_html, $allowed_protocols = array() ) {
+ $block['attrs'] = filter_block_kses_value( $block['attrs'], $allowed_html, $allowed_protocols );
+
+ if ( is_array( $block['innerBlocks'] ) ) {
+ foreach ( $block['innerBlocks'] as $i => $inner_block ) {
+ $block['innerBlocks'][ $i ] = filter_block_kses( $inner_block, $allowed_html, $allowed_protocols );
+ }
+ }
+
+ return $block;
+}
+
+/**
+ * Filters and sanitizes a parsed block attribute value to remove non-allowable
+ * HTML.
+ *
+ * @since 5.3.1
+ *
+ * @param mixed $value The attribute value to filter.
+ * @param array[]|string $allowed_html An array of allowed HTML elements
+ * and attributes, or a context name
+ * such as 'post'.
+ * @param string[] $allowed_protocols Array of allowed URL protocols.
+ * @return array The filtered and sanitized result.
+ */
+function filter_block_kses_value( $value, $allowed_html, $allowed_protocols = array() ) {
+ if ( is_array( $value ) ) {
+ foreach ( $value as $key => $inner_value ) {
+ $filtered_key = filter_block_kses_value( $key, $allowed_html, $allowed_protocols );
+ $filtered_value = filter_block_kses_value( $inner_value, $allowed_html, $allowed_protocols );
+
+ if ( $filtered_key !== $key ) {
+ unset( $value[ $key ] );
+ }
+
+ $value[ $filtered_key ] = $filtered_value;
+ }
+ } elseif ( is_string( $value ) ) {
+ return wp_kses( $value, $allowed_html, $allowed_protocols );
+ }
+
+ return $value;
+}
+
/**
* Parses blocks out of a content string, and renders those appropriate for the excerpt.
*
diff --git a/wp-includes/default-filters.php b/wp-includes/default-filters.php
index 1319826212..6379a55d24 100644
--- a/wp-includes/default-filters.php
+++ b/wp-includes/default-filters.php
@@ -245,6 +245,7 @@ add_filter( 'option_siteurl', '_config_wp_siteurl' );
add_filter( 'tiny_mce_before_init', '_mce_set_direction' );
add_filter( 'teeny_mce_before_init', '_mce_set_direction' );
add_filter( 'pre_kses', 'wp_pre_kses_less_than' );
+add_filter( 'pre_kses', 'wp_pre_kses_block_attributes', 10, 3 );
add_filter( 'sanitize_title', 'sanitize_title_with_dashes', 10, 3 );
add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 4 );
add_filter( 'comment_flood_filter', 'wp_throttle_comment_flood', 10, 3 );
diff --git a/wp-includes/formatting.php b/wp-includes/formatting.php
index d11700779b..a7ecf5b310 100644
--- a/wp-includes/formatting.php
+++ b/wp-includes/formatting.php
@@ -3041,8 +3041,26 @@ function wp_rel_nofollow_callback( $matches ) {
*/
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 ];
+ }
}
return $text;
@@ -3058,8 +3076,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.
@@ -3071,31 +3098,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 );
- } 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 "";
@@ -4796,6 +4813,31 @@ function wp_pre_kses_less_than_callback( $matches ) {
return $matches[0];
}
+/**
+ * Remove non-allowable HTML from parsed block attribute values when filtering
+ * in the post context.
+ *
+ * @since 5.3.1
+ *
+ * @param string $string Content to be run through KSES.
+ * @param array[]|string $allowed_html An array of allowed HTML elements
+ * and attributes, or a context name
+ * such as 'post'.
+ * @param string[] $allowed_protocols Array of allowed URL protocols.
+ * @return string Filtered text to run through KSES.
+ */
+function wp_pre_kses_block_attributes( $string, $allowed_html, $allowed_protocols ) {
+ /*
+ * `filter_block_content` is expected to call `wp_kses`. Temporarily remove
+ * the filter to avoid recursion.
+ */
+ remove_filter( 'pre_kses', 'wp_pre_kses_block_attributes', 10 );
+ $string = filter_block_content( $string, $allowed_html, $allowed_protocols );
+ add_filter( 'pre_kses', 'wp_pre_kses_block_attributes', 10, 3 );
+
+ return $string;
+}
+
/**
* WordPress implementation of PHP sprintf() with filters.
*
diff --git a/wp-includes/kses.php b/wp-includes/kses.php
index 4bbc5aeed5..78d55ae6b1 100644
--- a/wp-includes/kses.php
+++ b/wp-includes/kses.php
@@ -1658,7 +1658,7 @@ function wp_kses_html_error( $string ) {
*/
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 );
+ $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 );
diff --git a/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php b/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php
index bc0a179f7f..5e756231a4 100644
--- a/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php
+++ b/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php
@@ -499,7 +499,7 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
return new WP_Error( 'rest_cannot_edit_others', __( 'Sorry, you are not allowed to create posts as this user.' ), array( 'status' => rest_authorization_required_code() ) );
}
- if ( ! empty( $request['sticky'] ) && ! current_user_can( $post_type->cap->edit_others_posts ) ) {
+ if ( ! empty( $request['sticky'] ) && ! current_user_can( $post_type->cap->edit_others_posts ) && ! current_user_can( $post_type->cap->publish_posts ) ) {
return new WP_Error( 'rest_cannot_assign_sticky', __( 'Sorry, you are not allowed to make posts sticky.' ), array( 'status' => rest_authorization_required_code() ) );
}
@@ -654,7 +654,7 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
return new WP_Error( 'rest_cannot_edit_others', __( 'Sorry, you are not allowed to update posts as this user.' ), array( 'status' => rest_authorization_required_code() ) );
}
- if ( ! empty( $request['sticky'] ) && ! current_user_can( $post_type->cap->edit_others_posts ) ) {
+ if ( ! empty( $request['sticky'] ) && ! current_user_can( $post_type->cap->edit_others_posts ) && ! current_user_can( $post_type->cap->publish_posts ) ) {
return new WP_Error( 'rest_cannot_assign_sticky', __( 'Sorry, you are not allowed to make posts sticky.' ), array( 'status' => rest_authorization_required_code() ) );
}
@@ -956,7 +956,7 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
* @return stdClass|WP_Error Post object or WP_Error.
*/
protected function prepare_item_for_database( $request ) {
- $prepared_post = new stdClass;
+ $prepared_post = new stdClass();
// Post ID.
if ( isset( $request['id'] ) ) {