Description: WordPress 5.0.9 patches
 Customize: Add additional filters to Customizer to prevent
 JSON corruption. User: Invalidate `user_activation_key` on password update.
 Query: Ensure that only a single post can be returned on date/time based
 queries. Cache API: Ensure proper escaping around the stats method in the
 cache API. Formatting: Expand `sanitize_file_name` to have better support for
 utf8 characters.
 .
 Brings the changes in [47633], [47634], [47635], [47636], [47637], and [47638] to the 5.0 branch.
 .
 Props: aduth, batmoo, ehti, ellatrix, jorgefilipecosta, nickdaugherty, noisysocks, pento, peterwilsoncc, sergeybiryukov, sstoqnov, talldanwp, westi, westonruter, whyisjake, whyisjake, xknown.
Author: whyisjake <whyisjake@git.wordpress.org>
Origin: upstream, https://github.com/WordPress/WordPress/commit/afc65069bb2000a4ae6c56523b02c4b861d6cf36
Applied-Upstream: 5.0.9
Reviewed-by: Craig Small <csmall@debian.org>
Last-Update: 2020-05-02
--- a/wp-includes/cache.php
+++ b/wp-includes/cache.php
@@ -668,7 +668,7 @@
 		echo "</p>";
 		echo '<ul>';
 		foreach ($this->cache as $group => $cache) {
-			echo "<li><strong>Group:</strong> $group - ( " . number_format( strlen( serialize( $cache ) ) / KB_IN_BYTES, 2 ) . 'k )</li>';
+			echo '<li><strong>Group:</strong> ' . esc_html( $group ) . ' - ( ' . number_format( strlen( serialize( $cache ) ) / KB_IN_BYTES, 2 ) . 'k )</li>';
 		}
 		echo '</ul>';
 	}
--- a/wp-includes/class-wp-customize-manager.php
+++ b/wp-includes/class-wp-customize-manager.php
@@ -2859,13 +2859,12 @@
 		$this->store_changeset_revision = $allow_revision;
 		add_filter( 'wp_save_post_revision_post_has_changed', array( $this, '_filter_revision_post_has_changed' ), 5, 3 );
 
-		// Update the changeset post. The publish_customize_changeset action will cause the settings in the changeset to be saved via WP_Customize_Setting::save().
-		$has_kses = ( false !== has_filter( 'content_save_pre', 'wp_filter_post_kses' ) );
-		if ( $has_kses ) {
-			kses_remove_filters(); // Prevent KSES from corrupting JSON in post_content.
-		}
-
-		// Note that updating a post with publish status will trigger WP_Customize_Manager::publish_changeset_values().
+		/*
+		 * Update the changeset post. The publish_customize_changeset action will cause the settings in the
+		 * changeset to be saved via WP_Customize_Setting::save(). Updating a post with publish status will
+		 * trigger WP_Customize_Manager::publish_changeset_values().
+		 */
+		add_filter( 'wp_insert_post_data', array( $this, 'preserve_insert_changeset_post_content' ), 5, 3 );
 		if ( $changeset_post_id ) {
 			if ( $args['autosave'] && 'auto-draft' !== get_post_status( $changeset_post_id ) ) {
 				// See _wp_translate_postdata() for why this is required as it will use the edit_post meta capability.
@@ -2892,9 +2891,9 @@
 				$this->_changeset_post_id = $r; // Update cached post ID for the loaded changeset.
 			}
 		}
-		if ( $has_kses ) {
-			kses_init_filters();
-		}
+
+		remove_filter( 'wp_insert_post_data', array( $this, 'preserve_insert_changeset_post_content' ), 5 );
+
 		$this->_changeset_data = null; // Reset so WP_Customize_Manager::changeset_data() will re-populate with updated contents.
 
 		remove_filter( 'wp_save_post_revision_post_has_changed', array( $this, '_filter_revision_post_has_changed' ) );
