WordPress.org

Make WordPress Core

Ticket #30988: 30988.6.diff

File 30988.6.diff, 27.9 KB (added by westonruter, 6 years ago)

Fix phpDoc: https://github.com/xwp/wordpress-develop/commit/f5b19ccc7b4b94025593da3577daa8a870ce64e6

  • src/wp-includes/class-wp-customize-manager.php

    diff --git src/wp-includes/class-wp-customize-manager.php src/wp-includes/class-wp-customize-manager.php
    index b695271..4947d27 100644
    final class WP_Customize_Manager { 
    6363        protected $registered_control_types = array();
    6464
    6565        /**
    66          * $_POST values for Customize Settings.
     66         * Unsanitized values for Customize Settings parsed from $_POST['customized'].
    6767         *
    68          * @var array
     68         * @var array|false
    6969         */
    7070        private $_post_values;
    7171
    final class WP_Customize_Manager { 
    7575         * @since 3.4.0
    7676         */
    7777        public function __construct() {
    78                 require( ABSPATH . WPINC . '/class-wp-customize-setting.php' );
    79                 require( ABSPATH . WPINC . '/class-wp-customize-panel.php' );
    80                 require( ABSPATH . WPINC . '/class-wp-customize-section.php' );
    81                 require( ABSPATH . WPINC . '/class-wp-customize-control.php' );
    82                 require( ABSPATH . WPINC . '/class-wp-customize-widgets.php' );
     78                require_once( ABSPATH . WPINC . '/class-wp-customize-setting.php' );
     79                require_once( ABSPATH . WPINC . '/class-wp-customize-panel.php' );
     80                require_once( ABSPATH . WPINC . '/class-wp-customize-section.php' );
     81                require_once( ABSPATH . WPINC . '/class-wp-customize-control.php' );
     82                require_once( ABSPATH . WPINC . '/class-wp-customize-widgets.php' );
    8383
    8484                $this->widgets = new WP_Customize_Widgets( $this );
    8585
    final class WP_Customize_Manager { 
    399399        }
    400400
    401401        /**
    402          * Decode the $_POST['customized'] values for a specific Customize Setting.
     402         * Parse the incoming $_POST['customized'] JSON data and store the unsanitized
     403         * settings for subsequent post_value() lookups.
    403404         *
    404          * @since 3.4.0
     405         * @since 4.1.1
    405406         *
    406          * @param WP_Customize_Setting $setting A WP_Customize_Setting derived object
    407          * @return string $post_value Sanitized value
     407         * @return array
    408408         */
    409         public function post_value( $setting ) {
     409        public function unsanitized_post_values() {
    410410                if ( ! isset( $this->_post_values ) ) {
    411                         if ( isset( $_POST['customized'] ) )
     411                        if ( isset( $_POST['customized'] ) ) {
    412412                                $this->_post_values = json_decode( wp_unslash( $_POST['customized'] ), true );
    413                         else
     413                        }
     414                        if ( empty( $this->_post_values ) ) { // if not isset or of JSON error
    414415                                $this->_post_values = false;
     416                        }
     417                }
     418                if ( empty( $this->_post_values ) ) {
     419                        return array();
     420                } else {
     421                        return $this->_post_values;
    415422                }
     423        }
    416424
    417                 if ( isset( $this->_post_values[ $setting->id ] ) )
    418                         return $setting->sanitize( $this->_post_values[ $setting->id ] );
     425        /**
     426         * Return the sanitized value for a given setting from the request's POST data.
     427         *
     428         * @since 3.4.0
     429         *
     430         * @param WP_Customize_Setting $setting A WP_Customize_Setting derived object
     431         * @param mixed $default value returned $setting has no post value (added in 4.2.0).
     432         * @return string|mixed $post_value Sanitized value or the $default provided
     433         */
     434        public function post_value( $setting, $default = null ) {
     435                $post_values = $this->unsanitized_post_values();
     436                if ( array_key_exists( $setting->id, $post_values ) ) {
     437                        return $setting->sanitize( $post_values[ $setting->id ] );
     438                } else {
     439                        return $default;
     440                }
    419441        }
    420442
    421443        /**
  • src/wp-includes/class-wp-customize-setting.php

    diff --git src/wp-includes/class-wp-customize-setting.php src/wp-includes/class-wp-customize-setting.php
    index 7a7be45..adfea11 100644
    class WP_Customize_Setting { 
    100100                        add_filter( "customize_sanitize_js_{$this->id}", $this->sanitize_js_callback, 10, 2 );
    101101        }
    102102
     103        protected $_original_value;
     104
    103105        /**
    104106         * Handle previewing the setting.
    105107         *
    106108         * @since 3.4.0
    107109         */
    108110        public function preview() {
     111
     112                if ( ! isset( $this->_original_value ) ) {
     113                        $this->_original_value = $this->value();
     114                }
     115
    109116                switch( $this->type ) {
    110117                        case 'theme_mod' :
    111118                                add_filter( 'theme_mod_' . $this->id_data[ 'base' ], array( $this, '_preview_filter' ) );
    class WP_Customize_Setting { 
    156163         * @return mixed New or old value.
    157164         */
    158165        public function _preview_filter( $original ) {
    159                 return $this->multidimensional_replace( $original, $this->id_data[ 'keys' ], $this->post_value() );
     166                $undefined = new stdClass(); // symbol hack
     167                $post_value = $this->manager->post_value( $this, $undefined );
     168                if ( $undefined === $post_value ) {
     169                        $value = $this->_original_value;
     170                } else {
     171                        $value = $post_value;
     172                }
     173                $replaced = $this->multidimensional_replace( $original, $this->id_data['keys'], $value );
     174                return $replaced;
    160175        }
    161176
    162177        /**
    class WP_Customize_Setting { 
    422437                        $node = &$node[ $key ];
    423438                }
    424439
    425                 if ( $create && ! isset( $node[ $last ] ) )
    426                         $node[ $last ] = array();
     440                if ( $create ) {
     441                        if ( ! is_array( $node ) ) {
     442                                // account for an array overriding a string or object value
     443                                $node = array();
     444                        }
     445                        if ( ! isset( $node[ $last ] ) ) {
     446                                $node[ $last ] = array();
     447                        }
     448                }
    427449
    428450                if ( ! isset( $node[ $last ] ) )
    429451                        return;
  • new file tests/phpunit/tests/customize/manager.php

    diff --git tests/phpunit/tests/customize/manager.php tests/phpunit/tests/customize/manager.php
    new file mode 100644
    index 0000000..9a085b2
    - +  
     1<?php
     2
     3/**
     4 * Tests for the WP_Customize_Manager class.
     5 *
     6 * @group customize
     7 */
     8class Tests_WP_Customize_Manager extends WP_UnitTestCase {
     9
     10        function setUp() {
     11                parent::setUp();
     12                require_once( ABSPATH . WPINC . '/class-wp-customize-manager.php' );
     13                $GLOBALS['wp_customize'] = new WP_Customize_Manager(); // wpcs: override ok
     14                $this->manager = $GLOBALS['wp_customize'];
     15                $this->undefined = new stdClass();
     16        }
     17
     18        function tearDown() {
     19                parent::tearDown();
     20                $this->manager = null;
     21                unset( $GLOBALS['wp_customize'] );
     22        }
     23
     24        /**
     25         * Instantiate class, set global $wp_customize, and return instance.
     26         *
     27         * @return WP_Customize_Manager
     28         */
     29        function instantiate() {
     30                $GLOBALS['wp_customize'] = new WP_Customize_Manager(); // wpcs: override ok
     31                return $GLOBALS['wp_customize'];
     32        }
     33
     34        /**
     35         * Test WP_Customize_Manager::unsanitized_post_values()
     36         *
     37         * @ticket 30988
     38         */
     39        function test_unsanitized_post_values() {
     40                $manager = $this->instantiate();
     41
     42                $customized = array(
     43                        'foo' => 'bar',
     44                        'baz[quux]' => 123,
     45                );
     46                $_POST['customized'] = wp_slash( wp_json_encode( $customized ) );
     47                $post_values = $manager->unsanitized_post_values();
     48                $this->assertEquals( $customized, $post_values );
     49        }
     50
     51        /**
     52         * Test the WP_Customize_Manager::post_value() method
     53         *
     54         * @ticket 30936
     55         */
     56        function test_post_value() {
     57                $posted_settings = array(
     58                        'foo' => 'OOF',
     59                );
     60                $_POST['customized'] = wp_slash( wp_json_encode( $posted_settings ) );
     61
     62                $manager = $this->instantiate();
     63
     64                $manager->add_setting( 'foo', array( 'default' => 'foo_default' ) ); // @todo this should return the setting instance
     65                $foo_setting = $manager->get_setting( 'foo' );
     66                $this->assertEquals( 'foo_default', $manager->get_setting( 'foo' )->value(), 'Expected non-previewed setting to return default when value() method called.' );
     67                $this->assertEquals( $posted_settings['foo'], $manager->post_value( $foo_setting, 'post_value_foo_default' ), 'Expected post_value($foo_setting) to return value supplied in $_POST[customized][foo]' );
     68
     69                $manager->add_setting( 'bar', array( 'default' => 'bar_default' ) );
     70                $bar_setting = $manager->get_setting( 'bar' );
     71                $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]' );
     72        }
     73
     74}
     75
  • new file tests/phpunit/tests/customize/setting.php

    diff --git tests/phpunit/tests/customize/setting.php tests/phpunit/tests/customize/setting.php
    new file mode 100644
    index 0000000..23d7cfa
    - +  
     1<?php
     2
     3/**
     4 * Tests for the WP_Customize_Setting class.
     5 *
     6 * @group customize
     7 */
     8class Tests_WP_Customize_Setting extends WP_UnitTestCase {
     9
     10        /**
     11         * @var WP_Customize_Manager
     12         */
     13        protected $manager;
     14
     15        /**
     16         * @var stdClass an instance which serves as a symbol to do identity checks with
     17         */
     18        public $undefined;
     19
     20        function setUp() {
     21                parent::setUp();
     22                require_once( ABSPATH . WPINC . '/class-wp-customize-manager.php' );
     23                $GLOBALS['wp_customize'] = new WP_Customize_Manager(); // wpcs: override ok
     24                $this->manager = $GLOBALS['wp_customize'];
     25                $this->undefined = new stdClass();
     26        }
     27
     28        function tearDown() {
     29                parent::tearDown();
     30                $this->manager = null;
     31                unset( $GLOBALS['wp_customize'] );
     32        }
     33
     34        function test_construct() {
     35                $foo = new WP_Customize_Setting( $this->manager, 'foo' );
     36                $this->assertEquals( $this->manager, $foo->manager );
     37                $this->assertEquals( 'foo', $foo->id );
     38                $this->assertEquals( 'theme_mod', $foo->type );
     39                $this->assertEquals( 'edit_theme_options', $foo->capability );
     40                $this->assertEquals( '', $foo->theme_supports );
     41                $this->assertEquals( '', $foo->default );
     42                $this->assertEquals( 'refresh', $foo->transport );
     43                $this->assertEquals( '', $foo->sanitize_callback );
     44                $this->assertEquals( '', $foo->sanitize_js_callback );
     45                $this->assertFalse( has_filter( "customize_sanitize_{$foo->id}" ) );
     46                $this->assertFalse( has_filter( "customize_sanitize_js_{$foo->id}" ) );
     47
     48                $args = array(
     49                        'type' => 'option',
     50                        'capability' => 'edit_posts',
     51                        'theme_supports' => 'widgets',
     52                        'default' => 'barbar',
     53                        'transport' => 'postMessage',
     54                        'sanitize_callback' => create_function( '$value', 'return $value . ":sanitize_callback";' ),
     55                        'sanitize_js_callback' => create_function( '$value', 'return $value . ":sanitize_js_callback";' ),
     56                );
     57                $bar = new WP_Customize_Setting( $this->manager, 'bar', $args );
     58                $this->assertEquals( 'bar', $bar->id );
     59                foreach ( $args as $key => $value ) {
     60                        $this->assertEquals( $value, $bar->$key );
     61                }
     62                $this->assertEquals( 10, has_filter( "customize_sanitize_{$bar->id}", $args['sanitize_callback'] ) );
     63                $this->assertEquals( 10, has_filter( "customize_sanitize_js_{$bar->id}" ), $args['sanitize_js_callback'] );
     64        }
     65
     66        public $post_data_overrides = array(
     67                'unset_option_overridden' => 'unset_option_post_override_value',
     68                'unset_theme_mod_overridden' => 'unset_theme_mod_post_override_value',
     69                'set_option_overridden' => 'set_option_post_override_value',
     70                'set_theme_mod_overridden' => 'set_theme_mod_post_override_value',
     71                'unset_option_multi_overridden[foo]' => 'unset_option_multi_overridden[foo]_post_override_value',
     72                'unset_theme_mod_multi_overridden[foo]' => 'unset_theme_mod_multi_overridden[foo]_post_override_value',
     73                'set_option_multi_overridden[foo]' => 'set_option_multi_overridden[foo]_post_override_value',
     74                'set_theme_mod_multi_overridden[foo]' => 'set_theme_mod_multi_overridden[foo]_post_override_value',
     75        );
     76
     77        public $standard_type_configs = array(
     78                'option' => array(
     79                        'getter' => 'get_option',
     80                        'setter' => 'update_option',
     81                ),
     82                'theme_mod' => array(
     83                        'getter' => 'get_theme_mod',
     84                        'setter' => 'set_theme_mod',
     85                ),
     86        );
     87
     88        /**
     89         * Run assertions on non-multidimensional standard settings
     90         */
     91        function test_preview_standard_types_non_multidimensional() {
     92
     93                // @todo this is hacky. The manager should provide a mechanism to override the post_values
     94                $_POST['customized'] = wp_slash( wp_json_encode( $this->post_data_overrides ) );
     95
     96                // Try non-multidimensional settings
     97                foreach ( $this->standard_type_configs as $type => $type_options ) {
     98                        // Non-multidimensional: See what effect the preview filter has on a non-existent setting (default value should be seen)
     99                        $name = "unset_{$type}_without_post_value";
     100                        $default = "default_value_{$name}";
     101                        $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) );
     102                        $this->assertEquals( $this->undefined, call_user_func( $type_options['getter'], $name, $this->undefined ) );
     103                        $this->assertEquals( $default, $setting->value() );
     104                        $setting->preview();
     105                        $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 ) );
     106                        $this->assertEquals( $default, $setting->value() );
     107
     108                        // Non-multidimensional: See what effect the preview has on an extant setting (default value should not be seen)
     109                        $name = "set_{$type}_without_post_value";
     110                        $default = "default_value_{$name}";
     111                        $initial_value = "initial_value_{$name}";
     112                        call_user_func( $type_options['setter'], $name, $initial_value );
     113                        $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) );
     114                        $this->assertEquals( $initial_value, call_user_func( $type_options['getter'], $name ) );
     115                        $this->assertEquals( $initial_value, $setting->value() );
     116                        $setting->preview();
     117                        $this->assertEquals( 0, did_action( "customize_preview_{$setting->id}" ) ); // only applicable for custom types (not options or theme_mods)
     118                        $this->assertEquals( 0, did_action( "customize_preview_{$setting->type}" ) ); // only applicable for custom types (not options or theme_mods)
     119                        $this->assertEquals( $initial_value, call_user_func( $type_options['getter'], $name ) );
     120                        $this->assertEquals( $initial_value, $setting->value() );
     121
     122                        // @todo What if we call the setter after preview() is called? If no post_value, should the new set value be stored? If that happens, then the following 3 assertions should be inverted
     123                        $overridden_value = "overridden_value_$name";
     124                        call_user_func( $type_options['setter'], $name, $overridden_value );
     125                        $this->assertEquals( $initial_value, call_user_func( $type_options['getter'], $name ) );
     126                        $this->assertEquals( $initial_value, $setting->value() );
     127                        $this->assertNotEquals( $overridden_value, $setting->value() );
     128
     129                        // Non-multidimensional: Test unset setting being overridden by a post value
     130                        $name = "unset_{$type}_overridden";
     131                        $default = "default_value_{$name}";
     132                        $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) );
     133                        $this->assertEquals( $this->undefined, call_user_func( $type_options['getter'], $name, $this->undefined ) );
     134                        $this->assertEquals( $default, $setting->value() );
     135                        $setting->preview(); // activate post_data
     136                        $this->assertEquals( $this->post_data_overrides[ $name ], call_user_func( $type_options['getter'], $name, $this->undefined ) );
     137                        $this->assertEquals( $this->post_data_overrides[ $name ], $setting->value() );
     138
     139                        // Non-multidimensional: Test set setting being overridden by a post value
     140                        $name = "set_{$type}_overridden";
     141                        $default = "default_value_{$name}";
     142                        $initial_value = "initial_value_{$name}";
     143                        call_user_func( $type_options['setter'], $name, $initial_value );
     144                        $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) );
     145                        $this->assertEquals( $initial_value, call_user_func( $type_options['getter'], $name, $this->undefined ) );
     146                        $this->assertEquals( $initial_value, $setting->value() );
     147                        $setting->preview(); // activate post_data
     148                        $this->assertEquals( 0, did_action( "customize_preview_{$setting->id}" ) ); // only applicable for custom types (not options or theme_mods)
     149                        $this->assertEquals( 0, did_action( "customize_preview_{$setting->type}" ) ); // only applicable for custom types (not options or theme_mods)
     150                        $this->assertEquals( $this->post_data_overrides[ $name ], call_user_func( $type_options['getter'], $name, $this->undefined ) );
     151                        $this->assertEquals( $this->post_data_overrides[ $name ], $setting->value() );
     152                }
     153        }
     154
     155        /**
     156         * Run assertions on multidimensional standard settings
     157         */
     158        function test_preview_standard_types_multidimensional() {
     159                // @todo this is hacky. The manager should provide a mechanism to override the post_values
     160                $_POST['customized'] = wp_slash( wp_json_encode( $this->post_data_overrides ) );
     161
     162                foreach ( $this->standard_type_configs as $type => $type_options ) {
     163                        // Multidimensional: See what effect the preview filter has on a non-existent setting (default value should be seen)
     164                        $base_name = "unset_{$type}_multi";
     165                        $name = $base_name . '[foo]';
     166                        $default = "default_value_{$name}";
     167                        $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) );
     168                        $this->assertEquals( $this->undefined, call_user_func( $type_options['getter'], $base_name, $this->undefined ) );
     169                        $this->assertEquals( $default, $setting->value() );
     170                        $setting->preview();
     171                        $base_value = call_user_func( $type_options['getter'], $base_name, $this->undefined );
     172                        $this->assertArrayHasKey( 'foo', $base_value );
     173                        $this->assertEquals( $default, $base_value['foo'] );
     174
     175                        // Multidimensional: See what effect the preview has on an extant setting (default value should not be seen)
     176                        $base_name = "set_{$type}_multi";
     177                        $name = $base_name . '[foo]';
     178                        $default = "default_value_{$name}";
     179                        $initial_value = "initial_value_{$name}";
     180                        $base_initial_value = array( 'foo' => $initial_value, 'bar' => 'persisted' );
     181                        call_user_func( $type_options['setter'], $base_name, $base_initial_value );
     182                        $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) );
     183                        $base_value = call_user_func( $type_options['getter'], $base_name, array() );
     184                        $this->assertEquals( $initial_value, $base_value['foo'] );
     185                        $this->assertEquals( $initial_value, $setting->value() );
     186                        $setting->preview();
     187                        $this->assertEquals( 0, did_action( "customize_preview_{$setting->id}" ) ); // only applicable for custom types (not options or theme_mods)
     188                        $this->assertEquals( 0, did_action( "customize_preview_{$setting->type}" ) ); // only applicable for custom types (not options or theme_mods)
     189                        $base_value = call_user_func( $type_options['getter'], $base_name, array() );
     190                        $this->assertEquals( $initial_value, $base_value['foo'] );
     191                        $this->assertEquals( $initial_value, $setting->value() );
     192
     193                        // Multidimensional: Test unset setting being overridden by a post value
     194                        $base_name = "unset_{$type}_multi_overridden";
     195                        $name = $base_name . '[foo]';
     196                        $default = "default_value_{$name}";
     197                        $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) );
     198                        $this->assertEquals( $this->undefined, call_user_func( $type_options['getter'], $base_name, $this->undefined ) );
     199                        $this->assertEquals( $default, $setting->value() );
     200                        $setting->preview();
     201                        $this->assertEquals( 0, did_action( "customize_preview_{$setting->id}" ) ); // only applicable for custom types (not options or theme_mods)
     202                        $this->assertEquals( 0, did_action( "customize_preview_{$setting->type}" ) ); // only applicable for custom types (not options or theme_mods)
     203                        $base_value = call_user_func( $type_options['getter'], $base_name, $this->undefined );
     204                        $this->assertArrayHasKey( 'foo', $base_value );
     205                        $this->assertEquals( $this->post_data_overrides[ $name ], $base_value['foo'] );
     206
     207                        // Multidimemsional: Test set setting being overridden by a post value
     208                        $base_name = "set_{$type}_multi_overridden";
     209                        $name = $base_name . '[foo]';
     210                        $default = "default_value_{$name}";
     211                        $initial_value = "initial_value_{$name}";
     212                        $base_initial_value = array( 'foo' => $initial_value, 'bar' => 'persisted' );
     213                        call_user_func( $type_options['setter'], $base_name, $base_initial_value );
     214                        $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) );
     215                        $base_value = call_user_func( $type_options['getter'], $base_name, $this->undefined );
     216                        $this->arrayHasKey( 'foo', $base_value );
     217                        $this->arrayHasKey( 'bar', $base_value );
     218                        $this->assertEquals( $base_initial_value['foo'], $base_value['foo'] );
     219                        $this->assertEquals( $base_initial_value['bar'], call_user_func( $type_options['getter'], $base_name, $this->undefined )['bar'] );
     220                        $this->assertEquals( $initial_value, $setting->value() );
     221                        $setting->preview();
     222                        $this->assertEquals( 0, did_action( "customize_preview_{$setting->id}" ) ); // only applicable for custom types (not options or theme_mods)
     223                        $this->assertEquals( 0, did_action( "customize_preview_{$setting->type}" ) ); // only applicable for custom types (not options or theme_mods)
     224                        $base_value = call_user_func( $type_options['getter'], $base_name, $this->undefined );
     225                        $this->assertArrayHasKey( 'foo', $base_value );
     226                        $this->assertEquals( $this->post_data_overrides[ $name ], $base_value['foo'] );
     227                        $this->arrayHasKey( 'bar', call_user_func( $type_options['getter'], $base_name, $this->undefined ) );
     228                        $this->assertEquals( $base_initial_value['bar'], call_user_func( $type_options['getter'], $base_name, $this->undefined )['bar'] );
     229                }
     230        }
     231
     232        /**
     233         * @var array storage for saved custom type data that are tested in self::test_preview_custom_type()
     234         */
     235        protected $custom_type_data_saved;
     236
     237        /**
     238         * @var array storage for previewed custom type data that are tested in self::test_preview_custom_type()
     239         */
     240        protected $custom_type_data_previewed;
     241
     242        function custom_type_getter( $name, $default = null ) {
     243                if ( did_action( "customize_preview_{$name}" ) && array_key_exists( $name, $this->custom_type_data_previewed ) ) {
     244                        $value = $this->custom_type_data_previewed[ $name ];
     245                } else if ( array_key_exists( $name, $this->custom_type_data_saved ) ) {
     246                        $value = $this->custom_type_data_saved[ $name ];
     247                } else {
     248                        $value = $default;
     249                }
     250                return $value;
     251        }
     252
     253        function custom_type_setter( $name, $value ) {
     254                $this->custom_type_data_saved[ $name ] = $value;
     255        }
     256
     257        function custom_type_value_filter( $default ) {
     258                $name = preg_replace( '/^customize_value_/', '', current_filter() );
     259                return $this->custom_type_getter( $name, $default );
     260        }
     261
     262        /**
     263         * @var WP_Customize_Setting $setting
     264         */
     265        function custom_type_preview( $setting ) {
     266                $previewed_value = $setting->post_value( $this->undefined );
     267                if ( $this->undefined !== $previewed_value ) {
     268                        $this->custom_type_data_previewed[ $setting->id ] = $previewed_value;
     269                }
     270        }
     271
     272        function test_preview_custom_type() {
     273                $type = 'custom_type';
     274                $post_data_overrides = array(
     275                        "unset_{$type}_with_post_value" => "unset_{$type}_without_post_value",
     276                        "set_{$type}_with_post_value" => "set_{$type}_without_post_value",
     277                );
     278                $_POST['customized'] = wp_slash( wp_json_encode( $post_data_overrides ) );
     279
     280                $this->custom_type_data_saved = array();
     281                $this->custom_type_data_previewed = array();
     282
     283                add_action( "customize_preview_{$type}", array( $this, custom_type_preview ) );
     284
     285                // Custom type not existing and no post value override
     286                $name = "unset_{$type}_without_post_value";
     287                $default = "default_value_{$name}";
     288                $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) );
     289                // Note: #29316 will allow us to have one filter for all settings of a given type, which is what we need
     290
     291                add_filter( "customize_value_{$name}", array( $this, 'custom_type_value_filter' ) );
     292                $this->assertEquals( $this->undefined, $this->custom_type_getter( $name, $this->undefined ) );
     293                $this->assertEquals( $default, $setting->value() );
     294                $setting->preview();
     295                $this->assertEquals( 1, did_action( "customize_preview_{$setting->id}" ) );
     296                $this->assertEquals( 1, did_action( "customize_preview_{$setting->type}" ) );
     297                $this->assertEquals( $this->undefined, $this->custom_type_getter( $name, $this->undefined ) ); // Note: for a non-custom type this is $default
     298                $this->assertEquals( $default, $setting->value() ); // should be same as above
     299
     300                // Custom type existing and no post value override
     301                $name = "set_{$type}_without_post_value";
     302                $default = "default_value_{$name}";
     303                $initial_value = "initial_value_{$name}";
     304                $this->custom_type_setter( $name, $initial_value );
     305                $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) );
     306                // Note: #29316 will allow us to have one filter for all settings of a given type, which is what we need
     307                add_filter( "customize_value_{$name}", array( $this, 'custom_type_value_filter' ) );
     308                $this->assertEquals( $initial_value, $this->custom_type_getter( $name, $this->undefined ) );
     309                $this->assertEquals( $initial_value, $setting->value() );
     310                $setting->preview();
     311                $this->assertEquals( 1, did_action( "customize_preview_{$setting->id}" ) );
     312                $this->assertEquals( 2, did_action( "customize_preview_{$setting->type}" ) );
     313                $this->assertEquals( $initial_value, $this->custom_type_getter( $name, $this->undefined ) ); // should be same as above
     314                $this->assertEquals( $initial_value, $setting->value() ); // should be same as above
     315
     316                // Custom type not existing and with a post value override
     317                $name = "unset_{$type}_with_post_value";
     318                $default = "default_value_{$name}";
     319                $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) );
     320                // Note: #29316 will allow us to have one filter for all settings of a given type, which is what we need
     321                add_filter( "customize_value_{$name}", array( $this, 'custom_type_value_filter' ) );
     322                $this->assertEquals( $this->undefined, $this->custom_type_getter( $name, $this->undefined ) );
     323                $this->assertEquals( $default, $setting->value() );
     324                $setting->preview();
     325                $this->assertEquals( 1, did_action( "customize_preview_{$setting->id}" ) );
     326                $this->assertEquals( 3, did_action( "customize_preview_{$setting->type}" ) );
     327                $this->assertEquals( $post_data_overrides[ $name ], $this->custom_type_getter( $name, $this->undefined ) );
     328                $this->assertEquals( $post_data_overrides[ $name ], $setting->value() );
     329
     330                // Custom type not existing and with a post value override
     331                $name = "set_{$type}_with_post_value";
     332                $default = "default_value_{$name}";
     333                $initial_value = "initial_value_{$name}";
     334                $this->custom_type_setter( $name, $initial_value );
     335                $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) );
     336                // Note: #29316 will allow us to have one filter for all settings of a given type, which is what we need
     337                add_filter( "customize_value_{$name}", array( $this, 'custom_type_value_filter' ) );
     338                $this->assertEquals( $initial_value, $this->custom_type_getter( $name, $this->undefined ) );
     339                $this->assertEquals( $initial_value, $setting->value() );
     340                $setting->preview();
     341                $this->assertEquals( 1, did_action( "customize_preview_{$setting->id}" ) );
     342                $this->assertEquals( 4, did_action( "customize_preview_{$setting->type}" ) );
     343                $this->assertEquals( $post_data_overrides[ $name ], $this->custom_type_getter( $name, $this->undefined ) );
     344                $this->assertEquals( $post_data_overrides[ $name ], $setting->value() );
     345
     346                unset( $this->custom_type_data_previewed, $this->custom_type_data_saved );
     347        }
     348
     349        /**
     350         * Test specific fix for setting's default value not applying on preview window
     351         *
     352         * @ticket 30988
     353         */
     354        function test_non_posted_setting_applying_default_value_in_preview() {
     355                $type = 'option';
     356                $name = 'unset_option_without_post_value';
     357                $default = "default_value_{$name}";
     358                $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) );
     359                $this->assertEquals( $this->undefined, get_option( $name, $this->undefined ) );
     360                $this->assertEquals( $default, $setting->value() );
     361                $setting->preview();
     362                $this->assertEquals( $default, get_option( $name, $this->undefined ), sprintf( 'Expected get_option(%s) to return setting default: %s.', $name, $default ) );
     363                $this->assertEquals( $default, $setting->value() );
     364        }
     365
     366        // @todo function test_save() {
     367        // @todo test do_action( 'customize_save_' . $this->id_data[ 'base' ], $this );
     368        // @todo test_post_value()
     369        // @todo test_sanitize( $value )
     370        // @todo apply_filters( "customize_sanitize_{$this->id}", $value, $this );
     371        // @todo function update( $value )
     372        // @todo test_value()
     373        // @todo test customize_value_{$name} filter
     374        // @todo test_js_value()
     375        // @todo test apply_filters( "customize_sanitize_js_{$this->id}", $this->value(), $this );
     376        // @todo test_check_capabilities() {
     377
     378        // @todo final protected function multidimensional( &$root, $keys, $create = false )
     379        // @todo final protected function multidimensional_replace( $root, $keys, $value )
     380        // @todo final protected function multidimensional_get( $root, $keys, $default = null ) {
     381        // @todo final protected function multidimensional_isset( $root, $keys )
     382}
     383