From 89f49aad8026de2aff2f026fb93f84097d78b0c0 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 21 Nov 2015 02:51:57 +0000 Subject: [PATCH] Customize: Ensure that a setting (especially a multidimensional one) can still be previewed when the post value to preview is set after `preview()` is invoked. * Introduce `customize_post_value_set_{$setting_id}` and `customize_post_value_set` actions which are done when `WP_Customize_Manager::set_post_value()` is called. * Clear the `preview_applied` flag for aggregated multidimensional settings when a post value is set. This ensures the new value is used instead of a previously-cached previewed value. * Move `$is_preview` property from subclasses to `WP_Customize_Setting` parent class. * Deferred preview: Ensure that when `preview()` short-circuits due to not being applicable that it will be called again later when the post value is set. * Populate post value for updated-widget with the (unsanitized) JS-value in `WP_Customize_Widgets::call_widget_update()` so that value will be properly sanitized when accessed in `WP_Customize_Manager::post_value()`. Includes unit tests with assertions to check the reported issues and validate the fixes. Fixes defect introduced in [35007]. See #32103. Fixes #34738. git-svn-id: https://develop.svn.wordpress.org/trunk@35724 602fd350-edb4-49c9-b593-d223f7449a82 --- .../class-wp-customize-manager.php | 30 +++++ .../class-wp-customize-setting.php | 44 +++++- .../class-wp-customize-widgets.php | 2 +- ...ass-wp-customize-nav-menu-item-setting.php | 9 -- .../class-wp-customize-nav-menu-setting.php | 9 -- tests/phpunit/tests/customize/manager.php | 74 ++++++++-- tests/phpunit/tests/customize/setting.php | 127 +++++++++++------- tests/phpunit/tests/customize/widgets.php | 53 ++++++++ 8 files changed, 270 insertions(+), 78 deletions(-) diff --git a/src/wp-includes/class-wp-customize-manager.php b/src/wp-includes/class-wp-customize-manager.php index c3b1f32509..9f15d31f57 100644 --- a/src/wp-includes/class-wp-customize-manager.php +++ b/src/wp-includes/class-wp-customize-manager.php @@ -659,6 +659,36 @@ final class WP_Customize_Manager { public function set_post_value( $setting_id, $value ) { $this->unsanitized_post_values(); $this->_post_values[ $setting_id ] = $value; + + /** + * Announce when a specific setting's unsanitized post value has been set. + * + * Fires when the {@see WP_Customize_Manager::set_post_value()} method is called. + * + * The dynamic portion of the hook name, `$setting_id`, refers to the setting ID. + * + * @since 4.4.0 + * + * @param mixed $value Unsanitized setting post value. + * @param WP_Customize_Manager $this WP_Customize_Manager instance. + */ + do_action( "customize_post_value_set_{$setting_id}", $value, $this ); + + /** + * Announce when any setting's unsanitized post value has been set. + * + * Fires when the {@see WP_Customize_Manager::set_post_value()} method is called. + * + * This is useful for WP_Customize_Setting instances to watch + * in order to update a cached previewed value. + * + * @since 4.4.0 + * + * @param string $setting_id Setting ID. + * @param mixed $value Unsanitized setting post value. + * @param WP_Customize_Manager $this WP_Customize_Manager instance. + */ + do_action( 'customize_post_value_set', $setting_id, $value, $this ); } /** diff --git a/src/wp-includes/class-wp-customize-setting.php b/src/wp-includes/class-wp-customize-setting.php index 12f76d4309..434dec7c06 100644 --- a/src/wp-includes/class-wp-customize-setting.php +++ b/src/wp-includes/class-wp-customize-setting.php @@ -81,6 +81,15 @@ class WP_Customize_Setting { */ protected $id_data = array(); + /** + * Whether or not preview() was called. + * + * @since 4.4.0 + * @access protected + * @var bool + */ + protected $is_previewed = false; + /** * Cache of multidimensional values to improve performance. * @@ -191,6 +200,8 @@ class WP_Customize_Setting { } if ( ! empty( $this->id_data['keys'] ) ) { + // Note the preview-applied flag is cleared at priority 9 to ensure it is cleared before a deferred-preview runs. + add_action( "customize_post_value_set_{$this->id}", array( $this, '_clear_aggregated_multidimensional_preview_applied_flag' ), 9 ); $this->is_multidimensional_aggregated = true; } } @@ -245,6 +256,12 @@ class WP_Customize_Setting { if ( ! isset( $this->_previewed_blog_id ) ) { $this->_previewed_blog_id = get_current_blog_id(); } + + // Prevent re-previewing an already-previewed setting. + if ( $this->is_previewed ) { + return true; + } + $id_base = $this->id_data['base']; $is_multidimensional = ! empty( $this->id_data['keys'] ); $multidimensional_filter = array( $this, '_multidimensional_preview_filter' ); @@ -273,7 +290,11 @@ class WP_Customize_Setting { $needs_preview = ( $undefined === $value ); // Because the default needs to be supplied. } + // If the setting does not need previewing now, defer to when it has a value to preview. if ( ! $needs_preview ) { + if ( ! has_action( "customize_post_value_set_{$this->id}", array( $this, 'preview' ) ) ) { + add_action( "customize_post_value_set_{$this->id}", array( $this, 'preview' ) ); + } return false; } @@ -327,9 +348,28 @@ class WP_Customize_Setting { */ do_action( "customize_preview_{$this->type}", $this ); } + + $this->is_previewed = true; + return true; } + /** + * Clear out the previewed-applied flag for a multidimensional-aggregated value whenever its post value is updated. + * + * This ensures that the new value will get sanitized and used the next time + * that WP_Customize_Setting::_multidimensional_preview_filter() + * is called for this setting. + * + * @since 4.4.0 + * @access private + * @see WP_Customize_Manager::set_post_value() + * @see WP_Customize_Setting::_multidimensional_preview_filter() + */ + final public function _clear_aggregated_multidimensional_preview_applied_flag() { + unset( self::$aggregated_multidimensionals[ $this->type ][ $this->id_data['base'] ]['preview_applied_instances'][ $this->id ] ); + } + /** * Callback function to filter non-multidimensional theme mods and options. * @@ -369,13 +409,13 @@ class WP_Customize_Setting { * the first setting previewed will be used to apply the values for the others. * * @since 4.4.0 - * @access public + * @access private * * @see WP_Customize_Setting::$aggregated_multidimensionals * @param mixed $original Original root value. * @return mixed New or old value. */ - public function _multidimensional_preview_filter( $original ) { + final public function _multidimensional_preview_filter( $original ) { if ( ! $this->is_current_blog_previewed() ) { return $original; } diff --git a/src/wp-includes/class-wp-customize-widgets.php b/src/wp-includes/class-wp-customize-widgets.php index 6ee69421cd..7639d5091c 100644 --- a/src/wp-includes/class-wp-customize-widgets.php +++ b/src/wp-includes/class-wp-customize-widgets.php @@ -1380,7 +1380,7 @@ final class WP_Customize_Widgets { * in place from WP_Customize_Setting::preview() will use this value * instead of the default widget instance value (an empty array). */ - $this->manager->set_post_value( $setting_id, $instance ); + $this->manager->set_post_value( $setting_id, $this->sanitize_widget_js_instance( $instance ) ); // Obtain the widget control with the updated instance in place. ob_start(); diff --git a/src/wp-includes/customize/class-wp-customize-nav-menu-item-setting.php b/src/wp-includes/customize/class-wp-customize-nav-menu-item-setting.php index 2fa0b5c0d2..073423ecbe 100644 --- a/src/wp-includes/customize/class-wp-customize-nav-menu-item-setting.php +++ b/src/wp-includes/customize/class-wp-customize-nav-menu-item-setting.php @@ -119,15 +119,6 @@ class WP_Customize_Nav_Menu_Item_Setting extends WP_Customize_Setting { */ public $original_nav_menu_term_id; - /** - * Whether or not preview() was called. - * - * @since 4.3.0 - * @access protected - * @var bool - */ - protected $is_previewed = false; - /** * Whether or not update() was called. * diff --git a/src/wp-includes/customize/class-wp-customize-nav-menu-setting.php b/src/wp-includes/customize/class-wp-customize-nav-menu-setting.php index 766099e06b..5562a8df52 100644 --- a/src/wp-includes/customize/class-wp-customize-nav-menu-setting.php +++ b/src/wp-includes/customize/class-wp-customize-nav-menu-setting.php @@ -88,15 +88,6 @@ class WP_Customize_Nav_Menu_Setting extends WP_Customize_Setting { */ public $previous_term_id; - /** - * Whether or not preview() was called. - * - * @since 4.3.0 - * @access protected - * @var bool - */ - protected $is_previewed = false; - /** * Whether or not update() was called. * diff --git a/tests/phpunit/tests/customize/manager.php b/tests/phpunit/tests/customize/manager.php index 481fe61dde..f9034f2d44 100644 --- a/tests/phpunit/tests/customize/manager.php +++ b/tests/phpunit/tests/customize/manager.php @@ -32,8 +32,7 @@ class Tests_WP_Customize_Manager extends WP_UnitTestCase { function setUp() { parent::setUp(); require_once( ABSPATH . WPINC . '/class-wp-customize-manager.php' ); - $GLOBALS['wp_customize'] = new WP_Customize_Manager(); - $this->manager = $GLOBALS['wp_customize']; + $this->manager = $this->instantiate(); $this->undefined = new stdClass(); } @@ -66,7 +65,7 @@ class Tests_WP_Customize_Manager extends WP_UnitTestCase { define( 'DOING_AJAX', true ); } - $manager = $this->instantiate(); + $manager = $this->manager; $this->assertTrue( $manager->doing_ajax() ); $_REQUEST['action'] = 'customize_save'; @@ -82,7 +81,7 @@ class Tests_WP_Customize_Manager extends WP_UnitTestCase { $this->markTestSkipped( 'Cannot test when DOING_AJAX' ); } - $manager = $this->instantiate(); + $manager = $this->manager; $this->assertFalse( $manager->doing_ajax() ); } @@ -92,7 +91,7 @@ class Tests_WP_Customize_Manager extends WP_UnitTestCase { * @ticket 30988 */ function test_unsanitized_post_values() { - $manager = $this->instantiate(); + $manager = $this->manager; $customized = array( 'foo' => 'bar', @@ -114,7 +113,7 @@ class Tests_WP_Customize_Manager extends WP_UnitTestCase { ); $_POST['customized'] = wp_slash( wp_json_encode( $posted_settings ) ); - $manager = $this->instantiate(); + $manager = $this->manager; $manager->add_setting( 'foo', array( 'default' => 'foo_default' ) ); $foo_setting = $manager->get_setting( 'foo' ); @@ -126,13 +125,72 @@ class Tests_WP_Customize_Manager extends WP_UnitTestCase { $this->assertEquals( 'post_value_bar_default', $manager->post_value( $bar_setting, 'post_value_bar_default' ), 'Expected post_value($bar_setting, $default) to return $default since no value supplied in $_POST[customized][bar]' ); } + /** + * Test WP_Customize_Manager::set_post_value(). + * + * @see WP_Customize_Manager::set_post_value() + */ + function test_set_post_value() { + $this->manager->add_setting( 'foo', array( + 'sanitize_callback' => array( $this, 'sanitize_foo_for_test_set_post_value' ), + ) ); + $setting = $this->manager->get_setting( 'foo' ); + + $this->assertEmpty( $this->captured_customize_post_value_set_actions ); + add_action( 'customize_post_value_set', array( $this, 'capture_customize_post_value_set_actions' ), 10, 3 ); + add_action( 'customize_post_value_set_foo', array( $this, 'capture_customize_post_value_set_actions' ), 10, 2 ); + $this->manager->set_post_value( $setting->id, '123abc' ); + $this->assertCount( 2, $this->captured_customize_post_value_set_actions ); + $this->assertEquals( 'customize_post_value_set_foo', $this->captured_customize_post_value_set_actions[0]['action'] ); + $this->assertEquals( 'customize_post_value_set', $this->captured_customize_post_value_set_actions[1]['action'] ); + $this->assertEquals( array( '123abc', $this->manager ), $this->captured_customize_post_value_set_actions[0]['args'] ); + $this->assertEquals( array( $setting->id, '123abc', $this->manager ), $this->captured_customize_post_value_set_actions[1]['args'] ); + + $unsanitized = $this->manager->unsanitized_post_values(); + $this->assertArrayHasKey( $setting->id, $unsanitized ); + + $this->assertEquals( '123abc', $unsanitized[ $setting->id ] ); + $this->assertEquals( 123, $setting->post_value() ); + } + + /** + * Sanitize a value for Tests_WP_Customize_Manager::test_set_post_value(). + * + * @see Tests_WP_Customize_Manager::test_set_post_value() + * + * @param mixed $value Value. + * @return int Value. + */ + function sanitize_foo_for_test_set_post_value( $value ) { + return intval( $value ); + } + + /** + * Store data coming from customize_post_value_set action calls. + * + * @see Tests_WP_Customize_Manager::capture_customize_post_value_set_actions() + * @var array + */ + protected $captured_customize_post_value_set_actions = array(); + + /** + * Capture the actions fired when calling WP_Customize_Manager::set_post_value(). + * + * @see Tests_WP_Customize_Manager::test_set_post_value() + */ + function capture_customize_post_value_set_actions() { + $action = current_action(); + $args = func_get_args(); + $this->captured_customize_post_value_set_actions[] = compact( 'action', 'args' ); + } + /** * Test the WP_Customize_Manager::add_dynamic_settings() method. * * @ticket 30936 */ function test_add_dynamic_settings() { - $manager = $this->instantiate(); + $manager = $this->manager; $setting_ids = array( 'foo', 'bar' ); $manager->add_setting( 'foo', array( 'default' => 'foo_default' ) ); $this->assertEmpty( $manager->get_setting( 'bar' ), 'Expected there to not be a bar setting up front.' ); @@ -162,7 +220,7 @@ class Tests_WP_Customize_Manager extends WP_UnitTestCase { add_action( 'customize_register', array( $this, 'action_customize_register_for_dynamic_settings' ) ); - $manager = $this->instantiate(); + $manager = $this->manager; $manager->add_setting( 'foo', array( 'default' => 'foo_default' ) ); $this->assertEmpty( $manager->get_setting( 'bar' ), 'Expected dynamic setting "bar" to not be registered.' ); diff --git a/tests/phpunit/tests/customize/setting.php b/tests/phpunit/tests/customize/setting.php index da789b12d1..6d46f3be56 100644 --- a/tests/phpunit/tests/customize/setting.php +++ b/tests/phpunit/tests/customize/setting.php @@ -94,9 +94,9 @@ class Tests_WP_Customize_Setting extends WP_UnitTestCase { function test_preview_standard_types_non_multidimensional() { $_POST['customized'] = wp_slash( wp_json_encode( $this->post_data_overrides ) ); - // Try non-multidimensional settings + // Try non-multidimensional settings. foreach ( $this->standard_type_configs as $type => $type_options ) { - // Non-multidimensional: See what effect the preview filter has on a non-existent setting (default value should be seen) + // Non-multidimensional: See what effect the preview filter has on a non-existent setting (default value should be seen). $name = "unset_{$type}_without_post_value"; $default = "default_value_{$name}"; $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) ); @@ -106,7 +106,7 @@ class Tests_WP_Customize_Setting extends WP_UnitTestCase { $this->assertEquals( $default, call_user_func( $type_options['getter'], $name, $this->undefined ), sprintf( 'Expected %s(%s) to return setting default: %s.', $type_options['getter'], $name, $default ) ); $this->assertEquals( $default, $setting->value() ); - // Non-multidimensional: See what effect the preview has on an extant setting (default value should not be seen) + // Non-multidimensional: See what effect the preview has on an extant setting (default value should not be seen). $name = "set_{$type}_without_post_value"; $default = "default_value_{$name}"; $initial_value = "initial_value_{$name}"; @@ -115,11 +115,12 @@ class Tests_WP_Customize_Setting extends WP_UnitTestCase { $this->assertEquals( $initial_value, call_user_func( $type_options['getter'], $name ) ); $this->assertEquals( $initial_value, $setting->value() ); $this->assertFalse( $setting->preview(), 'Preview should no-op since setting value was extant and no post value was present.' ); - $this->assertEquals( 0, did_action( "customize_preview_{$setting->id}" ) ); // only applicable for custom types (not options or theme_mods) - $this->assertEquals( 0, did_action( "customize_preview_{$setting->type}" ) ); // only applicable for custom types (not options or theme_mods) + $this->assertEquals( 0, did_action( "customize_preview_{$setting->id}" ) ); // Only applicable for custom types (not options or theme_mods). + $this->assertEquals( 0, did_action( "customize_preview_{$setting->type}" ) ); // Only applicable for custom types (not options or theme_mods). $this->assertEquals( $initial_value, call_user_func( $type_options['getter'], $name ) ); $this->assertEquals( $initial_value, $setting->value() ); + // Non-multidimensional: Try updating a value that had a no-op preview. $overridden_value = "overridden_value_$name"; call_user_func( $type_options['setter'], $name, $overridden_value ); $message = 'Initial value should be overridden because initial preview() was no-op due to setting having existing value and/or post value was absent.'; @@ -127,17 +128,26 @@ class Tests_WP_Customize_Setting extends WP_UnitTestCase { $this->assertEquals( $overridden_value, $setting->value(), $message ); $this->assertNotEquals( $initial_value, $setting->value(), $message ); - // Non-multidimensional: Test unset setting being overridden by a post value + // Non-multidimensional: Ensure that setting a post value *after* preview() is called results in the post value being seen (deferred preview). + $post_value = "post_value_for_{$setting->id}_set_after_preview_called"; + $this->assertEquals( 0, did_action( "customize_post_value_set_{$setting->id}" ) ); + $this->manager->set_post_value( $setting->id, $post_value ); + $this->assertEquals( 1, did_action( "customize_post_value_set_{$setting->id}" ) ); + $this->assertNotEquals( $overridden_value, $setting->value() ); + $this->assertEquals( $post_value, call_user_func( $type_options['getter'], $name ) ); + $this->assertEquals( $post_value, $setting->value() ); + + // Non-multidimensional: Test unset setting being overridden by a post value. $name = "unset_{$type}_overridden"; $default = "default_value_{$name}"; $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) ); $this->assertEquals( $this->undefined, call_user_func( $type_options['getter'], $name, $this->undefined ) ); $this->assertEquals( $default, $setting->value() ); - $this->assertTrue( $setting->preview(), 'Preview applies because setting has post_data_overrides.' ); // activate post_data + $this->assertTrue( $setting->preview(), 'Preview applies because setting has post_data_overrides.' ); // Activate post_data. $this->assertEquals( $this->post_data_overrides[ $name ], call_user_func( $type_options['getter'], $name, $this->undefined ) ); $this->assertEquals( $this->post_data_overrides[ $name ], $setting->value() ); - // Non-multidimensional: Test set setting being overridden by a post value + // Non-multidimensional: Test set setting being overridden by a post value. $name = "set_{$type}_overridden"; $default = "default_value_{$name}"; $initial_value = "initial_value_{$name}"; @@ -145,9 +155,9 @@ class Tests_WP_Customize_Setting extends WP_UnitTestCase { $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) ); $this->assertEquals( $initial_value, call_user_func( $type_options['getter'], $name, $this->undefined ) ); $this->assertEquals( $initial_value, $setting->value() ); - $this->assertTrue( $setting->preview(), 'Preview applies because setting has post_data_overrides.' ); // activate post_data - $this->assertEquals( 0, did_action( "customize_preview_{$setting->id}" ) ); // only applicable for custom types (not options or theme_mods) - $this->assertEquals( 0, did_action( "customize_preview_{$setting->type}" ) ); // only applicable for custom types (not options or theme_mods) + $this->assertTrue( $setting->preview(), 'Preview applies because setting has post_data_overrides.' ); // Activate post_data. + $this->assertEquals( 0, did_action( "customize_preview_{$setting->id}" ) ); // Only applicable for custom types (not options or theme_mods). + $this->assertEquals( 0, did_action( "customize_preview_{$setting->type}" ) ); // Only applicable for custom types (not options or theme_mods). $this->assertEquals( $this->post_data_overrides[ $name ], call_user_func( $type_options['getter'], $name, $this->undefined ) ); $this->assertEquals( $this->post_data_overrides[ $name ], $setting->value() ); } @@ -155,24 +165,26 @@ class Tests_WP_Customize_Setting extends WP_UnitTestCase { /** * Run assertions on multidimensional standard settings. + * + * @see WP_Customize_Setting::preview() */ function test_preview_standard_types_multidimensional() { $_POST['customized'] = wp_slash( wp_json_encode( $this->post_data_overrides ) ); foreach ( $this->standard_type_configs as $type => $type_options ) { - // Multidimensional: See what effect the preview filter has on a non-existent setting (default value should be seen) + // Multidimensional: See what effect the preview filter has on a non-existent setting (default value should be seen). $base_name = "unset_{$type}_multi"; $name = $base_name . '[foo]'; $default = "default_value_{$name}"; $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) ); $this->assertEquals( $this->undefined, call_user_func( $type_options['getter'], $base_name, $this->undefined ) ); $this->assertEquals( $default, $setting->value() ); - $this->assertTrue( $setting->preview() ); + $this->assertTrue( $setting->preview(), "Preview for $setting->id should apply because setting is not in DB." ); $base_value = call_user_func( $type_options['getter'], $base_name, $this->undefined ); $this->assertArrayHasKey( 'foo', $base_value ); $this->assertEquals( $default, $base_value['foo'] ); - // Multidimensional: See what effect the preview has on an extant setting (default value should not be seen) + // Multidimensional: See what effect the preview has on an extant setting (default value should not be seen) without post value. $base_name = "set_{$type}_multi"; $name = $base_name . '[foo]'; $default = "default_value_{$name}"; @@ -183,28 +195,35 @@ class Tests_WP_Customize_Setting extends WP_UnitTestCase { $base_value = call_user_func( $type_options['getter'], $base_name, array() ); $this->assertEquals( $initial_value, $base_value['foo'] ); $this->assertEquals( $initial_value, $setting->value() ); - $setting->preview(); - $this->assertEquals( 0, did_action( "customize_preview_{$setting->id}" ) ); // only applicable for custom types (not options or theme_mods) - $this->assertEquals( 0, did_action( "customize_preview_{$setting->type}" ) ); // only applicable for custom types (not options or theme_mods) + $this->assertFalse( $setting->preview(), "Preview for $setting->id should no-op because setting is in DB and post value is absent." ); + $this->assertEquals( 0, did_action( "customize_preview_{$setting->id}" ) ); // Only applicable for custom types (not options or theme_mods). + $this->assertEquals( 0, did_action( "customize_preview_{$setting->type}" ) ); // Only applicable for custom types (not options or theme_mods). $base_value = call_user_func( $type_options['getter'], $base_name, array() ); $this->assertEquals( $initial_value, $base_value['foo'] ); $this->assertEquals( $initial_value, $setting->value() ); - // Multidimensional: Test unset setting being overridden by a post value + // Multidimensional: Ensure that setting a post value *after* preview() is called results in the post value being seen (deferred preview). + $override_value = "post_value_for_{$setting->id}_set_after_preview_called"; + $this->manager->set_post_value( $setting->id, $override_value ); + $base_value = call_user_func( $type_options['getter'], $base_name, array() ); + $this->assertEquals( $override_value, $base_value['foo'] ); + $this->assertEquals( $override_value, $setting->value() ); + + // Multidimensional: Test unset setting being overridden by a post value. $base_name = "unset_{$type}_multi_overridden"; $name = $base_name . '[foo]'; $default = "default_value_{$name}"; $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) ); $this->assertEquals( $this->undefined, call_user_func( $type_options['getter'], $base_name, $this->undefined ) ); $this->assertEquals( $default, $setting->value() ); - $setting->preview(); - $this->assertEquals( 0, did_action( "customize_preview_{$setting->id}" ) ); // only applicable for custom types (not options or theme_mods) - $this->assertEquals( 0, did_action( "customize_preview_{$setting->type}" ) ); // only applicable for custom types (not options or theme_mods) + $this->assertTrue( $setting->preview(), "Preview for $setting->id should apply because a post value is present." ); + $this->assertEquals( 0, did_action( "customize_preview_{$setting->id}" ) ); // Only applicable for custom types (not options or theme_mods). + $this->assertEquals( 0, did_action( "customize_preview_{$setting->type}" ) ); // Only applicable for custom types (not options or theme_mods). $base_value = call_user_func( $type_options['getter'], $base_name, $this->undefined ); $this->assertArrayHasKey( 'foo', $base_value ); $this->assertEquals( $this->post_data_overrides[ $name ], $base_value['foo'] ); - // Multidimemsional: Test set setting being overridden by a post value + // Multidimensional: Test set setting being overridden by a post value. $base_name = "set_{$type}_multi_overridden"; $name = $base_name . '[foo]'; $default = "default_value_{$name}"; @@ -213,20 +232,20 @@ class Tests_WP_Customize_Setting extends WP_UnitTestCase { call_user_func( $type_options['setter'], $base_name, $base_initial_value ); $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) ); $base_value = call_user_func( $type_options['getter'], $base_name, $this->undefined ); - $this->arrayHasKey( 'foo', $base_value ); - $this->arrayHasKey( 'bar', $base_value ); + $this->assertArrayHasKey( 'foo', $base_value ); + $this->assertArrayHasKey( 'bar', $base_value ); $this->assertEquals( $base_initial_value['foo'], $base_value['foo'] ); $getter = call_user_func( $type_options['getter'], $base_name, $this->undefined ); $this->assertEquals( $base_initial_value['bar'], $getter['bar'] ); $this->assertEquals( $initial_value, $setting->value() ); - $setting->preview(); - $this->assertEquals( 0, did_action( "customize_preview_{$setting->id}" ) ); // only applicable for custom types (not options or theme_mods) - $this->assertEquals( 0, did_action( "customize_preview_{$setting->type}" ) ); // only applicable for custom types (not options or theme_mods) + $this->assertTrue( $setting->preview(), "Preview for $setting->id should apply because post value is present." ); + $this->assertEquals( 0, did_action( "customize_preview_{$setting->id}" ) ); // Only applicable for custom types (not options or theme_mods). + $this->assertEquals( 0, did_action( "customize_preview_{$setting->type}" ) ); // Only applicable for custom types (not options or theme_mods). $base_value = call_user_func( $type_options['getter'], $base_name, $this->undefined ); $this->assertArrayHasKey( 'foo', $base_value ); $this->assertEquals( $this->post_data_overrides[ $name ], $base_value['foo'] ); - $this->arrayHasKey( 'bar', call_user_func( $type_options['getter'], $base_name, $this->undefined ) ); + $this->assertArrayHasKey( 'bar', call_user_func( $type_options['getter'], $base_name, $this->undefined ) ); $getter = call_user_func( $type_options['getter'], $base_name, $this->undefined ); $this->assertEquals( $base_initial_value['bar'], $getter['bar'] ); @@ -272,7 +291,11 @@ class Tests_WP_Customize_Setting extends WP_UnitTestCase { $this->custom_type_data_previewed[ $setting->id ] = $previewed_value; } } - + /** + * Run assertions on custom settings. + * + * @see WP_Customize_Setting::preview() + */ function test_preview_custom_type() { $type = 'custom_type'; $post_data_overrides = array( @@ -286,63 +309,69 @@ class Tests_WP_Customize_Setting extends WP_UnitTestCase { add_action( "customize_preview_{$type}", array( $this, 'custom_type_preview' ) ); - // Custom type not existing and no post value override + // Custom type not existing and no post value override. $name = "unset_{$type}_without_post_value"; $default = "default_value_{$name}"; $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) ); - // Note: #29316 will allow us to have one filter for all settings of a given type, which is what we need + // Note: #29316 will allow us to have one filter for all settings of a given type, which is what we need. add_filter( "customize_value_{$name}", array( $this, 'custom_type_value_filter' ) ); $this->assertEquals( $this->undefined, $this->custom_type_getter( $name, $this->undefined ) ); $this->assertEquals( $default, $setting->value() ); - $setting->preview(); + $this->assertTrue( $setting->preview() ); $this->assertEquals( 1, did_action( "customize_preview_{$setting->id}" ) ); $this->assertEquals( 1, did_action( "customize_preview_{$setting->type}" ) ); $this->assertEquals( $this->undefined, $this->custom_type_getter( $name, $this->undefined ) ); // Note: for a non-custom type this is $default - $this->assertEquals( $default, $setting->value() ); // should be same as above + $this->assertEquals( $default, $setting->value() ); // Should be same as above. - // Custom type existing and no post value override + // Custom type existing and no post value override. $name = "set_{$type}_without_post_value"; $default = "default_value_{$name}"; $initial_value = "initial_value_{$name}"; $this->custom_type_setter( $name, $initial_value ); $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) ); - // Note: #29316 will allow us to have one filter for all settings of a given type, which is what we need + // Note: #29316 will allow us to have one filter for all settings of a given type, which is what we need. add_filter( "customize_value_{$name}", array( $this, 'custom_type_value_filter' ) ); $this->assertEquals( $initial_value, $this->custom_type_getter( $name, $this->undefined ) ); $this->assertEquals( $initial_value, $setting->value() ); - $setting->preview(); + $this->assertFalse( $setting->preview(), "Preview for $setting->id should not apply because existing type without an override." ); $this->assertEquals( 0, did_action( "customize_preview_{$setting->id}" ), 'Zero preview actions because initial value is set with no incoming post value, so there is no preview to apply.' ); $this->assertEquals( 1, did_action( "customize_preview_{$setting->type}" ) ); - $this->assertEquals( $initial_value, $this->custom_type_getter( $name, $this->undefined ) ); // should be same as above - $this->assertEquals( $initial_value, $setting->value() ); // should be same as above + $this->assertEquals( $initial_value, $this->custom_type_getter( $name, $this->undefined ) ); // Should be same as above. + $this->assertEquals( $initial_value, $setting->value() ); // Should be same as above. - // Custom type not existing and with a post value override + // Custom type deferred preview (setting post value after preview ran). + $override_value = "custom_type_value_{$name}_override_deferred_preview"; + $this->manager->set_post_value( $setting->id, $override_value ); + $this->assertEquals( $override_value, $this->custom_type_getter( $name, $this->undefined ) ); // Should be same as above. + $this->assertEquals( $override_value, $setting->value() ); // Should be same as above. + + // Custom type not existing and with a post value override. $name = "unset_{$type}_with_post_value"; $default = "default_value_{$name}"; $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) ); - // Note: #29316 will allow us to have one filter for all settings of a given type, which is what we need + // Note: #29316 will allow us to have one filter for all settings of a given type, which is what we need. add_filter( "customize_value_{$name}", array( $this, 'custom_type_value_filter' ) ); $this->assertEquals( $this->undefined, $this->custom_type_getter( $name, $this->undefined ) ); $this->assertEquals( $default, $setting->value() ); - $setting->preview(); + $this->assertTrue( $setting->preview() ); $this->assertEquals( 1, did_action( "customize_preview_{$setting->id}" ), 'One preview action now because initial value was not set and/or there is no incoming post value, so there is is a preview to apply.' ); - $this->assertEquals( 2, did_action( "customize_preview_{$setting->type}" ) ); + $this->assertEquals( 3, did_action( "customize_preview_{$setting->type}" ) ); $this->assertEquals( $post_data_overrides[ $name ], $this->custom_type_getter( $name, $this->undefined ) ); $this->assertEquals( $post_data_overrides[ $name ], $setting->value() ); - // Custom type not existing and with a post value override + // Custom type not existing and with a post value override. $name = "set_{$type}_with_post_value"; $default = "default_value_{$name}"; $initial_value = "initial_value_{$name}"; $this->custom_type_setter( $name, $initial_value ); $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) ); - // Note: #29316 will allow us to have one filter for all settings of a given type, which is what we need + // Note: #29316 will allow us to have one filter for all settings of a given type, which is what we need. add_filter( "customize_value_{$name}", array( $this, 'custom_type_value_filter' ) ); $this->assertEquals( $initial_value, $this->custom_type_getter( $name, $this->undefined ) ); $this->assertEquals( $initial_value, $setting->value() ); - $setting->preview(); + $this->assertTrue( $setting->preview() ); $this->assertEquals( 1, did_action( "customize_preview_{$setting->id}" ) ); - $this->assertEquals( 3, did_action( "customize_preview_{$setting->type}" ) ); + $this->assertEquals( 4, did_action( "customize_preview_{$setting->type}" ) ); $this->assertEquals( $post_data_overrides[ $name ], $this->custom_type_getter( $name, $this->undefined ) ); $this->assertEquals( $post_data_overrides[ $name ], $setting->value() ); @@ -361,7 +390,7 @@ class Tests_WP_Customize_Setting extends WP_UnitTestCase { $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) ); $this->assertEquals( $this->undefined, get_option( $name, $this->undefined ) ); $this->assertEquals( $default, $setting->value() ); - $setting->preview(); + $this->assertTrue( $setting->preview() ); $this->assertEquals( $default, get_option( $name, $this->undefined ), sprintf( 'Expected get_option(%s) to return setting default: %s.', $name, $default ) ); $this->assertEquals( $default, $setting->value() ); } @@ -438,7 +467,7 @@ class Tests_WP_Customize_Setting extends WP_UnitTestCase { $this->manager->set_post_value( $name, $post_value ); $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type' ) ); $this->assertFalse( $setting->is_current_blog_previewed() ); - $setting->preview(); + $this->assertTrue( $setting->preview() ); $this->assertTrue( $setting->is_current_blog_previewed() ); $this->assertEquals( $post_value, $setting->value() ); @@ -462,7 +491,7 @@ class Tests_WP_Customize_Setting extends WP_UnitTestCase { $this->manager->set_post_value( $name, $post_value ); $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type' ) ); $this->assertFalse( $setting->is_current_blog_previewed() ); - $setting->preview(); + $this->assertTrue( $setting->preview() ); $this->assertTrue( $setting->is_current_blog_previewed() ); $blog_id = self::factory()->blog->create(); diff --git a/tests/phpunit/tests/customize/widgets.php b/tests/phpunit/tests/customize/widgets.php index 65843412e3..1ba37bb4c9 100644 --- a/tests/phpunit/tests/customize/widgets.php +++ b/tests/phpunit/tests/customize/widgets.php @@ -291,4 +291,57 @@ class Tests_WP_Customize_Widgets extends WP_UnitTestCase { $this->assertFalse( $this->manager->widgets->is_panel_active() ); $this->assertFalse( $this->manager->get_panel( 'widgets' )->active() ); } + + /** + * @ticket 34738 + * @see WP_Customize_Widgets::call_widget_update() + */ + function test_call_widget_update() { + + $widget_number = 2; + $widget_id = "search-{$widget_number}"; + $setting_id = "widget_search[{$widget_number}]"; + $instance = array( + 'title' => 'Buscar', + ); + + $_POST = wp_slash( array( + 'action' => 'update-widget', + 'wp_customize' => 'on', + 'nonce' => wp_create_nonce( 'update-widget' ), + 'theme' => $this->manager->get_stylesheet(), + 'customized' => '{}', + 'widget-search' => array( + 2 => $instance, + ), + 'widget-id' => $widget_id, + 'id_base' => 'search', + 'widget-width' => '250', + 'widget-height' => '200', + 'widget_number' => strval( $widget_number ), + 'multi_number' => '', + 'add_new' => '', + ) ); + + $this->do_customize_boot_actions(); + + $this->assertArrayNotHasKey( $setting_id, $this->manager->unsanitized_post_values() ); + $result = $this->manager->widgets->call_widget_update( $widget_id ); + + $this->assertInternalType( 'array', $result ); + $this->assertArrayHasKey( 'instance', $result ); + $this->assertArrayHasKey( 'form', $result ); + $this->assertEquals( $instance, $result['instance'] ); + $this->assertContains( sprintf( 'value="%s"', esc_attr( $instance['title'] ) ), $result['form'] ); + + $post_values = $this->manager->unsanitized_post_values(); + $this->assertArrayHasKey( $setting_id, $post_values ); + $post_value = $post_values[ $setting_id ]; + $this->assertInternalType( 'array', $post_value ); + $this->assertArrayHasKey( 'title', $post_value ); + $this->assertArrayHasKey( 'encoded_serialized_instance', $post_value ); + $this->assertArrayHasKey( 'instance_hash_key', $post_value ); + $this->assertArrayHasKey( 'is_widget_customizer_js_value', $post_value ); + $this->assertEquals( $post_value, $this->manager->widgets->sanitize_widget_js_instance( $instance ) ); + } }