diff -r 3d4e9c994f10 -r a86126ab1dd4 wp/wp-includes/class-wp-customize-manager.php --- 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.+)}}$/', $value, $matches ) ) { + + // Serialize the value to check for post symbols. + $value = maybe_serialize( $value ); + + if ( is_serialized( $value ) ) { + if ( preg_match( '/s:\d+:"{{(?P.+)}}"/', $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.+)}}$/', $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.+)}}$/', $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.+)}}"/', $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.+)}}$/', $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.' ), + 'customize_messenger_channel' + ) + ); 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, - 'customize_loaded_components' + sprintf( + '%2$s', + esc_url( 'https://developer.wordpress.org/reference/hooks/customize_loaded_components/' ), + 'customize_loaded_components' + ) ); _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( '

' . __( 'Site Icons are what you see in browser tabs, bookmark bars, and within the WordPress mobile apps. Upload one here!' ) . '

' . - /* translators: %s: site icon size in pixels */ + /* translators: %s: Site icon size in pixels. */ '

' . __( 'Site Icons should be square and at least %s pixels.' ) . '

', '512 × 512' ), @@ -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.' ), '.mp4', sprintf( '%s × %s', $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.' ), '.mp4', sprintf( '%s', $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.' ), '.mp4', sprintf( '%s', $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 @@ ' %2$s %3$s', esc_url( __( 'https://codex.wordpress.org/CSS' ) ), __( 'Learn more about CSS' ), - /* translators: accessibility text */ + /* translators: Accessibility text. */ __( '(opens in a new tab)' ) ); $section_description .= '

'; @@ -5577,19 +5665,19 @@ $section_description .= '
    '; $section_description .= '
  • ' . __( 'In the editing area, the Tab key enters a tab character.' ) . '
  • '; $section_description .= '
  • ' . __( 'To move away from this area, press the Esc key followed by the Tab key.' ) . '
  • '; - $section_description .= '
  • ' . __( 'Screen reader users: when in forms mode, you may need to press the escape key twice.' ) . '
  • '; + $section_description .= '
  • ' . __( 'Screen reader users: when in forms mode, you may need to press the Esc key twice.' ) . '
  • '; $section_description .= '
'; if ( 'false' !== wp_get_current_user()->syntax_highlighting ) { $section_description .= '

'; $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 user profile%3$s to work in plain text mode.' ), esc_url( get_edit_profile_url() ), 'class="external-link" target="_blank"', sprintf( ' %s', - /* 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 ) {