wp/wp-includes/class-wp-customize-manager.php
changeset 16 a86126ab1dd4
parent 9 177826044cd9
child 18 be944660c56a
--- a/wp/wp-includes/class-wp-customize-manager.php	Tue Oct 22 16:11:46 2019 +0200
+++ b/wp/wp-includes/class-wp-customize-manager.php	Tue Dec 15 13:49:49 2020 +0100
@@ -232,7 +232,7 @@
 	 * Changeset data loaded from a customize_changeset post.
 	 *
 	 * @since 4.7.0
-	 * @var array
+	 * @var array|null
 	 */
 	private $_changeset_data;
 
@@ -270,7 +270,8 @@
 			$args['changeset_uuid'] = wp_generate_uuid4();
 		}
 
-		// The theme and messenger_channel should be supplied via $args, but they are also looked at in the $_REQUEST global here for back-compat.
+		// The theme and messenger_channel should be supplied via $args,
+		// but they are also looked at in the $_REQUEST global here for back-compat.
 		if ( ! isset( $args['theme'] ) ) {
 			if ( isset( $_REQUEST['customize_theme'] ) ) {
 				$args['theme'] = wp_unslash( $_REQUEST['customize_theme'] );
@@ -293,46 +294,44 @@
 			}
 		}
 
-		require_once( ABSPATH . WPINC . '/class-wp-customize-setting.php' );
-		require_once( ABSPATH . WPINC . '/class-wp-customize-panel.php' );
-		require_once( ABSPATH . WPINC . '/class-wp-customize-section.php' );
-		require_once( ABSPATH . WPINC . '/class-wp-customize-control.php' );
-
-		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-color-control.php' );
-		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-media-control.php' );
-		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-upload-control.php' );
-		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-image-control.php' );
-		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-background-image-control.php' );
-		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-background-position-control.php' );
-		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-cropped-image-control.php' );
-		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-site-icon-control.php' );
-		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-header-image-control.php' );
-		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-theme-control.php' );
-		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-code-editor-control.php' );
-		require_once( ABSPATH . WPINC . '/customize/class-wp-widget-area-customize-control.php' );
-		require_once( ABSPATH . WPINC . '/customize/class-wp-widget-form-customize-control.php' );
-		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-control.php' );
-		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-item-control.php' );
-		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-location-control.php' );
-		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-name-control.php' );
-		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-locations-control.php' );
-		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-auto-add-control.php' );
-		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-new-menu-control.php' ); // @todo Remove in a future release. See #42364.
-
-		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menus-panel.php' );
-
-		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-themes-panel.php' );
-		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-themes-section.php' );
-		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-sidebar-section.php' );
-		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-section.php' );
-		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-new-menu-section.php' ); // @todo Remove in a future release. See #42364.
-
-		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-custom-css-setting.php' );
-		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-filter-setting.php' );
-		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-header-image-setting.php' );
-		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-background-image-setting.php' );
-		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-item-setting.php' );
-		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-setting.php' );
+		require_once ABSPATH . WPINC . '/class-wp-customize-setting.php';
+		require_once ABSPATH . WPINC . '/class-wp-customize-panel.php';
+		require_once ABSPATH . WPINC . '/class-wp-customize-section.php';
+		require_once ABSPATH . WPINC . '/class-wp-customize-control.php';
+
+		require_once ABSPATH . WPINC . '/customize/class-wp-customize-color-control.php';
+		require_once ABSPATH . WPINC . '/customize/class-wp-customize-media-control.php';
+		require_once ABSPATH . WPINC . '/customize/class-wp-customize-upload-control.php';
+		require_once ABSPATH . WPINC . '/customize/class-wp-customize-image-control.php';
+		require_once ABSPATH . WPINC . '/customize/class-wp-customize-background-image-control.php';
+		require_once ABSPATH . WPINC . '/customize/class-wp-customize-background-position-control.php';
+		require_once ABSPATH . WPINC . '/customize/class-wp-customize-cropped-image-control.php';
+		require_once ABSPATH . WPINC . '/customize/class-wp-customize-site-icon-control.php';
+		require_once ABSPATH . WPINC . '/customize/class-wp-customize-header-image-control.php';
+		require_once ABSPATH . WPINC . '/customize/class-wp-customize-theme-control.php';
+		require_once ABSPATH . WPINC . '/customize/class-wp-customize-code-editor-control.php';
+		require_once ABSPATH . WPINC . '/customize/class-wp-widget-area-customize-control.php';
+		require_once ABSPATH . WPINC . '/customize/class-wp-widget-form-customize-control.php';
+		require_once ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-control.php';
+		require_once ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-item-control.php';
+		require_once ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-location-control.php';
+		require_once ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-name-control.php';
+		require_once ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-locations-control.php';
+		require_once ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-auto-add-control.php';
+
+		require_once ABSPATH . WPINC . '/customize/class-wp-customize-nav-menus-panel.php';
+
+		require_once ABSPATH . WPINC . '/customize/class-wp-customize-themes-panel.php';
+		require_once ABSPATH . WPINC . '/customize/class-wp-customize-themes-section.php';
+		require_once ABSPATH . WPINC . '/customize/class-wp-customize-sidebar-section.php';
+		require_once ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-section.php';
+
+		require_once ABSPATH . WPINC . '/customize/class-wp-customize-custom-css-setting.php';
+		require_once ABSPATH . WPINC . '/customize/class-wp-customize-filter-setting.php';
+		require_once ABSPATH . WPINC . '/customize/class-wp-customize-header-image-setting.php';
+		require_once ABSPATH . WPINC . '/customize/class-wp-customize-background-image-setting.php';
+		require_once ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-item-setting.php';
+		require_once ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-setting.php';
 
 		/**
 		 * Filters the core Customizer components to load.
@@ -351,16 +350,16 @@
 		 */
 		$components = apply_filters( 'customize_loaded_components', $this->components, $this );
 
-		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-selective-refresh.php' );
+		require_once ABSPATH . WPINC . '/customize/class-wp-customize-selective-refresh.php';
 		$this->selective_refresh = new WP_Customize_Selective_Refresh( $this );
 
 		if ( in_array( 'widgets', $components, true ) ) {
-			require_once( ABSPATH . WPINC . '/class-wp-customize-widgets.php' );
+			require_once ABSPATH . WPINC . '/class-wp-customize-widgets.php';
 			$this->widgets = new WP_Customize_Widgets( $this );
 		}
 
 		if ( in_array( 'nav_menus', $components, true ) ) {
-			require_once( ABSPATH . WPINC . '/class-wp-customize-nav-menus.php' );
+			require_once ABSPATH . WPINC . '/class-wp-customize-nav-menus.php';
 			$this->nav_menus = new WP_Customize_Nav_Menus( $this );
 		}
 
