wp/wp-includes/kses.php
changeset 19 3d72ae0968f4
parent 18 be944660c56a
child 21 48c4eec2b7e6
--- a/wp/wp-includes/kses.php	Wed Sep 21 18:19:35 2022 +0200
+++ b/wp/wp-includes/kses.php	Tue Sep 27 16:37:53 2022 +0200
@@ -81,16 +81,10 @@
 			'target' => true,
 		),
 		'article'    => array(
-			'align'    => true,
-			'dir'      => true,
-			'lang'     => true,
-			'xml:lang' => true,
+			'align' => true,
 		),
 		'aside'      => array(
-			'align'    => true,
-			'dir'      => true,
-			'lang'     => true,
-			'xml:lang' => true,
+			'align' => true,
 		),
 		'audio'      => array(
 			'autoplay' => true,
@@ -101,14 +95,10 @@
 			'src'      => true,
 		),
 		'b'          => array(),
-		'bdo'        => array(
-			'dir' => true,
-		),
+		'bdo'        => array(),
 		'big'        => array(),
 		'blockquote' => array(
-			'cite'     => true,
-			'lang'     => true,
-			'xml:lang' => true,
+			'cite' => true,
 		),
 		'br'         => array(),
 		'button'     => array(
@@ -120,17 +110,13 @@
 		'caption'    => array(
 			'align' => true,
 		),
-		'cite'       => array(
-			'dir'  => true,
-			'lang' => true,
-		),
+		'cite'       => array(),
 		'code'       => array(),
 		'col'        => array(
 			'align'   => true,
 			'char'    => true,
 			'charoff' => true,
 			'span'    => true,
-			'dir'     => true,
 			'valign'  => true,
 			'width'   => true,
 		),
@@ -148,33 +134,21 @@
 		'dd'         => array(),
 		'dfn'        => array(),
 		'details'    => array(
-			'align'    => true,
-			'dir'      => true,
-			'lang'     => true,
-			'open'     => true,
-			'xml:lang' => true,
+			'align' => true,
+			'open'  => true,
 		),
 		'div'        => array(
-			'align'    => true,
-			'dir'      => true,
-			'lang'     => true,
-			'xml:lang' => true,
+			'align' => true,
 		),
 		'dl'         => array(),
 		'dt'         => array(),
 		'em'         => array(),
 		'fieldset'   => array(),
 		'figure'     => array(
-			'align'    => true,
-			'dir'      => true,
-			'lang'     => true,
-			'xml:lang' => true,
+			'align' => true,
 		),
 		'figcaption' => array(
-			'align'    => true,
-			'dir'      => true,
-			'lang'     => true,
-			'xml:lang' => true,
+			'align' => true,
 		),
 		'font'       => array(
 			'color' => true,
@@ -182,10 +156,7 @@
 			'size'  => true,
 		),
 		'footer'     => array(
-			'align'    => true,
-			'dir'      => true,
-			'lang'     => true,
-			'xml:lang' => true,
+			'align' => true,
 		),
 		'h1'         => array(
 			'align' => true,
@@ -206,16 +177,10 @@
 			'align' => true,
 		),
 		'header'     => array(
-			'align'    => true,
-			'dir'      => true,
-			'lang'     => true,
-			'xml:lang' => true,
+			'align' => true,
 		),
 		'hgroup'     => array(
-			'align'    => true,
-			'dir'      => true,
-			'lang'     => true,
-			'xml:lang' => true,
+			'align' => true,
 		),
 		'hr'         => array(
 			'align'   => true,
@@ -253,10 +218,7 @@
 			'value' => true,
 		),
 		'main'       => array(
-			'align'    => true,
-			'dir'      => true,
-			'lang'     => true,
-			'xml:lang' => true,
+			'align' => true,
 		),
 		'map'        => array(
 			'name' => true,
@@ -266,16 +228,20 @@
 			'type' => true,
 		),
 		'nav'        => array(
-			'align'    => true,
-			'dir'      => true,
-			'lang'     => true,
-			'xml:lang' => true,
+			'align' => true,
+		),
+		'object'     => array(
+			'data' => array(
+				'required'       => true,
+				'value_callback' => '_wp_kses_allow_pdf_objects',
+			),
+			'type' => array(
+				'required' => true,
+				'values'   => array( 'application/pdf' ),
+			),
 		),
 		'p'          => array(
-			'align'    => true,
-			'dir'      => true,
-			'lang'     => true,
-			'xml:lang' => true,
+			'align' => true,
 		),
 		'pre'        => array(
 			'width' => true,
@@ -283,29 +249,25 @@
 		'q'          => array(
 			'cite' => true,
 		),
+		'rb'         => array(),
+		'rp'         => array(),
+		'rt'         => array(),
+		'rtc'        => array(),
+		'ruby'       => array(),
 		's'          => array(),
 		'samp'       => array(),
 		'span'       => array(
-			'dir'      => true,
-			'align'    => true,
-			'lang'     => true,
-			'xml:lang' => true,
+			'align' => true,
 		),
 		'section'    => array(
-			'align'    => true,
-			'dir'      => true,
-			'lang'     => true,
-			'xml:lang' => true,
+			'align' => true,
 		),
 		'small'      => array(),
 		'strike'     => array(),
 		'strong'     => array(),
 		'sub'        => array(),
 		'summary'    => array(
-			'align'    => true,
-			'dir'      => true,
-			'lang'     => true,
-			'xml:lang' => true,
+			'align' => true,
 		),
 		'sup'        => array(),
 		'table'      => array(
@@ -314,7 +276,6 @@
 			'border'      => true,
 			'cellpadding' => true,
 			'cellspacing' => true,
-			'dir'         => true,
 			'rules'       => true,
 			'summary'     => true,
 			'width'       => true,
@@ -333,7 +294,6 @@
 			'char'    => true,
 			'charoff' => true,
 			'colspan' => true,
-			'dir'     => true,
 			'headers' => true,
 			'height'  => true,
 			'nowrap'  => true,
@@ -452,7 +412,7 @@
 	);
 
 	/**
-	 * @var string[] $allowedentitynames Array of KSES allowed HTML entitity names.
+	 * @var string[] $allowedentitynames Array of KSES allowed HTML entity names.
 	 * @since 1.0.0
 	 */
 	$allowedentitynames = array(
@@ -712,10 +672,10 @@
 	);
 
 	/**
-	 * @var string[] $allowedxmlentitynames Array of KSES allowed XML entitity names.
+	 * @var string[] $allowedxmlentitynames Array of KSES allowed XML entity names.
 	 * @since 5.5.0
 	 */
-	$allowedxmlnamedentities = array(
+	$allowedxmlentitynames = array(
 		'amp',
 		'lt',
 		'gt',
@@ -846,22 +806,30 @@
  *
  * @param string|array $context The context for which to retrieve tags. Allowed values are 'post',
  *                              'strip', 'data', 'entities', or the name of a field filter such as
- *                              'pre_user_description'.
+ *                              'pre_user_description', or an array of allowed HTML elements and attributes.
  * @return array Array of allowed HTML tags and their allowed attributes.
  */
 function wp_kses_allowed_html( $context = '' ) {
 	global $allowedposttags, $allowedtags, $allowedentitynames;
 
 	if ( is_array( $context ) ) {
+		// When `$context` is an array it's actually an array of allowed HTML elements and attributes.
+		$html    = $context;
+		$context = 'explicit';
+
 		/**
-		 * Filters the HTML that is allowed for a given context.
+		 * Filters the HTML tags that are allowed for a given context.
+		 *
+		 * HTML tags and attribute names are case-insensitive in HTML but must be
+		 * added to the KSES allow list in lowercase. An item added to the allow list
+		 * in upper or mixed case will not recognized as permitted by KSES.
 		 *
 		 * @since 3.5.0
 		 *
-		 * @param array[]|string $context      Context to judge allowed tags by.
-		 * @param string         $context_type Context name.
+		 * @param array[] $html    Allowed HTML tags.
+		 * @param string  $context Context name.
 		 */
-		return apply_filters( 'wp_kses_allowed_html', $context, 'explicit' );
+		return apply_filters( 'wp_kses_allowed_html', $html, $context );
 	}
 
 	switch ( $context ) {
@@ -1040,7 +1008,7 @@
  *                                                or a context name such as 'post'.
  * @global string[]       $pass_allowed_protocols Array of allowed URL protocols.
  *
- * @param array $matches preg_replace regexp matches
+ * @param array $match preg_replace regexp matches
  * @return string
  */
 function _wp_kses_split_callback( $match ) {
@@ -1126,12 +1094,22 @@
  * Removes all attributes, if none are allowed for this element.
  *
  * If some are allowed it calls `wp_kses_hair()` to split them further, and then
- * it builds up new HTML code from the data that `kses_hair()` returns. It also
+ * it builds up new HTML code from the data that `wp_kses_hair()` returns. It also
  * removes `<` and `>` characters, if there are any left. One more thing it does
  * is to check if the tag has a closing XHTML slash, and if it does, it puts one
  * in the returned code as well.
  *
+ * An array of allowed values can be defined for attributes. If the attribute value
+ * doesn't fall into the list, the attribute will be removed from the tag.
+ *
+ * Attributes can be marked as required. If a required attribute is not present,
+ * KSES will remove all attributes from the tag. As KSES doesn't match opening and
+ * closing tags, it's not possible to safely remove the tag itself, the safest
+ * fallback is to strip all attributes from the tag, instead.
+ *
  * @since 1.0.0
+ * @since 5.9.0 Added support for an array of allowed values for attributes.
+ *              Added support for required attributes.
  *
  * @param string         $element           HTML element/tag.
  * @param string         $attr              HTML attributes from HTML element to closing HTML element tag.
@@ -1161,15 +1139,48 @@
 	// Split it.
 	$attrarr = wp_kses_hair( $attr, $allowed_protocols );
 
-	// Go through $attrarr, and save the allowed attributes for this element
-	// in $attr2.
+	// Check if there are attributes that are required.
+	$required_attrs = array_filter(
+		$allowed_html[ $element_low ],
+		function( $required_attr_limits ) {
+			return isset( $required_attr_limits['required'] ) && true === $required_attr_limits['required'];
+		}
+	);
+
+	/*
+	 * If a required attribute check fails, we can return nothing for a self-closing tag,
+	 * but for a non-self-closing tag the best option is to return the element with attributes,
+	 * as KSES doesn't handle matching the relevant closing tag.
+	 */
+	$stripped_tag = '';
+	if ( empty( $xhtml_slash ) ) {
+		$stripped_tag = "<$element>";
+	}
+
+	// Go through $attrarr, and save the allowed attributes for this element in $attr2.
 	$attr2 = '';
 	foreach ( $attrarr as $arreach ) {
+		// Check if this attribute is required.
+		$required = isset( $required_attrs[ strtolower( $arreach['name'] ) ] );
+
 		if ( wp_kses_attr_check( $arreach['name'], $arreach['value'], $arreach['whole'], $arreach['vless'], $element, $allowed_html ) ) {
 			$attr2 .= ' ' . $arreach['whole'];
+
+			// If this was a required attribute, we can mark it as found.
+			if ( $required ) {
+				unset( $required_attrs[ strtolower( $arreach['name'] ) ] );
+			}
+		} elseif ( $required ) {
+			// This attribute was required, but didn't pass the check. The entire tag is not allowed.
+			return $stripped_tag;
 		}
 	}
 
+	// If some required attributes weren't set, the entire tag is not allowed.
+	if ( ! empty( $required_attrs ) ) {
+		return $stripped_tag;
+	}
+
 	// Remove any "<" or ">" characters.
 	$attr2 = preg_replace( '/[<>]/', '', $attr2 );
 
@@ -1180,7 +1191,7 @@
  * Determines whether an attribute is allowed.
  *
  * @since 4.2.3
- * @since 5.0.0 Add support for `data-*` wildcard attributes.
+ * @since 5.0.0 Added support for `data-*` wildcard attributes.
  *
  * @param string $name         The attribute name. Passed by reference. Returns empty string when not allowed.
  * @param string $value        The attribute value. Passed by reference. Returns a filtered value.
@@ -1214,7 +1225,9 @@
 		 * Note: the attribute name should only contain `A-Za-z0-9_-` chars,
 		 * double hyphens `--` are not accepted by WordPress.
 		 */
-		if ( strpos( $name_low, 'data-' ) === 0 && ! empty( $allowed_attr['data-*'] ) && preg_match( '/^data(?:-[a-z0-9_]+)+$/', $name_low, $match ) ) {
+		if ( strpos( $name_low, 'data-' ) === 0 && ! 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
 			 * for the `data-*` attribute values for the current element.
@@ -1596,6 +1609,28 @@
 				$ok = false;
 			}
 			break;
+
+		case 'values':
+			/*
+			 * The values check is used when you want to make sure that the attribute
+			 * has one of the given values.
+			 */
+
+			if ( false === array_search( strtolower( $value ), $checkvalue, true ) ) {
+				$ok = false;
+			}
+			break;
+
+		case 'value_callback':
+			/*
+			 * The value_callback check is used when you want to make sure that the attribute
+			 * value is accepted by the callback function.
+			 */
+
+			if ( ! call_user_func( $checkvalue, $value ) ) {
+				$ok = false;
+			}
+			break;
 	} // End switch.
 
 	return $ok;
@@ -1846,13 +1881,13 @@
  * @since 5.5.0
  *
  * @global array $allowedentitynames
- * @global array $allowedxmlnamedentities
+ * @global array $allowedxmlentitynames
  *
  * @param array $matches preg_replace_callback() matches array.
  * @return string Correctly encoded entity.
  */
 function wp_kses_xml_named_entities( $matches ) {
-	global $allowedentitynames, $allowedxmlnamedentities;
+	global $allowedentitynames, $allowedxmlentitynames;
 
 	if ( empty( $matches[1] ) ) {
 		return '';
@@ -1860,7 +1895,7 @@
 
 	$i = $matches[1];
 
-	if ( in_array( $i, $allowedxmlnamedentities, true ) ) {
+	if ( in_array( $i, $allowedxmlentitynames, true ) ) {
 		return "&$i;";
 	} elseif ( in_array( $i, $allowedentitynames, true ) ) {
 		return html_entity_decode( "&$i;", ENT_HTML5 );
@@ -2028,6 +2063,33 @@
 }
 
 /**
+ * Sanitizes global styles user content removing unsafe rules.
+ *
+ * @since 5.9.0
+ *
+ * @param string $data Post content to filter.
+ * @return string Filtered post content with unsafe rules removed.
+ */
+function wp_filter_global_styles_post( $data ) {
+	$decoded_data        = json_decode( wp_unslash( $data ), true );
+	$json_decoding_error = json_last_error();
+	if (
+		JSON_ERROR_NONE === $json_decoding_error &&
+		is_array( $decoded_data ) &&
+		isset( $decoded_data['isGlobalStylesUserThemeJSON'] ) &&
+		$decoded_data['isGlobalStylesUserThemeJSON']
+	) {
+		unset( $decoded_data['isGlobalStylesUserThemeJSON'] );
+
+		$data_to_encode = WP_Theme_JSON::remove_insecure_properties( $decoded_data );
+
+		$data_to_encode['isGlobalStylesUserThemeJSON'] = true;
+		return wp_slash( wp_json_encode( $data_to_encode ) );
+	}
+	return $data;
+}
+
+/**
  * Sanitizes content for allowed HTML tags for post content.
  *
  * Post content refers to the page contents of the 'post' type and not `$_POST`
@@ -2095,6 +2157,10 @@
 		add_filter( 'pre_comment_content', 'wp_filter_kses' );
 	}
 
+	// Global Styles filtering: Global Styles filters should be executed before normal post_kses HTML filters.
+	add_filter( 'content_save_pre', 'wp_filter_global_styles_post', 9 );
+	add_filter( 'content_filtered_save_pre', 'wp_filter_global_styles_post', 9 );
+
 	// Post filtering.
 	add_filter( 'content_save_pre', 'wp_filter_post_kses' );
 	add_filter( 'excerpt_save_pre', 'wp_filter_post_kses' );
@@ -2121,6 +2187,10 @@
 	remove_filter( 'pre_comment_content', 'wp_filter_post_kses' );
 	remove_filter( 'pre_comment_content', 'wp_filter_kses' );
 
+	// Global Styles filtering.
+	remove_filter( 'content_save_pre', 'wp_filter_global_styles_post', 9 );
+	remove_filter( 'content_filtered_save_pre', 'wp_filter_global_styles_post', 9 );
+
 	// Post filtering.
 	remove_filter( 'content_save_pre', 'wp_filter_post_kses' );
 	remove_filter( 'excerpt_save_pre', 'wp_filter_post_kses' );
@@ -2148,6 +2218,16 @@
  * Filters an inline style attribute and removes disallowed rules.
  *
  * @since 2.8.1
+ * @since 4.4.0 Added support for `min-height`, `max-height`, `min-width`, and `max-width`.
+ * @since 4.6.0 Added support for `list-style-type`.
+ * @since 5.0.0 Added support for `background-image`.
+ * @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.
+ * @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.
  *
  * @param string $css        A string of CSS rules.
  * @param string $deprecated Not used.
@@ -2166,19 +2246,9 @@
 	$css_array = explode( ';', trim( $css ) );
 
 	/**
-	 * Filters list of allowed CSS attributes.
+	 * Filters the list of allowed CSS attributes.
 	 *
 	 * @since 2.8.1
-	 * @since 4.4.0 Added support for `min-height`, `max-height`, `min-width`, and `max-width`.
-	 * @since 4.6.0 Added support for `list-style-type`.
-	 * @since 5.0.0 Added support for `background-image`.
-	 * @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.
-	 * @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.
 	 *
 	 * @param string[] $attr Array of allowed CSS attributes.
 	 */
@@ -2204,16 +2274,24 @@
 			'border-right-width',
 			'border-bottom',
 			'border-bottom-color',
+			'border-bottom-left-radius',
+			'border-bottom-right-radius',
 			'border-bottom-style',
 			'border-bottom-width',
+			'border-bottom-right-radius',
+			'border-bottom-left-radius',
 			'border-left',
 			'border-left-color',
 			'border-left-style',
 			'border-left-width',
 			'border-top',
 			'border-top-color',
+			'border-top-left-radius',
+			'border-top-right-radius',
 			'border-top-style',
 			'border-top-width',
+			'border-top-left-radius',
+			'border-top-right-radius',
 
 			'border-spacing',
 			'border-collapse',
@@ -2228,6 +2306,7 @@
 			'column-width',
 
 			'color',
+			'filter',
 			'font',
 			'font-family',
 			'font-size',
@@ -2411,7 +2490,7 @@
 			 */
 			$allow_css = apply_filters( 'safecss_filter_attr_allow_css', $allow_css, $css_test_string );
 
-			 // Only add the CSS part if it passes the regex check.
+			// Only add the CSS part if it passes the regex check.
 			if ( $allow_css ) {
 				if ( '' !== $css ) {
 					$css .= ';';
@@ -2429,7 +2508,9 @@
  * Helper function to add global attributes to a tag in the allowed HTML list.
  *
  * @since 3.5.0
- * @since 5.0.0 Add support for `data-*` wildcard attributes.
+ * @since 5.0.0 Added support for `data-*` wildcard attributes.
+ * @since 6.0.0 Added `dir`, `lang`, and `xml:lang` to global attributes.
+ *
  * @access private
  * @ignore
  *
@@ -2444,11 +2525,14 @@
 		'aria-labelledby'  => true,
 		'aria-hidden'      => true,
 		'class'            => true,
+		'data-*'           => true,
+		'dir'              => true,
 		'id'               => true,
+		'lang'             => true,
 		'style'            => true,
 		'title'            => true,
 		'role'             => true,
-		'data-*'           => true,
+		'xml:lang'         => true,
 	);
 
 	if ( true === $value ) {
@@ -2461,3 +2545,39 @@
 
 	return $value;
 }
+
+/**
+ * Helper function to check if this is a safe PDF URL.
+ *
+ * @since 5.9.0
+ * @access private
+ * @ignore
+ *
+ * @param string $url The URL to check.
+ * @return bool True if the URL is safe, false otherwise.
+ */
+function _wp_kses_allow_pdf_objects( $url ) {
+	// We're not interested in URLs that contain query strings or fragments.
+	if ( str_contains( $url, '?' ) || str_contains( $url, '#' ) ) {
+		return false;
+	}
+
+	// If it doesn't have a PDF extension, it's not safe.
+	if ( ! str_ends_with( $url, '.pdf' ) ) {
+		return false;
+	}
+
+	// If the URL host matches the current site's media URL, it's safe.
+	$upload_info = wp_upload_dir( null, false );
+	$parsed_url  = wp_parse_url( $upload_info['url'] );
+	$upload_host = isset( $parsed_url['host'] ) ? $parsed_url['host'] : '';
+	$upload_port = isset( $parsed_url['port'] ) ? ':' . $parsed_url['port'] : '';
+
+	if ( str_starts_with( $url, "http://$upload_host$upload_port/" )
+		|| str_starts_with( $url, "https://$upload_host$upload_port/" )
+	) {
+		return true;
+	}
+
+	return false;
+}