@@ -2912,6 +2911,51 @@
 	}
 
 	/**
+	 * Preserve the initial JSON post_content passed to save into the post.
+	 *
+	 * This is needed to prevent KSES and other {@see 'content_save_pre'} filters
+	 * from corrupting JSON data.
+	 *
+	 * Note that WP_Customize_Manager::validate_setting_values() have already
+	 * run on the setting values being serialized as JSON into the post content
+	 * so it is pre-sanitized.
+	 *
+	 * Also, the sanitization logic is re-run through the respective
+	 * WP_Customize_Setting::sanitize() method when being read out of the
+	 * changeset, via WP_Customize_Manager::post_value(), and this sanitized
+	 * value will also be sent into WP_Customize_Setting::update() for
+	 * persisting to the DB.
+	 *
+	 * Multiple users can collaborate on a single changeset, where one user may
+	 * have the unfiltered_html capability but another may not. A user with
+	 * unfiltered_html may add a script tag to some field which needs to be kept
+	 * intact even when another user updates the changeset to modify another field
+	 * when they do not have unfiltered_html.
+	 *
+	 * @since 5.4.1
+	 *
+	 * @param array $data                An array of slashed and processed post data.
+	 * @param array $postarr             An array of sanitized (and slashed) but otherwise unmodified post data.
+	 * @param array $unsanitized_postarr An array of slashed yet *unsanitized* and unprocessed post data as originally passed to wp_insert_post().
+	 * @return array Filtered post data.
+	 */
+	public function preserve_insert_changeset_post_content( $data, $postarr, $unsanitized_postarr ) {
+		if (
+			isset( $data['post_type'] ) &&
+			isset( $unsanitized_postarr['post_content'] ) &&
+			'customize_changeset' === $data['post_type'] ||
+			(
+				'revision' === $data['post_type'] &&
+				! empty( $data['post_parent'] ) &&
+				'customize_changeset' === get_post_type( $data['post_parent'] )
+			)
+		) {
+			$data['post_content'] = $unsanitized_postarr['post_content'];
+		}
+		return $data;
+	}
+
+	/**
 	 * Trash or delete a changeset post.
 	 *
 	 * The following re-formulates the logic from `wp_trash_post()` as done in
--- a/wp-includes/class-wp-query.php
+++ b/wp-includes/class-wp-query.php
@@ -759,10 +759,6 @@
 			$this->is_single = true;
 		} elseif ( $qv['p'] ) {
 			$this->is_single = true;
-		} elseif ( ('' !== $qv['hour']) && ('' !== $qv['minute']) &&('' !== $qv['second']) && ('' != $qv['year']) && ('' != $qv['monthnum']) && ('' != $qv['day']) ) {
-			// If year, month, day, hour, minute, and second are set, a single
-			// post is being queried.
-			$this->is_single = true;
 		} elseif ( '' != $qv['pagename'] || !empty($qv['page_id']) ) {
 			$this->is_page = true;
 			$this->is_single = false;
--- a/wp-includes/formatting.php
+++ b/wp-includes/formatting.php
@@ -1776,6 +1776,24 @@
 function sanitize_file_name( $filename ) {
 	$filename_raw = $filename;
 	$special_chars = array("?", "[", "]", "/", "\\", "=", "<", ">", ":", ";", ",", "'", "\"", "&", "$", "#", "*", "(", ")", "|", "~", "`", "!", "{", "}", "%", "+", chr(0));
+
+	// Check for support for utf8 in the installed PCRE library once and store the result in a static.
+	static $utf8_pcre = null;
+	if ( ! isset( $utf8_pcre ) ) {
+		// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
+		$utf8_pcre = @preg_match( '/^./u', 'a' );
+	}
+
+	if ( ! seems_utf8( $filename ) ) {
+		$_ext     = pathinfo( $filename, PATHINFO_EXTENSION );
+		$_name    = pathinfo( $filename, PATHINFO_FILENAME );
+		$filename = sanitize_title_with_dashes( $_name ) . '.' . $_ext;
+	}
+
+	if ( $utf8_pcre ) {
+		$filename = preg_replace( "#\x{00a0}#siu", ' ', $filename );
+	}
+
 	/**
 	 * Filters the list of characters to remove from a filename.
 	 *
@@ -1785,7 +1803,6 @@
 	 * @param string $filename_raw  Filename as it was passed into sanitize_file_name().
 	 */
 	$special_chars = apply_filters( 'sanitize_file_name_chars', $special_chars, $filename_raw );
-	$filename = preg_replace( "#\x{00a0}#siu", ' ', $filename );
 	$filename = str_replace( $special_chars, '', $filename );
 	$filename = str_replace( array( '%20', '+' ), '-', $filename );
 	$filename = preg_replace( '/[\r\n\t -]+/', '-', $filename );
--- a/wp-includes/post.php
+++ b/wp-includes/post.php
@@ -3203,6 +3203,9 @@
 function wp_insert_post( $postarr, $wp_error = false ) {
 	global $wpdb;
 
+	// Capture original pre-sanitized array for passing into filters.
+	$unsanitized_postarr = $postarr;
+
 	$user_id = get_current_user_id();
 
 	$defaults = array(
@@ -3506,21 +3509,27 @@
 		 * Filters attachment post data before it is updated in or added to the database.
 		 *
 		 * @since 3.9.0
+		 * @since 5.4.1 `$unsanitized_postarr` argument added.
 		 *
-		 * @param array $data    An array of sanitized attachment post data.
-		 * @param array $postarr An array of unsanitized attachment post data.
+		 * @param array $data                An array of slashed, sanitized, and processed attachment post data.
+		 * @param array $postarr             An array of slashed and sanitized attachment post data, but not processed.
+		 * @param array $unsanitized_postarr An array of slashed yet *unsanitized* and unprocessed attachment post data
+		 *                                   as originally passed to wp_insert_post().
 		 */
-		$data = apply_filters( 'wp_insert_attachment_data', $data, $postarr );
+		$data = apply_filters( 'wp_insert_attachment_data', $data, $postarr, $unsanitized_postarr );
 	} else {
 		/**
 		 * Filters slashed post data just before it is inserted into the database.
 		 *
 		 * @since 2.7.0
+		 * @since 5.4.1 `$unsanitized_postarr` argument added.
 		 *
-		 * @param array $data    An array of slashed post data.
-		 * @param array $postarr An array of sanitized, but otherwise unmodified post data.
+		 * @param array $data                An array of slashed, sanitized, and processed post data.
+		 * @param array $postarr             An array of sanitized (and slashed) but otherwise unmodified post data.
+		 * @param array $unsanitized_postarr An array of slashed yet *unsanitized* and unprocessed post data as
+		 *                                   originally passed to wp_insert_post().
 		 */
-		$data = apply_filters( 'wp_insert_post_data', $data, $postarr );
+		$data = apply_filters( 'wp_insert_post_data', $data, $postarr, $unsanitized_postarr );
 	}
 	$data = wp_unslash( $data );
 	$where = array( 'ID' => $post_ID );
--- a/wp-includes/user.php
+++ b/wp-includes/user.php
@@ -1700,7 +1700,7 @@
 	$data = apply_filters( 'wp_pre_insert_user_data', $data, $update, $update ? (int) $ID : null );
 
 	if ( $update ) {
-		if ( $user_email !== $old_user_data->user_email ) {
+		if ( $user_email !== $old_user_data->user_email || $user_pass !== $old_user_data->user_pass ) {
 			$data['user_activation_key'] = '';
 		}
 		$wpdb->update( $wpdb->users, $data, compact( 'ID' ) );
