diff --git a/src/wp-includes/class-wp-customize-manager.php b/src/wp-includes/class-wp-customize-manager.php index b26833beb7..9ba93c575c 100644 --- a/src/wp-includes/class-wp-customize-manager.php +++ b/src/wp-includes/class-wp-customize-manager.php @@ -173,6 +173,14 @@ final class WP_Customize_Manager { */ protected $messenger_channel; + /** + * Whether settings should be previewed. + * + * @since 4.9.0 + * @var bool + */ + protected $settings_previewed; + /** * Unsanitized values for Customize Settings parsed from $_POST['customized']. * @@ -213,15 +221,16 @@ final class WP_Customize_Manager { * @param array $args { * Args. * - * @type string $changeset_uuid Changeset UUID, the post_name for the customize_changeset post containing the customized state. Defaults to new UUID. - * @type string $theme Theme to be previewed (for theme switch). Defaults to customize_theme or theme query params. - * @type string $messenger_channel Messenger channel. Defaults to customize_messenger_channel query param. + * @type string $changeset_uuid Changeset UUID, the post_name for the customize_changeset post containing the customized state. Defaults to new UUID. + * @type string $theme Theme to be previewed (for theme switch). Defaults to customize_theme or theme query params. + * @type string $messenger_channel Messenger channel. Defaults to customize_messenger_channel query param. + * @type bool $settings_previewed If settings should be previewed. Defaults to true. * } */ public function __construct( $args = array() ) { $args = array_merge( - array_fill_keys( array( 'changeset_uuid', 'theme', 'messenger_channel' ), null ), + array_fill_keys( array( 'changeset_uuid', 'theme', 'messenger_channel', 'settings_previewed' ), null ), $args ); @@ -242,9 +251,14 @@ final class WP_Customize_Manager { $args['messenger_channel'] = sanitize_key( wp_unslash( $_REQUEST['customize_messenger_channel'] ) ); } + if ( ! isset( $args['settings_previewed'] ) ) { + $args['settings_previewed'] = true; + } + $this->original_stylesheet = get_stylesheet(); $this->theme = wp_get_theme( $args['theme'] ); $this->messenger_channel = $args['messenger_channel']; + $this->settings_previewed = ! empty( $args['settings_previewed'] ); $this->_changeset_uuid = $args['changeset_uuid']; require_once( ABSPATH . WPINC . '/class-wp-customize-setting.php' ); @@ -621,6 +635,18 @@ final class WP_Customize_Manager { do_action( 'stop_previewing_theme', $this ); } + /** + * Gets whether settings are or will be previewed. + * + * @since 4.9.0 + * @see WP_Customize_Setting::preview() + * + * @return bool + */ + public function settings_previewed() { + return $this->settings_previewed; + } + /** * Get the changeset UUID. * @@ -728,15 +754,7 @@ final class WP_Customize_Manager { */ do_action( 'customize_register', $this ); - /* - * Note that settings must be previewed here even outside the customizer preview - * and also in the customizer pane itself. This is to enable loading an existing - * changeset into the customizer. Previewing the settings only has to be prevented - * in the case of a customize_save action because then update_option() - * may short-circuit because it will detect that there are no changes to - * make. - */ - if ( ! $this->doing_ajax( 'customize_save' ) ) { + if ( $this->settings_previewed() ) { foreach ( $this->settings as $setting ) { $setting->preview(); } diff --git a/src/wp-includes/class-wp-customize-nav-menus.php b/src/wp-includes/class-wp-customize-nav-menus.php index 9b24fa0b67..3d7b1f0deb 100644 --- a/src/wp-includes/class-wp-customize-nav-menus.php +++ b/src/wp-includes/class-wp-customize-nav-menus.php @@ -526,13 +526,10 @@ final class WP_Customize_Nav_Menus { $nav_menus_setting_ids[] = $setting_id; } } - $this->manager->add_dynamic_settings( $nav_menus_setting_ids ); - if ( ! $this->manager->doing_ajax( 'customize_save' ) ) { - foreach ( $nav_menus_setting_ids as $setting_id ) { - $setting = $this->manager->get_setting( $setting_id ); - if ( $setting ) { - $setting->preview(); - } + $settings = $this->manager->add_dynamic_settings( $nav_menus_setting_ids ); + if ( $this->manager->settings_previewed() ) { + foreach ( $settings as $setting ) { + $setting->preview(); } } diff --git a/src/wp-includes/class-wp-customize-widgets.php b/src/wp-includes/class-wp-customize-widgets.php index 856a832ed9..973411b520 100644 --- a/src/wp-includes/class-wp-customize-widgets.php +++ b/src/wp-includes/class-wp-customize-widgets.php @@ -211,12 +211,7 @@ final class WP_Customize_Widgets { $settings = $this->manager->add_dynamic_settings( array_unique( $widget_setting_ids ) ); - /* - * Preview settings right away so that widgets and sidebars will get registered properly. - * But don't do this if a customize_save because this will cause WP to think there is nothing - * changed that needs to be saved. - */ - if ( ! $this->manager->doing_ajax( 'customize_save' ) ) { + if ( $this->manager->settings_previewed() ) { foreach ( $settings as $setting ) { $setting->preview(); } @@ -506,7 +501,7 @@ final class WP_Customize_Widgets { } } - if ( ! $this->manager->doing_ajax( 'customize_save' ) ) { + if ( $this->manager->settings_previewed() ) { foreach ( $new_setting_ids as $new_setting_id ) { $this->manager->get_setting( $new_setting_id )->preview(); } diff --git a/src/wp-includes/theme.php b/src/wp-includes/theme.php index 33b491ab1c..a1cc0866b2 100644 --- a/src/wp-includes/theme.php +++ b/src/wp-includes/theme.php @@ -2816,8 +2816,24 @@ function _wp_customize_include() { $messenger_channel = sanitize_key( $input_vars['customize_messenger_channel'] ); } + /* + * Note that settings must be previewed even outside the customizer preview + * and also in the customizer pane itself. This is to enable loading an existing + * changeset into the customizer. Previewing the settings only has to be prevented + * here in the case of a customize_save action because this will cause WP to think + * there is nothing changed that needs to be saved. + */ + $is_customize_save_action = ( + wp_doing_ajax() + && + isset( $_REQUEST['action'] ) + && + 'customize_save' === wp_unslash( $_REQUEST['action'] ) + ); + $settings_previewed = ! $is_customize_save_action; + require_once ABSPATH . WPINC . '/class-wp-customize-manager.php'; - $GLOBALS['wp_customize'] = new WP_Customize_Manager( compact( 'changeset_uuid', 'theme', 'messenger_channel' ) ); + $GLOBALS['wp_customize'] = new WP_Customize_Manager( compact( 'changeset_uuid', 'theme', 'messenger_channel', 'settings_previewed' ) ); } /** @@ -2849,7 +2865,10 @@ function _wp_customize_publish_changeset( $new_status, $old_status, $changeset_p if ( empty( $wp_customize ) ) { require_once ABSPATH . WPINC . '/class-wp-customize-manager.php'; - $wp_customize = new WP_Customize_Manager( array( 'changeset_uuid' => $changeset_post->post_name ) ); + $wp_customize = new WP_Customize_Manager( array( + 'changeset_uuid' => $changeset_post->post_name, + 'settings_previewed' => false, + ) ); } if ( ! did_action( 'customize_register' ) ) { diff --git a/tests/phpunit/tests/customize/manager.php b/tests/phpunit/tests/customize/manager.php index 056dd2398f..2aaafb6278 100644 --- a/tests/phpunit/tests/customize/manager.php +++ b/tests/phpunit/tests/customize/manager.php @@ -215,6 +215,20 @@ class Tests_WP_Customize_Manager extends WP_UnitTestCase { $this->assertTrue( $show_admin_bar ); } + /** + * Test WP_Customize_Manager::settings_previewed(). + * + * @ticket 39221 + * @covers WP_Customize_Manager::settings_previewed() + */ + function test_settings_previewed() { + $wp_customize = new WP_Customize_Manager( array( 'settings_previewed' => false ) ); + $this->assertSame( false, $wp_customize->settings_previewed() ); + + $wp_customize = new WP_Customize_Manager(); + $this->assertSame( true, $wp_customize->settings_previewed() ); + } + /** * Test WP_Customize_Manager::changeset_uuid(). * @@ -1291,6 +1305,54 @@ class Tests_WP_Customize_Manager extends WP_UnitTestCase { $this->assertEquals( 'Unfiltered', get_option( 'scratchpad' ) ); } + /** + * Test saving settings by publishing a changeset outside of Customizer entirely. + * + * Widgets get their settings registered and previewed early in the admin, + * so this ensures that the previewing is bypassed when in the context of + * publishing + * + * @ticket 39221 + * @covers _wp_customize_publish_changeset() + * @see WP_Customize_Widgets::schedule_customize_register() + * @see WP_Customize_Widgets::customize_register() + */ + function test_wp_customize_publish_changeset() { + global $wp_customize; + $wp_customize = null; + + // Set the admin current screen to cause WP_Customize_Widgets::schedule_customize_register() to do early setting registration. + set_current_screen( 'edit' ); + $this->assertTrue( is_admin() ); + + $old_sidebars_widgets = get_option( 'sidebars_widgets' ); + $new_sidebars_widgets = $old_sidebars_widgets; + $this->assertGreaterThan( 2, count( $new_sidebars_widgets['sidebar-1'] ) ); + $new_sidebar_1 = array_reverse( $new_sidebars_widgets['sidebar-1'] ); + + $post_id = $this->factory()->post->create( array( + 'post_type' => 'customize_changeset', + 'post_status' => 'draft', + 'post_name' => wp_generate_uuid4(), + 'post_content' => wp_json_encode( array( + 'sidebars_widgets[sidebar-1]' => array( + 'value' => $new_sidebar_1, + ), + ) ), + ) ); + + // Save the updated sidebar widgets into the options table by publishing the changeset. + wp_publish_post( $post_id ); + + // Make sure previewing filters were never added, since WP_Customize_Manager should be constructed with settings_previewed=false. + $this->assertFalse( has_filter( 'option_sidebars_widgets' ) ); + $this->assertFalse( has_filter( 'default_option_sidebars_widgets' ) ); + + // Ensure that the value has actually been written to the DB. + $updated_sidebars_widgets = get_option( 'sidebars_widgets' ); + $this->assertEquals( $new_sidebar_1, $updated_sidebars_widgets['sidebar-1'] ); + } + /** * Ensure that save_changeset_post method bails updating an underlying changeset which is invalid. *