@@ -385,7 +384,7 @@
 		add_action( 'wp_ajax_customize_dismiss_autosave_or_lock', array( $this, 'handle_dismiss_autosave_or_lock_request' ) );
 
 		add_action( 'customize_register', array( $this, 'register_controls' ) );
-		add_action( 'customize_register', array( $this, 'register_dynamic_settings' ), 11 ); // allow code to create settings first
+		add_action( 'customize_register', array( $this, 'register_dynamic_settings' ), 11 ); // Allow code to create settings first.
 		add_action( 'customize_controls_init', array( $this, 'prepare_controls' ) );
 		add_action( 'customize_controls_enqueue_scripts', array( $this, 'enqueue_control_scripts' ) );
 
@@ -438,8 +437,8 @@
 	 *
 	 * @since 3.4.0
 	 *
-	 * @param mixed $ajax_message Ajax return
-	 * @param mixed $message UI message
+	 * @param string|WP_Error $ajax_message Ajax return.
+	 * @param string          $message      Optional. UI message.
 	 */
 	protected function wp_die( $ajax_message, $message = null ) {
 		if ( $this->doing_ajax() ) {
@@ -506,7 +505,7 @@
 	public function setup_theme() {
 		global $pagenow;
 
-		// Check permissions for customize.php access since this method is called before customize.php can run any code,
+		// Check permissions for customize.php access since this method is called before customize.php can run any code.
 		if ( 'customize.php' === $pagenow && ! current_user_can( 'customize' ) ) {
 			if ( ! is_user_logged_in() ) {
 				auth_redirect();
@@ -565,8 +564,8 @@
 			// Once the theme is loaded, we'll validate it.
 			add_action( 'after_setup_theme', array( $this, 'after_setup_theme' ) );
 		} else {
-			// If the requested theme is not the active theme and the user doesn't have the
-			// switch_themes cap, bail.
+			// If the requested theme is not the active theme and the user doesn't have
+			// the switch_themes cap, bail.
 			if ( ! current_user_can( 'switch_themes' ) ) {
 				$this->wp_die( -1, __( 'Sorry, you are not allowed to edit theme options on this site.' ) );
 			}
@@ -607,6 +606,7 @@
 	 * enabled, then a new UUID will be generated.
 	 *
 	 * @since 4.9.0
+	 *
 	 * @global string $pagenow
 	 */
 	public function establish_loaded_changeset() {
@@ -738,6 +738,7 @@
 	 * Gets whether settings are or will be previewed.
 	 *
 	 * @since 4.9.0
+	 *
 	 * @see WP_Customize_Setting::preview()
 	 *
 	 * @return bool
@@ -750,6 +751,7 @@
 	 * Gets whether data from a changeset's autosaved revision should be loaded if it exists.
 	 *
 	 * @since 4.9.0
+	 *
 	 * @see WP_Customize_Manager::changeset_data()
 	 *
 	 * @return bool Is using autosaved changeset revision.
@@ -762,6 +764,7 @@
 	 * Whether the changeset branching is allowed.
 	 *
 	 * @since 4.9.0
+	 *
 	 * @see WP_Customize_Manager::establish_loaded_changeset()
 	 *
 	 * @return bool Is changeset branching.
@@ -802,6 +805,7 @@
 	 * Get the changeset UUID.
 	 *
 	 * @since 4.7.0
+	 *
 	 * @see WP_Customize_Manager::establish_loaded_changeset()
 	 *
 	 * @return string UUID.
@@ -890,7 +894,7 @@
 	 * @return bool
 	 */
 	public function is_theme_active() {
-		return $this->get_stylesheet() == $this->original_stylesheet;
+		return $this->get_stylesheet() === $this->original_stylesheet;
 	}
 
 	/**
@@ -900,7 +904,8 @@
 	 */
 	public function wp_loaded() {
 
-		// Unconditionally register core types for panels, sections, and controls in case plugin unhooks all customize_register actions.
+		// Unconditionally register core types for panels, sections, and controls
+		// in case plugin unhooks all customize_register actions.
 		$this->register_panel_type( 'WP_Customize_Panel' );
 		$this->register_panel_type( 'WP_Customize_Themes_Panel' );
 		$this->register_section_type( 'WP_Customize_Section' );
@@ -1072,7 +1077,7 @@
 	}
 
 	/**
-	 * Get the changeset post id for the loaded changeset.
+	 * Get the changeset post ID for the loaded changeset.
 	 *
 	 * @since 4.7.0
 	 *
@@ -1116,8 +1121,9 @@
 			return new WP_Error( 'wrong_post_type' );
 		}
 		$changeset_data = json_decode( $changeset_post->post_content, true );
-		if ( function_exists( 'json_last_error' ) && json_last_error() ) {
-			return new WP_Error( 'json_parse_error', '', json_last_error() );
+		$last_error     = json_last_error();
+		if ( $last_error ) {
+			return new WP_Error( 'json_parse_error', '', $last_error );
 		}
 		if ( ! is_array( $changeset_data ) ) {
 			return new WP_Error( 'expected_array' );
@@ -1229,7 +1235,7 @@
 					$widget_numbers = array_keys( $settings );
 					if ( count( $widget_numbers ) > 0 ) {
 						$widget_numbers[]               = 1;
-						$max_widget_numbers[ $id_base ] = call_user_func_array( 'max', $widget_numbers );
+						$max_widget_numbers[ $id_base ] = max( ...$widget_numbers );
 					} else {
 						$max_widget_numbers[ $id_base ] = 1;
 					}
@@ -1375,13 +1381,6 @@
 						)
 					);
 
-					// In PHP < 5.6 filesize() returns 0 for the temp files unless we clear the file status cache.
-					// Technically, PHP < 5.6.0 || < 5.5.13 || < 5.4.29 but no need to be so targeted.
-					// See https://bugs.php.net/bug.php?id=65701
-					if ( version_compare( PHP_VERSION, '5.6', '<' ) ) {
-						clearstatcache();
-					}
-
 					$attachment_id = media_handle_sideload( $file_array, 0, null, $attachment_post_data );
 					if ( is_wp_error( $attachment_id ) ) {
 						continue;
@@ -1523,7 +1522,27 @@
 
 		// Options.
 		foreach ( $options as $name => $value ) {
-			if ( preg_match( '/^{{(?P<symbol>.+)}}$/', $value, $matches ) ) {
+
+			// Serialize the value to check for post symbols.
+			$value = maybe_serialize( $value );
+
+			if ( is_serialized( $value ) ) {
+				if ( preg_match( '/s:\d+:"{{(?P<symbol>.+)}}"/', $value, $matches ) ) {
+					if ( isset( $posts[ $matches['symbol'] ] ) ) {
+						$symbol_match = $posts[ $matches['symbol'] ]['ID'];
+					} elseif ( isset( $attachment_ids[ $matches['symbol'] ] ) ) {
+						$symbol_match = $attachment_ids[ $matches['symbol'] ];
+					}
+
+					// If we have any symbol matches, update the values.
+					if ( isset( $symbol_match ) ) {
+						// Replace found string matches with post IDs.
+						$value = str_replace( $matches[0], "i:{$symbol_match}", $value );
+					} else {
+						continue;
+					}
+				}
+			} elseif ( preg_match( '/^{{(?P<symbol>.+)}}$/', $value, $matches ) ) {
 				if ( isset( $posts[ $matches['symbol'] ] ) ) {
 					$value = $posts[ $matches['symbol'] ]['ID'];
 				} elseif ( isset( $attachment_ids[ $matches['symbol'] ] ) ) {
@@ -1533,6 +1552,9 @@
 				}
 			}
 
+			// Unserialize values after checking for post symbols, so they can be properly referenced.
+			$value = maybe_unserialize( $value );
+
 			if ( empty( $changeset_data[ $name ] ) || ! empty( $changeset_data[ $name ]['starter_content'] ) ) {
 				$this->set_post_value( $name, $value );
 				$this->pending_starter_content_settings_ids[] = $name;
@@ -1541,7 +1563,28 @@
 
 		// Theme mods.
 		foreach ( $theme_mods as $name => $value ) {
-			if ( preg_match( '/^{{(?P<symbol>.+)}}$/', $value, $matches ) ) {
+
+			// Serialize the value to check for post symbols.
+			$value = maybe_serialize( $value );
+
+			// Check if value was serialized.
+			if ( is_serialized( $value ) ) {
+				if ( preg_match( '/s:\d+:"{{(?P<symbol>.+)}}"/', $value, $matches ) ) {
+					if ( isset( $posts[ $matches['symbol'] ] ) ) {
+						$symbol_match = $posts[ $matches['symbol'] ]['ID'];
+					} elseif ( isset( $attachment_ids[ $matches['symbol'] ] ) ) {
+						$symbol_match = $attachment_ids[ $matches['symbol'] ];
+					}
+
+					// If we have any symbol matches, update the values.
+					if ( isset( $symbol_match ) ) {
+						// Replace found string matches with post IDs.
+						$value = str_replace( $matches[0], "i:{$symbol_match}", $value );
+					} else {
+						continue;
+					}
+				}
+			} elseif ( preg_match( '/^{{(?P<symbol>.+)}}$/', $value, $matches ) ) {
 				if ( isset( $posts[ $matches['symbol'] ] ) ) {
 					$value = $posts[ $matches['symbol'] ]['ID'];
 				} elseif ( isset( $attachment_ids[ $matches['symbol'] ] ) ) {
@@ -1551,6 +1594,9 @@
 				}
 			}
 
+			// Unserialize values after checking for post symbols, so they can be properly referenced.
+			$value = maybe_unserialize( $value );
+
 			// Handle header image as special case since setting has a legacy format.
 			if ( 'header_image' === $name ) {
 				$name     = 'header_image_data';
@@ -1600,9 +1646,9 @@
 		}
 
 		// Such is The WordPress Way.
-		require_once( ABSPATH . 'wp-admin/includes/file.php' );
-		require_once( ABSPATH . 'wp-admin/includes/media.php' );
-		require_once( ABSPATH . 'wp-admin/includes/image.php' );
+		require_once ABSPATH . 'wp-admin/includes/file.php';
+		require_once ABSPATH . 'wp-admin/includes/media.php';
+		require_once ABSPATH . 'wp-admin/includes/image.php';
 
 		foreach ( $attachments as $symbol => $attachment ) {
 
@@ -1768,7 +1814,7 @@
 	 * @param WP_Customize_Setting $setting A WP_Customize_Setting derived object.
 	 * @param mixed                $default Value returned $setting has no post value (added in 4.2.0)
 	 *                                      or the post value is invalid (added in 4.6.0).
-	 * @return string|mixed $post_value Sanitized value or the $default provided.
+	 * @return string|mixed Sanitized value or the $default provided.
 	 */
 	public function post_value( $setting, $default = null ) {
 		$post_values = $this->unsanitized_post_values();
@@ -1861,7 +1907,14 @@
 		 * that the user's session has expired and they need to re-authenticate.
 		 */
 		if ( $this->messenger_channel && ! current_user_can( 'customize' ) ) {
-			$this->wp_die( -1, __( 'Unauthorized. You may remove the customize_messenger_channel param to preview as frontend.' ) );
+			$this->wp_die(
+				-1,
+				sprintf(
+					/* translators: %s: customize_messenger_channel */
+					__( 'Unauthorized. You may remove the %s param to preview as frontend.' ),
+					'<code>customize_messenger_channel<code>'
+				)
+			);
 			return;
 		}
 
@@ -1905,6 +1958,7 @@
 	 * Add customize state query params to a given URL if preview is allowed.
 	 *
 	 * @since 4.7.0
+	 *
 	 * @see wp_redirect()
 	 * @see WP_Customize_Manager::get_allowed_url()
 	 *
@@ -2245,7 +2299,7 @@
 	 *
 	 * @since 3.4.0
 	 *
-	 * @param $current_theme {@internal Parameter is not used}
+	 * @param mixed $current_theme {@internal Parameter is not used}
 	 * @return string Theme name.
 	 */
 	public function current_theme( $current_theme ) {
@@ -2500,7 +2554,7 @@
 				$this->dismiss_user_auto_draft_changesets();
 			}
 
-			// Note that if the changeset status was publish, then it will get set to trash if revisions are not supported.
+			// Note that if the changeset status was publish, then it will get set to Trash if revisions are not supported.
 			$response['changeset_status'] = $changeset_post->post_status;
 			if ( $is_publish && 'trash' === $response['changeset_status'] ) {
 				$response['changeset_status'] = 'publish';
@@ -2729,7 +2783,7 @@
 		if ( $update_transactionally && $invalid_setting_count > 0 ) {
 			$response = array(
 				'setting_validities' => $setting_validities,
-				/* translators: %s: number of invalid settings */
+				/* translators: %s: Number of invalid settings. */
 				'message'            => sprintf( _n( 'Unable to save due to %s invalid setting.', 'Unable to save due to %s invalid settings.', $invalid_setting_count ), number_format_i18n( $invalid_setting_count ) ),
 			);
 			return new WP_Error( 'transaction_fail', '', $response );
@@ -2843,13 +2897,9 @@
 		}
 
 		// Gather the data for wp_insert_post()/wp_update_post().
-		$json_options = 0;
-		if ( defined( 'JSON_UNESCAPED_SLASHES' ) ) {
-			$json_options |= JSON_UNESCAPED_SLASHES; // Introduced in PHP 5.4. This is only to improve readability as slashes needn't be escaped in storage.
-		}
-		$json_options |= JSON_PRETTY_PRINT; // Also introduced in PHP 5.4, but WP defines constant for back compat. See WP Trac #30139.
-		$post_array    = array(
-			'post_content' => wp_json_encode( $data, $json_options ),
+		$post_array = array(
+			// JSON_UNESCAPED_SLASHES is only to improve readability as slashes needn't be escaped in storage.
+			'post_content' => wp_json_encode( $data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT ),
 		);
 		if ( $args['title'] ) {
 			$post_array['post_title'] = $args['title'];
@@ -2886,33 +2936,26 @@
 		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().
+		 * 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().
 		 */
-
-		// Prevent content filters from corrupting JSON in post_content.
-		$has_kses = ( false !== has_filter( 'content_save_pre', 'wp_filter_post_kses' ) );
-		if ( $has_kses ) {
-			kses_remove_filters();
-		}
-		$has_targeted_link_rel_filters = ( false !== has_filter( 'content_save_pre', 'wp_targeted_link_rel' ) );
-		if ( $has_targeted_link_rel_filters ) {
-			wp_remove_targeted_link_rel_filters();
-		}
-
-		// Note that 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.
 				add_filter( 'map_meta_cap', array( $this, 'grant_edit_post_capability_for_changeset' ), 10, 4 );
+
 				$post_array['post_ID']   = $post_array['ID'];
 				$post_array['post_type'] = 'customize_changeset';
-				$r                       = wp_create_post_autosave( wp_slash( $post_array ) );
+
+				$r = wp_create_post_autosave( wp_slash( $post_array ) );
+
 				remove_filter( 'map_meta_cap', array( $this, 'grant_edit_post_capability_for_changeset' ), 10 );
 			} else {
 				$post_array['edit_date'] = true; // Prevent date clearing.
-				$r                       = wp_update_post( wp_slash( $post_array ), true );
+
+				$r = wp_update_post( wp_slash( $post_array ), true );
 
 				// Delete autosave revision for user when the changeset is updated.
 				if ( ! empty( $args['user_id'] ) ) {
@@ -2928,14 +2971,7 @@
 				$this->_changeset_post_id = $r; // Update cached post ID for the loaded changeset.
 			}
 		}
-
-		// Restore removed content filters.
-		if ( $has_kses ) {
-			kses_init_filters();
-		}
-		if ( $has_targeted_link_rel_filters ) {
-			wp_init_targeted_link_rel_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.
 
@@ -2954,6 +2990,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
@@ -2962,8 +3043,9 @@
 	 * untouched.
 	 *
 	 * @since 4.9.0
+	 *
+	 * @see wp_trash_post()
 	 * @global wpdb $wpdb WordPress database abstraction object.
-	 * @see wp_trash_post()
 	 *
 	 * @param int|WP_Post $post The changeset post.
 	 * @return mixed A WP_Post object for the trashed post or an empty value on failure.
@@ -3064,13 +3146,27 @@
 			return;
 		}
 
-		if ( $changeset_post_id && ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->delete_post, $changeset_post_id ) ) {
-			wp_send_json_error(
-				array(
-					'code'    => 'changeset_trash_unauthorized',
-					'message' => __( 'Unable to trash changes.' ),
-				)
-			);
+		if ( $changeset_post_id ) {
+			if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->delete_post, $changeset_post_id ) ) {
+				wp_send_json_error(
+					array(
+						'code'    => 'changeset_trash_unauthorized',
+						'message' => __( 'Unable to trash changes.' ),
+					)
+				);
+			}
+
+			$lock_user = (int) wp_check_post_lock( $changeset_post_id );
+
+			if ( $lock_user && get_current_user_id() !== $lock_user ) {
+				wp_send_json_error(
+					array(
+						'code'     => 'changeset_locked',
+						'message'  => __( 'Changeset is being edited by other user.' ),
+						'lockUser' => $this->get_lock_user_data( $lock_user ),
+					)
+				);
+			}
 		}
 
 		if ( 'trash' === get_post_status( $changeset_post_id ) ) {
@@ -3111,6 +3207,7 @@
 	 * This should be able to be removed once #40922 is addressed in core.
 	 *
 	 * @since 4.9.0
+	 *
 	 * @link https://core.trac.wordpress.org/ticket/40922
 	 * @see WP_Customize_Manager::save_changeset_post()
 	 * @see _wp_translate_postdata()
@@ -3119,7 +3216,7 @@
 	 * @param string   $cap     Capability name.
 	 * @param int      $user_id The user ID.
 	 * @param array    $args    Adds the context to the cap. Typically the object ID.
-	 * @return array   Capabilities.
+	 * @return array Capabilities.
 	 */
 	public function grant_edit_post_capability_for_changeset( $caps, $cap, $user_id, $args ) {
 		if ( 'edit_post' === $cap && ! empty( $args[0] ) && 'customize_changeset' === get_post_type( $args[0] ) ) {
@@ -3134,8 +3231,8 @@
 	 *
 	 * @since 4.9.0
 	 *
-	 * @param int  $changeset_post_id Changeset post id.
-	 * @param bool $take_over Take over the changeset, default is false.
+	 * @param int  $changeset_post_id Changeset post ID.
+	 * @param bool $take_over Whether to take over the changeset. Default false.
 	 */
 	public function set_changeset_lock( $changeset_post_id, $take_over = false ) {
 		if ( $changeset_post_id ) {
@@ -3159,7 +3256,7 @@
 	 *
 	 * @since 4.9.0
 	 *
-	 * @param int $changeset_post_id Changeset post id.
+	 * @param int $changeset_post_id Changeset post ID.
 	 */
 	public function refresh_changeset_lock( $changeset_post_id ) {
 		if ( ! $changeset_post_id ) {
@@ -3315,7 +3412,6 @@
 	 * @param bool    $post_has_changed Whether the post has changed.
 	 * @param WP_Post $last_revision    The last revision post object.
 	 * @param WP_Post $post             The post object.
-	 *
 	 * @return bool Whether a revision should be made.
 	 */
 	public function _filter_revision_post_has_changed( $post_has_changed, $last_revision, $post ) {
@@ -3340,8 +3436,9 @@
 	 * invoking this method.
 	 *
 	 * @since 4.7.0
+	 *
 	 * @see _wp_customize_publish_changeset()
-	 * @global wpdb $wpdb
+	 * @global wpdb $wpdb WordPress database abstraction object.
 	 *
 	 * @param int $changeset_post_id ID for customize_changeset post. Defaults to the changeset for the current manager instance.
 	 * @return true|WP_Error True or error info.
@@ -3639,26 +3736,14 @@
 	 * @since 3.4.0
 	 * @since 4.5.0 Return added WP_Customize_Setting instance.
 	 *
+	 * @see WP_Customize_Setting::__construct()
+	 * @link https://developer.wordpress.org/themes/customize-api
+	 *
 	 * @param WP_Customize_Setting|string $id   Customize Setting object, or ID.
-	 * @param array                       $args {
-	 *  Optional. Array of properties for the new WP_Customize_Setting. Default empty array.
-	 *
-	 *  @type string       $type                  Type of the setting. Default 'theme_mod'.
-	 *  @type string       $capability            Capability required for the setting. Default 'edit_theme_options'
-	 *  @type string|array $theme_supports        Theme features required to support the panel. Default is none.
-	 *  @type string       $default               Default value for the setting. Default is empty string.
-	 *  @type string       $transport             Options for rendering the live preview of changes in Customizer.
-	 *                                            Using 'refresh' makes the change visible by reloading the whole preview.
-	 *                                            Using 'postMessage' allows a custom JavaScript to handle live changes.
-	 * @link https://developer.wordpress.org/themes/customize-api
-	 *                                            Default is 'refresh'
-	 *  @type callable     $validate_callback     Server-side validation callback for the setting's value.
-	 *  @type callable     $sanitize_callback     Callback to filter a Customize setting value in un-slashed form.
-	 *  @type callable     $sanitize_js_callback  Callback to convert a Customize PHP setting value to a value that is
-	 *                                            JSON serializable.
-	 *  @type bool         $dirty                 Whether or not the setting is initially dirty when created.
-	 * }
-	 * @return WP_Customize_Setting             The instance of the setting that was added.
+	 * @param array                       $args Optional. Array of properties for the new Setting object.
+	 *                                          See WP_Customize_Setting::__construct() for information
+	 *                                          on accepted arguments. Default empty array.
+	 * @return WP_Customize_Setting The instance of the setting that was added.
 	 */
 	public function add_setting( $id, $args = array() ) {
 		if ( $id instanceof WP_Customize_Setting ) {
@@ -3696,7 +3781,7 @@
 	public function add_dynamic_settings( $setting_ids ) {
 		$new_settings = array();
 		foreach ( $setting_ids as $setting_id ) {
-			// Skip settings already created
+			// Skip settings already created.
 			if ( $this->get_setting( $setting_id ) ) {
 				continue;
 			}
@@ -3757,6 +3842,8 @@
 	/**
 	 * Remove a customize setting.
 	 *
+	 * Note that removing the setting doesn't destroy the WP_Customize_Setting instance or remove its filters.
+	 *
 	 * @since 3.4.0
 	 *
 	 * @param string $id Customize Setting ID.
@@ -3771,19 +3858,13 @@
 	 * @since 4.0.0
 	 * @since 4.5.0 Return added WP_Customize_Panel instance.
 	 *
-	 * @param WP_Customize_Panel|string $id   Customize Panel object, or Panel ID.
-	 * @param array                     $args {
-	 *  Optional. Array of properties for the new Panel object. Default empty array.
-	 *  @type int          $priority              Priority of the panel, defining the display order of panels and sections.
-	 *                                            Default 160.
-	 *  @type string       $capability            Capability required for the panel. Default `edit_theme_options`
-	 *  @type string|array $theme_supports        Theme features required to support the panel.
-	 *  @type string       $title                 Title of the panel to show in UI.
-	 *  @type string       $description           Description to show in the UI.
-	 *  @type string       $type                  Type of the panel.
-	 *  @type callable     $active_callback       Active callback.
-	 * }
-	 * @return WP_Customize_Panel             The instance of the panel that was added.
+	 * @see WP_Customize_Panel::__construct()
+	 *
+	 * @param WP_Customize_Panel|string $id   Customize Panel object, or ID.
+	 * @param array                     $args Optional. Array of properties for the new Panel object.
+	 *                                        See WP_Customize_Panel::__construct() for information
+	 *                                        on accepted arguments. Default empty array.
+	 * @return WP_Customize_Panel The instance of the panel that was added.
 	 */
 	public function add_panel( $id, $args = array() ) {
 		if ( $id instanceof WP_Customize_Panel ) {
@@ -3813,6 +3894,8 @@
 	/**
 	 * Remove a customize panel.
 	 *
+	 * Note that removing the panel doesn't destroy the WP_Customize_Panel instance or remove its filters.
+	 *
 	 * @since 4.0.0
 	 *
 	 * @param string $id Panel ID to remove.
@@ -3820,11 +3903,15 @@
 	public function remove_panel( $id ) {
 		// Removing core components this way is _doing_it_wrong().
 		if ( in_array( $id, $this->components, true ) ) {
-			/* translators: 1: panel id, 2: link to 'customize_loaded_components' filter reference */
 			$message = sprintf(
+				/* translators: 1: Panel ID, 2: Link to 'customize_loaded_components' filter reference. */
 				__( 'Removing %1$s manually will cause PHP warnings. Use the %2$s filter instead.' ),
 				$id,
-				'<a href="' . esc_url( 'https://developer.wordpress.org/reference/hooks/customize_loaded_components/' ) . '"><code>customize_loaded_components</code></a>'
+				sprintf(
+					'<a href="%1$s">%2$s</a>',
+					esc_url( 'https://developer.wordpress.org/reference/hooks/customize_loaded_components/' ),
+					'<code>customize_loaded_components</code>'
+				)
 			);
 
 			_doing_it_wrong( __METHOD__, $message, '4.5.0' );
@@ -3865,21 +3952,13 @@
 	 * @since 3.4.0
 	 * @since 4.5.0 Return added WP_Customize_Section instance.
 	 *
-	 * @param WP_Customize_Section|string $id   Customize Section object, or Section ID.
-	 * @param array                     $args {
-	 *  Optional. Array of properties for the new Section object. Default empty array.
-	 *  @type int          $priority              Priority of the section, defining the display order of panels and sections.
-	 *                                            Default 160.
-	 *  @type string       $panel                 The panel this section belongs to (if any). Default empty.
-	 *  @type string       $capability            Capability required for the section. Default 'edit_theme_options'
-	 *  @type string|array $theme_supports        Theme features required to support the section.
-	 *  @type string       $title                 Title of the section to show in UI.
-	 *  @type string       $description           Description to show in the UI.
-	 *  @type string       $type                  Type of the section.
-	 *  @type callable     $active_callback       Active callback.
-	 *  @type bool         $description_hidden    Hide the description behind a help icon, instead of inline above the first control. Default false.
-	 * }
-	 * @return WP_Customize_Section             The instance of the section that was added.
+	 * @see WP_Customize_Section::__construct()
+	 *
+	 * @param WP_Customize_Section|string $id   Customize Section object, or ID.
+	 * @param array                       $args Optional. Array of properties for the new Section object.
+	 *                                          See WP_Customize_Section::__construct() for information
+	 *                                          on accepted arguments. Default empty array.
+	 * @return WP_Customize_Section The instance of the section that was added.
 	 */
 	public function add_section( $id, $args = array() ) {
 		if ( $id instanceof WP_Customize_Section ) {
@@ -3909,6 +3988,8 @@
 	/**
 	 * Remove a customize section.
 	 *
+	 * Note that removing the section doesn't destroy the WP_Customize_Section instance or remove its filters.
+	 *
 	 * @since 3.4.0
 	 *
 	 * @param string $id Section ID.
@@ -3950,28 +4031,13 @@
 	 * @since 3.4.0
 	 * @since 4.5.0 Return added WP_Customize_Control instance.
 	 *
+	 * @see WP_Customize_Control::__construct()
+	 *
 	 * @param WP_Customize_Control|string $id   Customize Control object, or ID.
-	 * @param array                       $args {
-	 *  Optional. Array of properties for the new Control object. Default empty array.
-	 *
-	 *  @type array        $settings              All settings tied to the control. If undefined, defaults to `$setting`.
-	 *                                            IDs in the array correspond to the ID of a registered `WP_Customize_Setting`.
-	 *  @type string       $setting               The primary setting for the control (if there is one). Default is 'default'.
-	 *  @type string       $capability            Capability required to use this control. Normally derived from `$settings`.
-	 *  @type int          $priority              Order priority to load the control. Default 10.
-	 *  @type string       $section               The section this control belongs to. Default empty.
-	 *  @type string       $label                 Label for the control. Default empty.
-	 *  @type string       $description           Description for the control. Default empty.
-	 *  @type array        $choices               List of choices for 'radio' or 'select' type controls, where values
-	 *                                            are the keys, and labels are the values. Default empty array.
-	 *  @type array        $input_attrs           List of custom input attributes for control output, where attribute
-	 *                                            names are the keys and values are the values. Default empty array.
-	 *  @type bool         $allow_addition        Show UI for adding new content, currently only used for the
-	 *                                            dropdown-pages control. Default false.
-	 *  @type string       $type                  The type of the control. Default 'text'.
-	 *  @type callback     $active_callback       Active callback.
-	 * }
-	 * @return WP_Customize_Control             The instance of the control that was added.
+	 * @param array                       $args Optional. Array of properties for the new Control object.
+	 *                                          See WP_Customize_Control::__construct() for information
+	 *                                          on accepted arguments. Default empty array.
+	 * @return WP_Customize_Control The instance of the control that was added.
 	 */
 	public function add_control( $id, $args = array() ) {
 		if ( $id instanceof WP_Customize_Control ) {
@@ -4001,6 +4067,8 @@
 	/**
 	 * Remove a customize control.
 	 *
+	 * Note that removing the control doesn't destroy the WP_Customize_Control instance or remove its filters.
+	 *
 	 * @since 3.4.0
 	 *
 	 * @param string $id ID of the control.
@@ -4467,10 +4535,10 @@
 	 */
 	public function get_document_title_template() {
 		if ( $this->is_theme_active() ) {
-			/* translators: %s: document title from the preview */
+			/* translators: %s: Document title from the preview. */
 			$document_title_tmpl = __( 'Customize: %s' );
 		} else {
-			/* translators: %s: document title from the preview */
+			/* translators: %s: Document title from the preview. */
 			$document_title_tmpl = __( 'Live Preview: %s' );
 		}
 		$document_title_tmpl = html_entity_decode( $document_title_tmpl, ENT_QUOTES, 'UTF-8' ); // Because exported to JS and assigned to document.title.
@@ -4533,7 +4601,7 @@
 	 *
 	 * @since 4.7.0
 	 *
-	 * @returns array Allowed URLs.
+	 * @return array Allowed URLs.
 	 */
 	public function get_allowed_urls() {
 		$allowed_urls = array( home_url( '/' ) );
@@ -4586,9 +4654,13 @@
 	 *
 	 * @since 4.4.0
 	 *
+	 * @global array $_registered_pages
+	 *
 	 * @return string URL for link to close Customizer.
 	 */
 	public function get_return_url() {
+		global $_registered_pages;
+
 		$referer                    = wp_get_referer();
 		$excluded_referer_basenames = array( 'customize.php', 'wp-login.php' );
 
@@ -4601,6 +4673,22 @@
 		} else {
 			$return_url = home_url( '/' );
 		}
+
+		$return_url_basename = wp_basename( parse_url( $this->return_url, PHP_URL_PATH ) );
+		$return_url_query    = parse_url( $this->return_url, PHP_URL_QUERY );
+
+		if ( 'themes.php' === $return_url_basename && $return_url_query ) {
+			parse_str( $return_url_query, $query_vars );
+
+			/*
+			 * If the return URL is a page added by a theme to the Appearance menu via add_submenu_page(),
+			 * verify that belongs to the active theme, otherwise fall back to the Themes screen.
+			 */
+			if ( isset( $query_vars['page'] ) && ! isset( $_registered_pages[ "appearance_page_{$query_vars['page']}" ] ) ) {
+				$return_url = admin_url( 'themes.php' );
+			}
+		}
+
 		return $return_url;
 	}
 
@@ -4612,9 +4700,9 @@
 	 * @param array $autofocus {
 	 *     Mapping of 'panel', 'section', 'control' to the ID which should be autofocused.
 	 *
-	 *     @type string [$control]  ID for control to be autofocused.
-	 *     @type string [$section]  ID for section to be autofocused.
-	 *     @type string [$panel]    ID for panel to be autofocused.
+	 *     @type string $control ID for control to be autofocused.
+	 *     @type string $section ID for section to be autofocused.
+	 *     @type string $panel   ID for panel to be autofocused.
 	 * }
 	 */
 	public function set_autofocus( $autofocus ) {
@@ -4629,9 +4717,9 @@
 	 * @return array {
 	 *     Mapping of 'panel', 'section', 'control' to the ID which should be autofocused.
 	 *
-	 *     @type string [$control]  ID for control to be autofocused.
-	 *     @type string [$section]  ID for section to be autofocused.
-	 *     @type string [$panel]    ID for panel to be autofocused.
+	 *     @type string $control ID for control to be autofocused.
+	 *     @type string $section ID for section to be autofocused.
+	 *     @type string $panel   ID for panel to be autofocused.
 	 * }
 	 */
 	public function get_autofocus() {
@@ -4809,11 +4897,11 @@
 			'previewableDevices'     => $this->get_previewable_devices(),
 			'l10n'                   => array(
 				'confirmDeleteTheme'   => __( 'Are you sure you want to delete this theme?' ),
-				/* translators: %d: number of theme search results, which cannot currently consider singular vs. plural forms */
+				/* translators: %d: Number of theme search results, which cannot currently consider singular vs. plural forms. */
 				'themeSearchResults'   => __( '%d themes found' ),
-				/* translators: %d: number of themes being displayed, which cannot currently consider singular vs. plural forms */
+				/* translators: %d: Number of themes being displayed, which cannot currently consider singular vs. plural forms. */
 				'announceThemeCount'   => __( 'Displaying %d themes' ),
-				/* translators: %s: theme name */
+				/* translators: %s: Theme name. */
 				'announceThemeDetails' => __( 'Showing details for theme: %s' ),
 			),
 		);
@@ -5069,7 +5157,7 @@
 					'label'       => __( 'Site Icon' ),
 					'description' => sprintf(
 						'<p>' . __( 'Site Icons are what you see in browser tabs, bookmark bars, and within the WordPress mobile apps. Upload one here!' ) . '</p>' .
-						/* translators: %s: site icon size in pixels */
+						/* translators: %s: Site icon size in pixels. */
 						'<p>' . __( 'Site Icons should be square and at least %s pixels.' ) . '</p>',
 						'<strong>512 &times; 512</strong>'
 					),
@@ -5098,10 +5186,10 @@
 					'label'         => __( 'Logo' ),
 					'section'       => 'title_tagline',
 					'priority'      => 8,
-					'height'        => $custom_logo_args[0]['height'],
-					'width'         => $custom_logo_args[0]['width'],
-					'flex_height'   => $custom_logo_args[0]['flex-height'],
-					'flex_width'    => $custom_logo_args[0]['flex-width'],
+					'height'        => isset( $custom_logo_args[0]['height'] ) ? $custom_logo_args[0]['height'] : null,
+					'width'         => isset( $custom_logo_args[0]['width'] ) ? $custom_logo_args[0]['width'] : null,
+					'flex_height'   => isset( $custom_logo_args[0]['flex-height'] ) ? $custom_logo_args[0]['flex-height'] : null,
+					'flex_width'    => isset( $custom_logo_args[0]['flex-width'] ) ? $custom_logo_args[0]['flex-width'] : null,
 					'button_labels' => array(
 						'select'       => __( 'Select logo' ),
 						'change'       => __( 'Change logo' ),
@@ -5146,8 +5234,8 @@
 			)
 		);
 
-		// Input type: checkbox
-		// With custom value
+		// Input type: checkbox.
+		// With custom value.
 		$this->add_control(
 			'display_header_text',
 			array(
@@ -5170,8 +5258,8 @@
 			)
 		);
 
-		// Input type: Color
-		// With sanitize_callback
+		// Input type: color.
+		// With sanitize_callback.
 		$this->add_setting(
 			'background_color',
 			array(
@@ -5204,21 +5292,21 @@
 			$height = absint( get_theme_support( 'custom-header', 'height' ) );
 			if ( $width && $height ) {
 				$control_description = sprintf(
-					/* translators: 1: .mp4, 2: header size in pixels */
+					/* translators: 1: .mp4, 2: Header size in pixels. */
 					__( 'Upload your video in %1$s format and minimize its file size for best results. Your theme recommends dimensions of %2$s pixels.' ),
 					'<code>.mp4</code>',
 					sprintf( '<strong>%s &times; %s</strong>', $width, $height )
 				);
 			} elseif ( $width ) {
 				$control_description = sprintf(
-					/* translators: 1: .mp4, 2: header width in pixels */
+					/* translators: 1: .mp4, 2: Header width in pixels. */
 					__( 'Upload your video in %1$s format and minimize its file size for best results. Your theme recommends a width of %2$s pixels.' ),
 					'<code>.mp4</code>',
 					sprintf( '<strong>%s</strong>', $width )
 				);
 			} else {
 				$control_description = sprintf(
-					/* translators: 1: .mp4, 2: header height in pixels */
+					/* translators: 1: .mp4, 2: Header height in pixels. */
 					__( 'Upload your video in %1$s format and minimize its file size for best results. Your theme recommends a height of %2$s pixels.' ),
 					'<code>.mp4</code>',
 					sprintf( '<strong>%s</strong>', $height )
@@ -5436,7 +5524,7 @@
 				'section' => 'background_image',
 				'type'    => 'select',
 				'choices' => array(
-					'auto'    => __( 'Original' ),
+					'auto'    => _x( 'Original', 'Original Size' ),
 					'contain' => __( 'Fit to Screen' ),
 					'cover'   => __( 'Fill Screen' ),
 				),
@@ -5568,7 +5656,7 @@
 			' <a href="%1$s" class="external-link" target="_blank">%2$s<span class="screen-reader-text"> %3$s</span></a>',
 			esc_url( __( 'https://codex.wordpress.org/CSS' ) ),
 			__( 'Learn more about CSS' ),
-			/* translators: accessibility text */
+			/* translators: Accessibility text. */
 			__( '(opens in a new tab)' )
 		);
 		$section_description .= '</p>';
@@ -5577,19 +5665,19 @@
 		$section_description .= '<ul>';
 		$section_description .= '<li id="editor-keyboard-trap-help-2">' . __( 'In the editing area, the Tab key enters a tab character.' ) . '</li>';
 		$section_description .= '<li id="editor-keyboard-trap-help-3">' . __( 'To move away from this area, press the Esc key followed by the Tab key.' ) . '</li>';
-		$section_description .= '<li id="editor-keyboard-trap-help-4">' . __( 'Screen reader users: when in forms mode, you may need to press the escape key twice.' ) . '</li>';
+		$section_description .= '<li id="editor-keyboard-trap-help-4">' . __( 'Screen reader users: when in forms mode, you may need to press the Esc key twice.' ) . '</li>';
 		$section_description .= '</ul>';
 
 		if ( 'false' !== wp_get_current_user()->syntax_highlighting ) {
 			$section_description .= '<p>';
 			$section_description .= sprintf(
-				/* translators: 1: link to user profile, 2: additional link attributes, 3: accessibility text */
+				/* translators: 1: Link to user profile, 2: Additional link attributes, 3: Accessibility text. */
 				__( 'The edit field automatically highlights code syntax. You can disable this in your <a href="%1$s" %2$s>user profile%3$s</a> to work in plain text mode.' ),
 				esc_url( get_edit_profile_url() ),
 				'class="external-link" target="_blank"',
 				sprintf(
 					'<span class="screen-reader-text"> %s</span>',
-					/* translators: accessibility text */
+					/* translators: Accessibility text. */
 					__( '(opens in a new tab)' )
 				)
 			);
@@ -5644,7 +5732,7 @@
 	 *
 	 * @since 4.7.0
 	 *
-	 * @returns bool Whether there are published (or to be published) pages.
+	 * @return bool Whether there are published (or to be published) pages.
 	 */
 	public function has_published_pages() {
 
@@ -5799,9 +5887,11 @@
 				$theme->active = ( isset( $_POST['customized_theme'] ) && $_POST['customized_theme'] === $theme->slug );
 
 				// Map available theme properties to installed theme properties.
-				$theme->id           = $theme->slug;
-				$theme->screenshot   = array( $theme->screenshot_url );
-				$theme->authorAndUri = wp_kses( $theme->author['display_name'], $themes_allowedtags );
+				$theme->id            = $theme->slug;
+				$theme->screenshot    = array( $theme->screenshot_url );
+				$theme->authorAndUri  = wp_kses( $theme->author['display_name'], $themes_allowedtags );
+				$theme->compatibleWP  = is_wp_version_compatible( $theme->requires ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName
+				$theme->compatiblePHP = is_php_version_compatible( $theme->requires_php ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName
 
 				if ( isset( $theme->parent ) ) {
 					$theme->parent = $theme->parent['slug'];
@@ -5866,17 +5956,17 @@
 	 *
 	 * @since 4.7.0
 	 *
-	 * @param string $value Repeat value.
+	 * @param string               $value   Repeat value.
 	 * @param WP_Customize_Setting $setting Setting.
 	 * @return string|WP_Error Background value or validation error.
 	 */
 	public function _sanitize_background_setting( $value, $setting ) {
 		if ( 'background_repeat' === $setting->id ) {
-			if ( ! in_array( $value, array( 'repeat-x', 'repeat-y', 'repeat', 'no-repeat' ) ) ) {
+			if ( ! in_array( $value, array( 'repeat-x', 'repeat-y', 'repeat', 'no-repeat' ), true ) ) {
 				return new WP_Error( 'invalid_value', __( 'Invalid value for background repeat.' ) );
 			}
 		} elseif ( 'background_attachment' === $setting->id ) {
-			if ( ! in_array( $value, array( 'fixed', 'scroll' ) ) ) {
+			if ( ! in_array( $value, array( 'fixed', 'scroll' ), true ) ) {
 				return new WP_Error( 'invalid_value', __( 'Invalid value for background attachment.' ) );
 			}
 		} elseif ( 'background_position_x' === $setting->id ) {
@@ -5908,9 +5998,9 @@
 	 *
 	 * @since 4.7.0
 	 *
-	 * @param array $response Response.
+	 * @param array                          $response          Response.
 	 * @param WP_Customize_Selective_Refresh $selective_refresh Selective refresh component.
-	 * @param array $partials Array of partials.
+	 * @param array                          $partials          Array of partials.
 	 * @return array
 	 */
 	public function export_header_video_settings( $response, $selective_refresh, $partials ) {
@@ -5929,14 +6019,14 @@
 	 * @since 4.7.0
 	 *
 	 * @param WP_Error $validity
-	 * @param mixed $value
+	 * @param mixed    $value
 	 * @return mixed
 	 */
 	public function _validate_header_video( $validity, $value ) {
 		$video = get_attached_file( absint( $value ) );
 		if ( $video ) {
 			$size = filesize( $video );
-			if ( 8 < $size / pow( 1024, 2 ) ) { // Check whether the size is larger than 8MB.
+			if ( $size > 8 * MB_IN_BYTES ) {
 				$validity->add(
 					'size_too_large',
 					__( 'This video file is too large to use as a header video. Try a shorter video or optimize the compression settings and re-upload a file that is less than 8MB. Or, upload your video to YouTube and link it with the option below.' )
@@ -5965,7 +6055,7 @@
 	 * @since 4.7.0
 	 *
 	 * @param WP_Error $validity
-	 * @param mixed $value
+	 * @param mixed    $value
 	 * @return mixed
 	 */
 	public function _validate_external_header_video( $validity, $value ) {