WordPress.org

Make WordPress Core

Ticket #30936: 30936.diff

File 30936.diff, 61.7 KB (added by westonruter, 5 years ago)

https://github.com/xwp/wordpress-develop/pull/67

  • 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..9ede050 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 { 
    102102                add_action( 'wp_ajax_customize_save', array( $this, 'save' ) );
    103103
    104104                add_action( 'customize_register',                 array( $this, 'register_controls' ) );
     105                add_action( 'customize_register',                 array( $this, 'register_dynamic_settings' ), 11 ); // allow code to create settings first
    105106                add_action( 'customize_controls_init',            array( $this, 'prepare_controls' ) );
    106107                add_action( 'customize_controls_enqueue_scripts', array( $this, 'enqueue_control_scripts' ) );
    107108        }
    final class WP_Customize_Manager { 
    111112         *
    112113         * @since 3.4.0
    113114         *
     115         * @param string|null $action whether the supplied Ajax action is being run (since 4.2.0).
     116         *
    114117         * @return bool
    115118         */
    116         public function doing_ajax() {
    117                 return isset( $_POST['customized'] ) || ( defined( 'DOING_AJAX' ) && DOING_AJAX );
     119        public function doing_ajax( $action = null ) {
     120                $doing_ajax = ( defined( 'DOING_AJAX' ) && DOING_AJAX );
     121                if ( ! $doing_ajax ) {
     122                        return false;
     123                }
     124
     125                if ( ! $action ) {
     126                        return true;
     127                } else {
     128                        // Note: we can't just use doing_action( "wp_ajax_{$action}" ) because we need to check before admin-ajax.php gets to that point
     129                        return isset( $_REQUEST['action'] ) && wp_unslash( $_REQUEST['action'] ) === $action;
     130                }
    118131        }
    119132
    120133        /**
    final class WP_Customize_Manager { 
    399412        }
    400413
    401414        /**
    402          * Decode the $_POST['customized'] values for a specific Customize Setting.
     415         * Parse the incoming $_POST['customized'] JSON data and store the unsanitized
     416         * settings for subsequent post_value() lookups.
    403417         *
    404          * @since 3.4.0
     418         * @since 4.1.1
    405419         *
    406          * @param WP_Customize_Setting $setting A WP_Customize_Setting derived object
    407          * @return string $post_value Sanitized value
     420         * @return array
    408421         */
    409         public function post_value( $setting ) {
     422        public function unsanitized_post_values() {
    410423                if ( ! isset( $this->_post_values ) ) {
    411                         if ( isset( $_POST['customized'] ) )
     424                        if ( isset( $_POST['customized'] ) ) {
    412425                                $this->_post_values = json_decode( wp_unslash( $_POST['customized'] ), true );
    413                         else
     426                        }
     427                        if ( empty( $this->_post_values ) ) { // if not isset or of JSON error
    414428                                $this->_post_values = false;
     429                        }
     430                }
     431                if ( empty( $this->_post_values ) ) {
     432                        return array();
     433                } else {
     434                        return $this->_post_values;
    415435                }
     436        }
    416437
    417                 if ( isset( $this->_post_values[ $setting->id ] ) )
    418                         return $setting->sanitize( $this->_post_values[ $setting->id ] );
     438        /**
     439         * Return the sanitized value for a given setting from the request's POST data.
     440         *
     441         * @since 3.4.0
     442         *
     443         * @param WP_Customize_Setting $setting A WP_Customize_Setting derived object
     444         * @param mixed $default value returned $setting has no post value (added in 4.2.0).
     445         * @return string|mixed $post_value Sanitized value or the $default provided
     446         */
     447        public function post_value( $setting, $default = null ) {
     448                $post_values = $this->unsanitized_post_values();
     449                if ( array_key_exists( $setting->id, $post_values ) ) {
     450                        return $setting->sanitize( $post_values[ $setting->id ] );
     451                } else {
     452                        return $default;
     453                }
    419454        }
    420455
    421456        /**
    final class WP_Customize_Manager { 
    704739        }
    705740
    706741        /**
     742         * Register any dynamically-created settings, such as those from $_POST['customized'] that have no corresponding setting created.
     743         *
     744         * This is a mechanism to "wake up" settings that have been dynamically created
     745         * on the frontend and have been added to a transaction. When the transaction is
     746         * loaded, the dynamically-created settings then will get created and previewed
     747         * even though they are not directly created statically with code.
     748         *
     749         * @param string[] $setting_ids  The setting IDs to add.
     750         * @return WP_Customize_Setting[]  The settings added.
     751         */
     752        public function add_dynamic_settings( $setting_ids ) {
     753                $new_settings = array();
     754                foreach ( $setting_ids as $setting_id ) {
     755                        // Skip settings already created
     756                        if ( $this->get_setting( $setting_id ) ) {
     757                                continue;
     758                        }
     759                        $setting_args = false;
     760                        $setting_class = 'WP_Customize_Setting';
     761
     762                        /**
     763                         * Filter a dynamic setting's constructor args.
     764                         *
     765                         * For a dynamic setting to be registered, this filter must be employed
     766                         * to override the default false value with an array of args to pass to
     767                         * the WP_Customize_Setting constructor.
     768                         *
     769                         * @since 4.2.0
     770                         *
     771                         * @param false|array $setting_args  The arguments to the WP_Customize_Setting constructor.
     772                         * @param string $setting_id  ID for dynamic setting, usually coming from $_POST['customized'].
     773                         */
     774                        $setting_args = apply_filters( 'customize_dynamic_setting_args', $setting_args, $setting_id );
     775                        if ( false === $setting_args ) {
     776                                continue;
     777                        }
     778
     779                        /**
     780                         * Allow non-statically created settings to be constructed with custom WP_Customize_Setting subclass.
     781                         *
     782                         * @since 4.2.0
     783                         *
     784                         * @param string $setting_class  WP_Customize_Setting or a subclass.
     785                         * @param string $setting_id  ID for dynamic setting, usually coming from $_POST['customized'].
     786                         * @param string $setting_args  WP_Customize_Setting or a subclass.
     787                         */
     788                        $setting_class = apply_filters( 'customize_dynamic_setting_class', $setting_class, $setting_id, $setting_args );
     789
     790                        $setting = new $setting_class( $this, $setting_id, $setting_args );
     791                        $this->add_setting( $setting );
     792                        $new_settings[] = $setting;
     793                }
     794                return $new_settings;
     795        }
     796
     797        /**
    707798         * Retrieve a customize setting.
    708799         *
    709800         * @since 3.4.0
    final class WP_Customize_Manager { 
    12521343        }
    12531344
    12541345        /**
     1346         * Add settings from the POST data that were not added with code, e.g. dynamically-created settings for Widgets
     1347         *
     1348         * @since 4.2.0
     1349         */
     1350        public function register_dynamic_settings() {
     1351                $this->add_dynamic_settings( array_keys( $this->unsanitized_post_values() ) );
     1352        }
     1353
     1354        /**
    12551355         * Callback for validating the header_textcolor value.
    12561356         *
    12571357         * Accepts 'blank', and otherwise uses sanitize_hex_color_no_hash().
  • 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..ffcd85c 100644
    class WP_Customize_Setting { 
    5555        protected $id_data = array();
    5656
    5757        /**
    58          * Cached and sanitized $_POST value for the setting.
    59          *
    60          * @access private
    61          * @var mixed
    62          */
    63         private $_post_value;
    64 
    65         /**
    6658         * Constructor.
    6759         *
    6860         * Any supplied $args override class property defaults.
    class WP_Customize_Setting { 
    10092                        add_filter( "customize_sanitize_js_{$this->id}", $this->sanitize_js_callback, 10, 2 );
    10193        }
    10294
     95        protected $_original_value;
     96
    10397        /**
    10498         * Handle previewing the setting.
    10599         *
    106100         * @since 3.4.0
    107101         */
    108102        public function preview() {
     103
     104                if ( ! isset( $this->_original_value ) ) {
     105                        $this->_original_value = $this->value();
     106                }
     107
    109108                switch( $this->type ) {
    110109                        case 'theme_mod' :
    111110                                add_filter( 'theme_mod_' . $this->id_data[ 'base' ], array( $this, '_preview_filter' ) );
    class WP_Customize_Setting { 
    156155         * @return mixed New or old value.
    157156         */
    158157        public function _preview_filter( $original ) {
    159                 return $this->multidimensional_replace( $original, $this->id_data[ 'keys' ], $this->post_value() );
     158                $undefined = new stdClass(); // symbol hack
     159                $post_value = $this->post_value( $undefined );
     160                if ( $undefined === $post_value ) {
     161                        $value = $this->_original_value;
     162                } else {
     163                        $value = $post_value;
     164                }
     165                $replaced = $this->multidimensional_replace( $original, $this->id_data['keys'], $value );
     166                return $replaced;
    160167        }
    161168
    162169        /**
    class WP_Customize_Setting { 
    197204         * @return mixed The default value on failure, otherwise the sanitized value.
    198205         */
    199206        final public function post_value( $default = null ) {
    200                 // Check for a cached value
    201                 if ( isset( $this->_post_value ) )
    202                         return $this->_post_value;
    203 
    204                 // Call the manager for the post value
    205                 $result = $this->manager->post_value( $this );
    206 
    207                 if ( isset( $result ) )
    208                         return $this->_post_value = $result;
    209                 else
    210                         return $default;
     207                return $this->manager->post_value( $this, $default );
    211208        }
    212209
    213210        /**
    class WP_Customize_Setting { 
    422419                        $node = &$node[ $key ];
    423420                }
    424421
    425                 if ( $create && ! isset( $node[ $last ] ) )
    426                         $node[ $last ] = array();
     422                if ( $create ) {
     423                        if ( ! is_array( $node ) ) {
     424                                // account for an array overriding a string or object value
     425                                $node = array();
     426                        }
     427                        if ( ! isset( $node[ $last ] ) ) {
     428                                $node[ $last ] = array();
     429                        }
     430                }
    427431
    428432                if ( ! isset( $node[ $last ] ) )
    429433                        return;
  • src/wp-includes/class-wp-customize-widgets.php

    diff --git src/wp-includes/class-wp-customize-widgets.php src/wp-includes/class-wp-customize-widgets.php
    index ba14828..9258e18 100644
    final class WP_Customize_Widgets { 
    3535        /**
    3636         * @since 3.9.0
    3737         * @access protected
    38          * @var
    39          */
    40         protected $_customized;
    41 
    42         /**
    43          * @since 3.9.0
    44          * @access protected
    4538         * @var array
    4639         */
    47         protected $_prepreview_added_filters = array();
     40        protected $rendered_sidebars = array();
    4841
    4942        /**
    5043         * @since 3.9.0
    5144         * @access protected
    5245         * @var array
    5346         */
    54         protected $rendered_sidebars = array();
     47        protected $rendered_widgets = array();
    5548
    5649        /**
    5750         * @since 3.9.0
    5851         * @access protected
    5952         * @var array
    6053         */
    61         protected $rendered_widgets = array();
     54        protected $old_sidebars_widgets = array();
    6255
    6356        /**
    64          * @since 3.9.0
     57         * Mapping of setting type to setting ID pattern.
     58         *
     59         * @since 4.2.0
    6560         * @access protected
    6661         * @var array
    6762         */
    68         protected $old_sidebars_widgets = array();
     63        protected $setting_id_patterns = array(
     64                'widget_instance' => '/^(widget_.+?)(?:\[(\d+)\])?$/',
     65                'sidebar_widgets' => '/^sidebars_widgets\[(.+?)\]$/',
     66        );
    6967
    7068        /**
    7169         * Initial loader.
    final class WP_Customize_Widgets { 
    7876        public function __construct( $manager ) {
    7977                $this->manager = $manager;
    8078
    81                 add_action( 'after_setup_theme',                       array( $this, 'setup_widget_addition_previews' ) );
     79                add_filter( 'customize_dynamic_setting_args',          array( $this, 'filter_customize_dynamic_setting_args' ), 10, 2 );
     80                add_action( 'after_setup_theme',                       array( $this, 'register_settings' ) );
    8281                add_action( 'wp_loaded',                               array( $this, 'override_sidebars_widgets_for_theme_switch' ) );
    8382                add_action( 'customize_controls_init',                 array( $this, 'customize_controls_init' ) );
    8483                add_action( 'customize_register',                      array( $this, 'schedule_customize_register' ), 1 );
    final class WP_Customize_Widgets { 
    9594        }
    9695
    9796        /**
    98          * Get an unslashed post value or return a default.
     97         * Get the widget setting type given a setting ID.
    9998         *
    100          * @since 3.9.0
     99         * @since 4.2.0
    101100         *
    102          * @access protected
     101         * @param $setting_id
    103102         *
    104          * @param string $name    Post value.
    105          * @param mixed  $default Default post value.
    106          * @return mixed Unslashed post value or default value.
     103         * @return string|null
    107104         */
    108         protected function get_post_value( $name, $default = null ) {
    109                 if ( ! isset( $_POST[ $name ] ) ) {
    110                         return $default;
     105        protected function get_setting_type( $setting_id ) {
     106                static $cache = array();
     107                if ( isset( $cache[ $setting_id ] ) ) {
     108                        return $cache[ $setting_id ];
    111109                }
    112 
    113                 return wp_unslash( $_POST[$name] );
     110                foreach ( $this->setting_id_patterns as $type => $pattern ) {
     111                        if ( preg_match( $pattern, $setting_id ) ) {
     112                                $cache[ $setting_id ] = $type;
     113                                return $type;
     114                        }
     115                }
     116                return null;
    114117        }
    115118
    116119        /**
    117          * Set up widget addition previews.
     120         * Inspect the incoming customized data for any widget settings, and dynamically add them up-front so widgets will be initialized properly.
    118121         *
    119          * Since the widgets get registered on 'widgets_init' before the Customizer
    120          * settings are set up on 'customize_register', we have to filter the options
    121          * similarly to how the setting previewer will filter the options later.
    122          *
    123          * @since 3.9.0
    124          *
    125          * @access public
     122         * @since 4.2.0
    126123         */
    127         public function setup_widget_addition_previews() {
    128                 $is_customize_preview = false;
    129 
    130                 if ( ! empty( $this->manager ) && ! is_admin() && 'on' === $this->get_post_value( 'wp_customize' ) ) {
    131                         $is_customize_preview = check_ajax_referer( 'preview-customize_' . $this->manager->get_stylesheet(), 'nonce', false );
    132                 }
    133 
    134                 $is_ajax_widget_update = false;
    135                 if ( $this->manager->doing_ajax() && 'update-widget' === $this->get_post_value( 'action' ) ) {
    136                         $is_ajax_widget_update = check_ajax_referer( 'update-widget', 'nonce', false );
    137                 }
    138 
    139                 $is_ajax_customize_save = false;
    140                 if ( $this->manager->doing_ajax() && 'customize_save' === $this->get_post_value( 'action' ) ) {
    141                         $is_ajax_customize_save = check_ajax_referer( 'save-customize_' . $this->manager->get_stylesheet(), 'nonce', false );
    142                 }
    143 
    144                 $is_valid_request = ( $is_ajax_widget_update || $is_customize_preview || $is_ajax_customize_save );
    145                 if ( ! $is_valid_request ) {
    146                         return;
    147                 }
    148 
    149                 // Input from Customizer preview.
    150                 if ( isset( $_POST['customized'] ) ) {
    151                         $this->_customized = json_decode( $this->get_post_value( 'customized' ), true );
    152                 } else { // Input from ajax widget update request.
    153                         $this->_customized = array();
    154                         $id_base = $this->get_post_value( 'id_base' );
    155                         $widget_number = $this->get_post_value( 'widget_number', false );
    156                         $option_name = 'widget_' . $id_base;
    157                         $this->_customized[ $option_name ] = array();
    158                         if ( preg_match( '/^[0-9]+$/', $widget_number ) ) {
    159                                 $option_name .= '[' . $widget_number . ']';
    160                                 $this->_customized[ $option_name ][ $widget_number ] = array();
     124        public function register_settings() {
     125                $widget_setting_ids = array();
     126                $incoming_setting_ids = array_keys( $this->manager->unsanitized_post_values() );
     127                foreach ( $incoming_setting_ids as $setting_id ) {
     128                        if ( ! is_null( $this->get_setting_type( $setting_id ) ) ) {
     129                                $widget_setting_ids[] = $setting_id;
    161130                        }
    162131                }
     132                if ( $this->manager->doing_ajax( 'update-widget' ) && isset( $_REQUEST['widget-id'] ) ) {
     133                        $widget_setting_ids[] = $this->get_setting_id( wp_unslash( $_REQUEST['widget-id'] ) );
     134                }
    163135
    164                 $function = array( $this, 'prepreview_added_sidebars_widgets' );
    165 
    166                 $hook = 'option_sidebars_widgets';
    167                 add_filter( $hook, $function );
    168                 $this->_prepreview_added_filters[] = compact( 'hook', 'function' );
    169 
    170                 $hook = 'default_option_sidebars_widgets';
    171                 add_filter( $hook, $function );
    172                 $this->_prepreview_added_filters[] = compact( 'hook', 'function' );
    173 
    174                 $function = array( $this, 'prepreview_added_widget_instance' );
    175                 foreach ( $this->_customized as $setting_id => $value ) {
    176                         if ( preg_match( '/^(widget_.+?)(?:\[(\d+)\])?$/', $setting_id, $matches ) ) {
    177                                 $option = $matches[1];
    178 
    179                                 $hook = sprintf( 'option_%s', $option );
    180                                 if ( ! has_filter( $hook, $function ) ) {
    181                                         add_filter( $hook, $function );
    182                                         $this->_prepreview_added_filters[] = compact( 'hook', 'function' );
    183                                 }
    184 
    185                                 $hook = sprintf( 'default_option_%s', $option );
    186                                 if ( ! has_filter( $hook, $function ) ) {
    187                                         add_filter( $hook, $function );
    188                                         $this->_prepreview_added_filters[] = compact( 'hook', 'function' );
    189                                 }
     136                $settings = $this->manager->add_dynamic_settings( array_unique( $widget_setting_ids ) );
    190137
    191                                 /*
    192                                  * Make sure the option is registered so that the update_option()
    193                                  * won't fail due to the filters providing a default value, which
    194                                  * causes the update_option() to get confused.
    195                                  */
    196                                 add_option( $option, array() );
     138                /*
     139                 * Preview settings right away so that widgets and sidebars will get registered properly.
     140                 * But don't do this if a customize_save because this will cause WP to think there is nothing
     141                 * changed that needs to be saved.
     142                 */
     143                if ( ! $this->manager->doing_ajax( 'customize_save' ) ) {
     144                        foreach ( $settings as $setting ) {
     145                                $setting->preview();
    197146                        }
    198147                }
    199148        }
    200149
    201150        /**
    202          * Ensure that newly-added widgets will appear in the widgets_sidebars.
     151         * Determine the arguments for a dynamically-created setting.
    203152         *
    204          * This is necessary because the Customizer's setting preview filters
    205          * are added after the widgets_init action, which is too late for the
    206          * widgets to be set up properly.
     153         * @since 4.2.0
    207154         *
    208          * @since 3.9.0
    209          * @access public
    210          *
    211          * @param array $sidebars_widgets Associative array of sidebars and their widgets.
    212          * @return array Filtered array of sidebars and their widgets.
     155         * @param false|array $args
     156         * @param string $setting_id
     157         * @return false|array
    213158         */
    214         public function prepreview_added_sidebars_widgets( $sidebars_widgets ) {
    215                 foreach ( $this->_customized as $setting_id => $value ) {
    216                         if ( preg_match( '/^sidebars_widgets\[(.+?)\]$/', $setting_id, $matches ) ) {
    217                                 $sidebar_id = $matches[1];
    218                                 $sidebars_widgets[ $sidebar_id ] = $value;
    219                         }
     159        public function filter_customize_dynamic_setting_args( $args, $setting_id ) {
     160                if ( $this->get_setting_type( $setting_id ) ) {
     161                        $args = $this->get_setting_args( $setting_id );
    220162                }
    221                 return $sidebars_widgets;
     163                return $args;
    222164        }
    223165
    224166        /**
    225          * Ensure newly-added widgets have empty instances so they
    226          * will be recognized.
    227          *
    228          * This is necessary because the Customizer's setting preview
    229          * filters are added after the widgets_init action, which is
    230          * too late for the widgets to be set up properly.
     167         * Get an unslashed post value or return a default.
    231168         *
    232169         * @since 3.9.0
    233          * @access public
    234170         *
    235          * @param array|bool|mixed $value Widget instance(s), false if open was empty.
    236          * @return array|mixed Widget instance(s) with additions.
    237          */
    238         public function prepreview_added_widget_instance( $value = false ) {
    239                 if ( ! preg_match( '/^(?:default_)?option_(widget_(.+))/', current_filter(), $matches ) ) {
    240                         return $value;
    241                 }
    242                 $id_base = $matches[2];
    243 
    244                 foreach ( $this->_customized as $setting_id => $setting ) {
    245                         $parsed_setting_id = $this->parse_widget_setting_id( $setting_id );
    246                         if ( is_wp_error( $parsed_setting_id ) || $id_base !== $parsed_setting_id['id_base'] ) {
    247                                 continue;
    248                         }
    249                         $widget_number = $parsed_setting_id['number'];
    250 
    251                         if ( is_null( $widget_number ) ) {
    252                                 // Single widget.
    253                                 if ( false === $value ) {
    254                                         $value = array();
    255                                 }
    256                         } else {
    257                                 // Multi widget.
    258                                 if ( empty( $value ) ) {
    259                                         $value = array( '_multiwidget' => 1 );
    260                                 }
    261                                 if ( ! isset( $value[ $widget_number ] ) ) {
    262                                         $value[ $widget_number ] = array();
    263                                 }
    264                         }
    265                 }
    266 
    267                 return $value;
    268         }
    269 
    270         /**
    271          * Remove pre-preview filters.
    272          *
    273          * Removes filters added in setup_widget_addition_previews()
    274          * to ensure widgets are populating the options during
    275          * 'widgets_init'.
     171         * @access protected
    276172         *
    277          * @since 3.9.0
    278          * @access public
     173         * @param string $name    Post value.
     174         * @param mixed  $default Default post value.
     175         * @return mixed Unslashed post value or default value.
    279176         */
    280         public function remove_prepreview_filters() {
    281                 foreach ( $this->_prepreview_added_filters as $prepreview_added_filter ) {
    282                         remove_filter( $prepreview_added_filter['hook'], $prepreview_added_filter['function'] );
     177        protected function get_post_value( $name, $default = null ) {
     178                if ( ! isset( $_POST[ $name ] ) ) {
     179                        return $default;
    283180                }
    284                 $this->_prepreview_added_filters = array();
     181
     182                return wp_unslash( $_POST[ $name ] );
    285183        }
    286184
    287185        /**
    final class WP_Customize_Widgets { 
    380278         * @access public
    381279         */
    382280        public function schedule_customize_register() {
    383                 if ( is_admin() ) { // @todo for some reason, $wp_customize->is_preview() is true here?
     281                if ( is_admin() ) {
    384282                        $this->customize_register();
    385283                } else {
    386284                        add_action( 'wp', array( $this, 'customize_register' ) );
    final class WP_Customize_Widgets { 
    412310                foreach ( array_keys( $wp_registered_widgets ) as $widget_id ) {
    413311                        $setting_id   = $this->get_setting_id( $widget_id );
    414312                        $setting_args = $this->get_setting_args( $setting_id );
    415 
    416                         $setting_args['sanitize_callback']    = array( $this, 'sanitize_widget_instance' );
    417                         $setting_args['sanitize_js_callback'] = array( $this, 'sanitize_widget_js_instance' );
    418 
    419                         $this->manager->add_setting( $setting_id, $setting_args );
    420 
     313                        if ( ! $this->manager->get_setting( $setting_id ) ) {
     314                                $this->manager->add_setting( $setting_id, $setting_args );
     315                        }
    421316                        $new_setting_ids[] = $setting_id;
    422317                }
    423318
    final class WP_Customize_Widgets { 
    452347                        if ( $is_registered_sidebar || $is_inactive_widgets ) {
    453348                                $setting_id   = sprintf( 'sidebars_widgets[%s]', $sidebar_id );
    454349                                $setting_args = $this->get_setting_args( $setting_id );
    455 
    456                                 $setting_args['sanitize_callback']    = array( $this, 'sanitize_sidebar_widgets' );
    457                                 $setting_args['sanitize_js_callback'] = array( $this, 'sanitize_sidebar_widgets_js_instance' );
    458 
    459                                 $this->manager->add_setting( $setting_id, $setting_args );
     350                                if ( ! $this->manager->get_setting( $setting_id ) ) {
     351                                        $this->manager->add_setting( $setting_id, $setting_args );
     352                                }
    460353                                $new_setting_ids[] = $setting_id;
    461354
    462355                                // Add section to contain controls.
    final class WP_Customize_Widgets { 
    527420                 * We have to register these settings later than customize_preview_init
    528421                 * so that other filters have had a chance to run.
    529422                 */
    530                 if ( did_action( 'customize_preview_init' ) ) {
     423                if ( ! $this->manager->doing_ajax( 'customize_save' ) ) {
    531424                        foreach ( $new_setting_ids as $new_setting_id ) {
    532425                                $this->manager->get_setting( $new_setting_id )->preview();
    533426                        }
    534427                }
    535                 $this->remove_prepreview_filters();
     428
     429                add_filter( 'sidebars_widgets',   array( $this, 'preview_sidebars_widgets' ), 1 );
    536430        }
    537431
    538432        /**
    final class WP_Customize_Widgets { 
    804698                        'transport'  => 'refresh',
    805699                        'default'    => array(),
    806700                );
     701
     702                if ( preg_match( $this->setting_id_patterns['sidebar_widgets'], $id, $matches ) ) {
     703                        $args['sanitize_callback'] = array( $this, 'sanitize_sidebar_widgets' );
     704                        $args['sanitize_js_callback'] = array( $this, 'sanitize_sidebar_widgets_js_instance' );
     705                } else if ( preg_match( $this->setting_id_patterns['widget_instance'], $id, $matches ) ) {
     706                        $args['sanitize_callback'] = array( $this, 'sanitize_widget_instance' );
     707                        $args['sanitize_js_callback'] = array( $this, 'sanitize_widget_js_instance' );
     708                }
     709
    807710                $args = array_merge( $args, $overrides );
    808711
    809712                /**
    final class WP_Customize_Widgets { 
    831734         * @return array Array of sanitized widget IDs.
    832735         */
    833736        public function sanitize_sidebar_widgets( $widget_ids ) {
    834                 global $wp_registered_widgets;
    835 
    836                 $widget_ids           = array_map( 'strval', (array) $widget_ids );
     737                $widget_ids = array_map( 'strval', (array) $widget_ids );
    837738                $sanitized_widget_ids = array();
    838 
    839739                foreach ( $widget_ids as $widget_id ) {
    840                         if ( array_key_exists( $widget_id, $wp_registered_widgets ) ) {
    841                                 $sanitized_widget_ids[] = $widget_id;
    842                         }
     740                        $sanitized_widget_ids[] = preg_replace( '/[^a-z0-9_\-]/', '', $widget_id );
    843741                }
    844742                return $sanitized_widget_ids;
    845743        }
    final class WP_Customize_Widgets { 
    974872         * @access public
    975873         */
    976874        public function customize_preview_init() {
    977                 add_filter( 'sidebars_widgets',   array( $this, 'preview_sidebars_widgets' ), 1 );
    978875                add_action( 'wp_enqueue_scripts', array( $this, 'customize_preview_enqueue' ) );
    979876                add_action( 'wp_print_styles',    array( $this, 'print_preview_css' ), 1 );
    980877                add_action( 'wp_footer',          array( $this, 'export_preview_data' ), 20 );
    final class WP_Customize_Widgets { 
    13441241                $form = ob_get_clean();
    13451242
    13461243                // Obtain the widget instance.
    1347                 $option = get_option( $option_name );
    1348 
     1244                $option = $this->get_captured_option( $option_name );
    13491245                if ( null !== $parsed_id['number'] ) {
    13501246                        $instance = $option[$parsed_id['number']];
    13511247                } else {
    final class WP_Customize_Widgets { 
    13831279                        wp_die( -1 );
    13841280                }
    13851281
    1386                 if ( ! isset( $_POST['widget-id'] ) ) {
    1387                         wp_send_json_error();
     1282                if ( empty( $_POST['widget-id'] ) ) {
     1283                        wp_send_json_error( 'missing_widget-id' );
    13881284                }
    13891285
    13901286                /** This action is documented in wp-admin/includes/ajax-actions.php */
    final class WP_Customize_Widgets { 
    13981294
    13991295                $widget_id = $this->get_post_value( 'widget-id' );
    14001296                $parsed_id = $this->parse_widget_id( $widget_id );
    1401                 $id_base   = $parsed_id['id_base'];
    1402 
    1403                 if ( isset( $_POST['widget-' . $id_base] ) && is_array( $_POST['widget-' . $id_base] ) && preg_match( '/__i__|%i%/', key( $_POST['widget-' . $id_base] ) ) ) {
    1404                         wp_send_json_error();
     1297                $id_base = $parsed_id['id_base'];
     1298
     1299                $is_updating_widget_template = (
     1300                        isset( $_POST[ 'widget-' . $id_base ] )
     1301                        &&
     1302                        is_array( $_POST[ 'widget-' . $id_base ] )
     1303                        &&
     1304                        preg_match( '/__i__|%i%/', key( $_POST[ 'widget-' . $id_base ] ) )
     1305                );
     1306                if ( $is_updating_widget_template ) {
     1307                        wp_send_json_error( 'template_widget_not_updatable' );
    14051308                }
    14061309
    14071310                $updated_widget = $this->call_widget_update( $widget_id ); // => {instance,form}
    14081311                if ( is_wp_error( $updated_widget ) ) {
    1409                         wp_send_json_error();
     1312                        wp_send_json_error( $updated_widget->get_error_message() );
    14101313                }
    14111314
    14121315                $form = $updated_widget['form'];
    final class WP_Customize_Widgets { 
    14631366        }
    14641367
    14651368        /**
     1369         * Get the option that was captured from being saved.
     1370         *
     1371         * @since 4.2.0
     1372         * @access protected
     1373         * @return mixed
     1374         */
     1375        protected function get_captured_option( $name, $default = false ) {
     1376                if ( array_key_exists( $name, $this->_captured_options ) ) {
     1377                        $value = $this->_captured_options[ $name ];
     1378                } else {
     1379                        $value = $default;
     1380                }
     1381                return $value;
     1382        }
     1383
     1384        /**
    14661385         * Get the number of captured widget option updates.
    14671386         *
    14681387         * @since 3.9.0
  • 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..e131735
    - +  
     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::doing_ajax()
     36         *
     37         * @group ajax
     38         */
     39        function test_doing_ajax() {
     40                if ( ! defined( 'DOING_AJAX' ) ) {
     41                        define( 'DOING_AJAX', true );
     42                }
     43
     44                $manager = $this->instantiate();
     45                $this->assertTrue( $manager->doing_ajax() );
     46
     47                $_REQUEST['action'] = 'customize_save';
     48                $this->assertTrue( $manager->doing_ajax( 'customize_save' ) );
     49                $this->assertFalse( $manager->doing_ajax( 'update-widget' ) );
     50        }
     51
     52        /**
     53         * Test ! WP_Customize_Manager::doing_ajax()
     54         */
     55        function test_not_doing_ajax() {
     56                if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
     57                        $this->markTestSkipped( 'Cannot test when DOING_AJAX' );
     58                }
     59
     60                $manager = $this->instantiate();
     61                $this->assertFalse( $manager->doing_ajax() );
     62        }
     63
     64        /**
     65         * Test WP_Customize_Manager::unsanitized_post_values()
     66         *
     67         * @ticket 30988
     68         */
     69        function test_unsanitized_post_values() {
     70                $manager = $this->instantiate();
     71
     72                $customized = array(
     73                        'foo' => 'bar',
     74                        'baz[quux]' => 123,
     75                );
     76                $_POST['customized'] = wp_slash( wp_json_encode( $customized ) );
     77                $post_values = $manager->unsanitized_post_values();
     78                $this->assertEquals( $customized, $post_values );
     79        }
     80
     81        /**
     82         * Test the WP_Customize_Manager::post_value() method
     83         *
     84         * @ticket 30936
     85         */
     86        function test_post_value() {
     87                $posted_settings = array(
     88                        'foo' => 'OOF',
     89                );
     90                $_POST['customized'] = wp_slash( wp_json_encode( $posted_settings ) );
     91
     92                $manager = $this->instantiate();
     93
     94                $manager->add_setting( 'foo', array( 'default' => 'foo_default' ) ); // @todo this should return the setting instance
     95                $foo_setting = $manager->get_setting( 'foo' );
     96                $this->assertEquals( 'foo_default', $manager->get_setting( 'foo' )->value(), 'Expected non-previewed setting to return default when value() method called.' );
     97                $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]' );
     98
     99                $manager->add_setting( 'bar', array( 'default' => 'bar_default' ) );
     100                $bar_setting = $manager->get_setting( 'bar' );
     101                $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]' );
     102        }
     103
     104        /**
     105         * Test the WP_Customize_Manager::add_dynamic_settings() method.
     106         *
     107         * @ticket 30936
     108         */
     109        function test_add_dynamic_settings() {
     110                $manager = $this->instantiate();
     111                $setting_ids = array( 'foo', 'bar' );
     112                $manager->add_setting( 'foo', array( 'default' => 'foo_default' ) );
     113                $this->assertEmpty( $manager->get_setting( 'bar' ), 'Expected there to not be a bar setting up front.' );
     114                $manager->add_dynamic_settings( $setting_ids );
     115                $this->assertEmpty( $manager->get_setting( 'bar' ), 'Expected the bar setting to remain absent since filters not added.' );
     116
     117                $this->action_customize_register_for_dynamic_settings();
     118                $manager->add_dynamic_settings( $setting_ids );
     119                $this->assertNotEmpty( $manager->get_setting( 'bar' ), 'Expected bar setting to be created since filters were added.' );
     120                $this->assertEquals( 'foo_default', $manager->get_setting( 'foo' )->default, 'Expected static foo setting to not get overridden by dynamic setting.' );
     121                $this->assertEquals( 'dynamic_bar_default', $manager->get_setting( 'bar' )->default, 'Expected dynamic setting bar to have default providd by filter.' );
     122        }
     123
     124        /**
     125         * Test the WP_Customize_Manager::register_dynamic_settings() method.
     126         *
     127         * This is similar to test_add_dynamic_settings, except the settings are passed via $_POST['customized'].
     128         *
     129         * @ticket 30936
     130         */
     131        function test_register_dynamic_settings() {
     132                $posted_settings = array(
     133                        'foo' => 'OOF',
     134                        'bar' => 'RAB',
     135                );
     136                $_POST['customized'] = wp_slash( wp_json_encode( $posted_settings ) );
     137
     138                add_action( 'customize_register', array( $this, 'action_customize_register_for_dynamic_settings' ) );
     139
     140                $manager = $this->instantiate();
     141                $manager->add_setting( 'foo', array( 'default' => 'foo_default' ) );
     142
     143                $this->assertEmpty( $manager->get_setting( 'bar' ), 'Expected dynamic setting "bar" to not be registered.' );
     144                do_action( 'customize_register', $manager );
     145                $this->assertNotEmpty( $manager->get_setting( 'bar' ), 'Expected dynamic setting "bar" to be automatically registered after customize_register action.' );
     146                $this->assertEmpty( $manager->get_setting( 'baz' ), 'Expected unrecognized dynamic setting "baz" to remain unregistered.' );
     147        }
     148
     149        /**
     150         * In lieu of closures, callback for customize_register action added in test_register_dynamic_settings()
     151         */
     152        function action_customize_register_for_dynamic_settings() {
     153                add_filter( 'customize_dynamic_setting_args', array( $this, 'filter_customize_dynamic_setting_args_for_test_dynamic_settings' ), 10, 2 );
     154                add_filter( 'customize_dynamic_setting_class', array( $this, 'filter_customize_dynamic_setting_class_for_test_dynamic_settings' ), 10, 3 );
     155        }
     156
     157        /**
     158         * In lieu of closures, callback for customize_dynamic_setting_args filter added for test_register_dynamic_settings()
     159         */
     160        function filter_customize_dynamic_setting_args_for_test_dynamic_settings( $setting_args, $setting_id ) {
     161                $this->assertEquals( false, $setting_args, 'Expected $setting_args to be false by default.' );
     162                $this->assertInternalType( 'string', $setting_id );
     163                if ( in_array( $setting_id, array( 'foo', 'bar' ) ) ) {
     164                        $setting_args = array( 'default' => "dynamic_{$setting_id}_default" );
     165                }
     166                return $setting_args;
     167        }
     168
     169        /**
     170         * In lieu of closures, callback for customize_dynamic_setting_class filter added for test_register_dynamic_settings()
     171         */
     172        function filter_customize_dynamic_setting_class_for_test_dynamic_settings( $setting_class, $setting_id, $setting_args ) {
     173                $this->assertEquals( 'WP_Customize_Setting', $setting_class );
     174                $this->assertInternalType( 'string', $setting_id );
     175                $this->assertInternalType( 'array', $setting_args );
     176                return $setting_class;
     177        }
     178
     179}
     180
  • 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..625335f
    - +  
     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 custom types that
     234         */
     235        protected $custom_type_data_saved;
     236
     237        protected $custom_type_data_previewed;
     238
     239        function custom_type_getter( $name, $default = null ) {
     240                if ( did_action( "customize_preview_{$name}" ) && array_key_exists( $name, $this->custom_type_data_previewed ) ) {
     241                        $value = $this->custom_type_data_previewed[ $name ];
     242                } else if ( array_key_exists( $name, $this->custom_type_data_saved ) ) {
     243                        $value = $this->custom_type_data_saved[ $name ];
     244                } else {
     245                        $value = $default;
     246                }
     247                return $value;
     248        }
     249
     250        function custom_type_setter( $name, $value ) {
     251                $this->custom_type_data_saved[ $name ] = $value;
     252        }
     253
     254        function custom_type_value_filter( $default ) {
     255                $name = preg_replace( '/^customize_value_/', '', current_filter() );
     256                return $this->custom_type_getter( $name, $default );
     257        }
     258
     259        /**
     260         * @var WP_Customize_Setting $setting
     261         */
     262        function custom_type_preview( $setting ) {
     263                $previewed_value = $setting->post_value( $this->undefined );
     264                if ( $this->undefined !== $previewed_value ) {
     265                        $this->custom_type_data_previewed[ $setting->id ] = $previewed_value;
     266                }
     267        }
     268
     269        function test_preview_custom_type() {
     270                $type = 'custom_type';
     271                $post_data_overrides = array(
     272                        "unset_{$type}_with_post_value" => "unset_{$type}_without_post_value",
     273                        "set_{$type}_with_post_value" => "set_{$type}_without_post_value",
     274                );
     275                $_POST['customized'] = wp_slash( wp_json_encode( $post_data_overrides ) );
     276
     277                $this->custom_type_data_saved = array();
     278                $this->custom_type_data_previewed = array();
     279
     280                add_action( "customize_preview_{$type}", array( $this, custom_type_preview ) );
     281
     282                // Custom type not existing and no post value override
     283                $name = "unset_{$type}_without_post_value";
     284                $default = "default_value_{$name}";
     285                $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) );
     286                // Note: #29316 will allow us to have one filter for all settings of a given type, which is what we need
     287
     288                add_filter( "customize_value_{$name}", array( $this, 'custom_type_value_filter' ) );
     289                $this->assertEquals( $this->undefined, $this->custom_type_getter( $name, $this->undefined ) );
     290                $this->assertEquals( $default, $setting->value() );
     291                $setting->preview();
     292                $this->assertEquals( 1, did_action( "customize_preview_{$setting->id}" ) );
     293                $this->assertEquals( 1, did_action( "customize_preview_{$setting->type}" ) );
     294                $this->assertEquals( $this->undefined, $this->custom_type_getter( $name, $this->undefined ) ); // Note: for a non-custom type this is $default
     295                $this->assertEquals( $default, $setting->value() ); // should be same as above
     296
     297                // Custom type existing and no post value override
     298                $name = "set_{$type}_without_post_value";
     299                $default = "default_value_{$name}";
     300                $initial_value = "initial_value_{$name}";
     301                $this->custom_type_setter( $name, $initial_value );
     302                $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) );
     303                // Note: #29316 will allow us to have one filter for all settings of a given type, which is what we need
     304                add_filter( "customize_value_{$name}", array( $this, 'custom_type_value_filter' ) );
     305                $this->assertEquals( $initial_value, $this->custom_type_getter( $name, $this->undefined ) );
     306                $this->assertEquals( $initial_value, $setting->value() );
     307                $setting->preview();
     308                $this->assertEquals( 1, did_action( "customize_preview_{$setting->id}" ) );
     309                $this->assertEquals( 2, did_action( "customize_preview_{$setting->type}" ) );
     310                $this->assertEquals( $initial_value, $this->custom_type_getter( $name, $this->undefined ) ); // should be same as above
     311                $this->assertEquals( $initial_value, $setting->value() ); // should be same as above
     312
     313                // Custom type not existing and with a post value override
     314                $name = "unset_{$type}_with_post_value";
     315                $default = "default_value_{$name}";
     316                $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) );
     317                // Note: #29316 will allow us to have one filter for all settings of a given type, which is what we need
     318                add_filter( "customize_value_{$name}", array( $this, 'custom_type_value_filter' ) );
     319                $this->assertEquals( $this->undefined, $this->custom_type_getter( $name, $this->undefined ) );
     320                $this->assertEquals( $default, $setting->value() );
     321                $setting->preview();
     322                $this->assertEquals( 1, did_action( "customize_preview_{$setting->id}" ) );
     323                $this->assertEquals( 3, did_action( "customize_preview_{$setting->type}" ) );
     324                $this->assertEquals( $post_data_overrides[ $name ], $this->custom_type_getter( $name, $this->undefined ) );
     325                $this->assertEquals( $post_data_overrides[ $name ], $setting->value() );
     326
     327                // Custom type not existing and with a post value override
     328                $name = "set_{$type}_with_post_value";
     329                $default = "default_value_{$name}";
     330                $initial_value = "initial_value_{$name}";
     331                $this->custom_type_setter( $name, $initial_value );
     332                $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) );
     333                // Note: #29316 will allow us to have one filter for all settings of a given type, which is what we need
     334                add_filter( "customize_value_{$name}", array( $this, 'custom_type_value_filter' ) );
     335                $this->assertEquals( $initial_value, $this->custom_type_getter( $name, $this->undefined ) );
     336                $this->assertEquals( $initial_value, $setting->value() );
     337                $setting->preview();
     338                $this->assertEquals( 1, did_action( "customize_preview_{$setting->id}" ) );
     339                $this->assertEquals( 4, did_action( "customize_preview_{$setting->type}" ) );
     340                $this->assertEquals( $post_data_overrides[ $name ], $this->custom_type_getter( $name, $this->undefined ) );
     341                $this->assertEquals( $post_data_overrides[ $name ], $setting->value() );
     342
     343                unset( $this->custom_type_data_previewed, $this->custom_type_data_saved );
     344        }
     345
     346        /**
     347         * Test specific fix for setting's default value not applying on preview window
     348         *
     349         * @ticket 30988
     350         */
     351        function test_non_posted_setting_applying_default_value_in_preview() {
     352                $type = 'option';
     353                $name = 'unset_option_without_post_value';
     354                $default = "default_value_{$name}";
     355                $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) );
     356                $this->assertEquals( $this->undefined, get_option( $name, $this->undefined ) );
     357                $this->assertEquals( $default, $setting->value() );
     358                $setting->preview();
     359                $this->assertEquals( $default, get_option( $name, $this->undefined ), sprintf( 'Expected get_option(%s) to return setting default: %s.', $name, $default ) );
     360                $this->assertEquals( $default, $setting->value() );
     361        }
     362
     363        // @todo function test_save() {
     364        // @todo test do_action( 'customize_save_' . $this->id_data[ 'base' ], $this );
     365        // @todo test_post_value()
     366        // @todo test_sanitize( $value )
     367        // @todo apply_filters( "customize_sanitize_{$this->id}", $value, $this );
     368        // @todo function update( $value )
     369        // @todo test_value()
     370        // @todo test customize_value_{$name} filter
     371        // @todo test_js_value()
     372        // @todo test apply_filters( "customize_sanitize_js_{$this->id}", $this->value(), $this );
     373        // @todo test_check_capabilities() {
     374
     375        // @todo final protected function multidimensional( &$root, $keys, $create = false )
     376        // @todo final protected function multidimensional_replace( $root, $keys, $value )
     377        // @todo final protected function multidimensional_get( $root, $keys, $default = null ) {
     378        // @todo final protected function multidimensional_isset( $root, $keys )
     379}
     380
  • new file tests/phpunit/tests/customize/widgets.php

    diff --git tests/phpunit/tests/customize/widgets.php tests/phpunit/tests/customize/widgets.php
    new file mode 100644
    index 0000000..f070df2
    - +  
     1<?php
     2
     3/**
     4 * Tests for the WP_Customize_Widgets class.
     5 *
     6 * @group customize
     7 */
     8class Tests_WP_Customize_Widgets extends WP_UnitTestCase {
     9
     10        /**
     11         * @var WP_Customize_Manager
     12         */
     13        protected $manager;
     14
     15        function setUp() {
     16                parent::setUp();
     17                require_once( ABSPATH . WPINC . '/class-wp-customize-manager.php' );
     18                $GLOBALS['wp_customize'] = new WP_Customize_Manager(); // wpcs: override ok
     19                $this->manager = $GLOBALS['wp_customize'];
     20
     21                unset( $GLOBALS['_wp_sidebars_widgets'] ); // clear out cache set by wp_get_sidebars_widgets()
     22                $sidebars_widgets = wp_get_sidebars_widgets();
     23                $this->assertEqualSets( array( 'wp_inactive_widgets', 'sidebar-1' ), array_keys( wp_get_sidebars_widgets() ) );
     24                $this->assertContains( 'search-2', $sidebars_widgets['sidebar-1'] );
     25                $this->assertContains( 'categories-2', $sidebars_widgets['sidebar-1'] );
     26                $this->assertArrayHasKey( 2, get_option( 'widget_search' ) );
     27                $widget_categories = get_option( 'widget_categories' );
     28                $this->assertArrayHasKey( 2, $widget_categories );
     29                $this->assertEquals( '', $widget_categories['title'] );
     30
     31                remove_action( 'after_setup_theme', 'twentyfifteen_setup' ); // @todo We should not be including a theme anyway
     32
     33                $user_id = $this->factory->user->create( array( 'role' => 'administrator' ) );
     34                wp_set_current_user( $user_id );
     35        }
     36
     37        function tearDown() {
     38                parent::tearDown();
     39                $this->manager = null;
     40                unset( $GLOBALS['wp_customize'] );
     41        }
     42
     43        function set_customized_post_data( $customized ) {
     44                $_POST['customized'] = wp_slash( wp_json_encode( $customized ) );
     45        }
     46
     47        function do_customize_boot_actions() {
     48                do_action( 'setup_theme' );
     49                $_REQUEST['nonce'] = wp_create_nonce( 'preview-customize_' . $this->manager->theme()->get_stylesheet() );
     50                do_action( 'after_setup_theme' );
     51                do_action( 'init' );
     52                do_action( 'wp_loaded' );
     53                do_action( 'wp', $GLOBALS['wp'] );
     54        }
     55
     56        /**
     57         * Test WP_Customize_Widgets::__construct()
     58         */
     59        function test_construct() {
     60                $this->assertInstanceOf( 'WP_Customize_Widgets', $this->manager->widgets );
     61                $this->assertEquals( $this->manager, $this->manager->widgets->manager );
     62        }
     63
     64        /**
     65         * Test WP_Customize_Widgets::register_settings()
     66         *
     67         * @ticket 30988
     68         */
     69        function test_register_settings() {
     70
     71                $raw_widget_customized = array(
     72                        'widget_categories[2]' => array(
     73                                'title' => 'Taxonomies Brand New Value',
     74                                'count' => 0,
     75                                'hierarchical' => 0,
     76                                'dropdown' => 0,
     77                        ),
     78                        'widget_search[3]' => array(
     79                                'title' => 'Not as good as Google!',
     80                        ),
     81                );
     82                $customized = array();
     83                foreach ( $raw_widget_customized as $setting_id => $instance ) {
     84                        $customized[ $setting_id ] = $this->manager->widgets->sanitize_widget_js_instance( $instance );
     85                }
     86
     87                $this->set_customized_post_data( $customized );
     88                $this->do_customize_boot_actions();
     89                $this->assertTrue( is_customize_preview() );
     90
     91                $this->assertNotEmpty( $this->manager->get_setting( 'widget_categories[2]' ), 'Expected setting for pre-existing widget category-2, being customized.' );
     92                $this->assertNotEmpty( $this->manager->get_setting( 'widget_search[2]' ), 'Expected setting for pre-existing widget search-2, not being customized.' );
     93                $this->assertNotEmpty( $this->manager->get_setting( 'widget_search[3]' ), 'Expected dynamic setting for non-existing widget search-3, being customized.' );
     94
     95                $widget_categories = get_option( 'widget_categories' );
     96                $this->assertEquals( $raw_widget_customized['widget_categories[2]'], $widget_categories[2], 'Expected $wp_customize->get_setting(widget_categories[2])->preview() to have been called.' );
     97        }
     98
     99        /**
     100         * Test WP_Customize_Widgets::get_setting_args()
     101         */
     102        function test_get_setting_args() {
     103
     104                add_filter( 'widget_customizer_setting_args', array( $this, 'filter_widget_customizer_setting_args' ), 10, 2 );
     105
     106                $default_args = array(
     107                        'type' => 'option',
     108                        'capability' => 'edit_theme_options',
     109                        'transport' => 'refresh',
     110                        'default' => array(),
     111                        'sanitize_callback' => array( $this->manager->widgets, 'sanitize_widget_instance' ),
     112                        'sanitize_js_callback' => array( $this->manager->widgets, 'sanitize_widget_js_instance' ),
     113                );
     114
     115                $args = $this->manager->widgets->get_setting_args( 'widget_foo[2]' );
     116                foreach ( $default_args as $key => $default_value ) {
     117                        $this->assertEquals( $default_value, $args[ $key ] );
     118                }
     119                $this->assertEquals( 'WIDGET_FOO[2]', $args['uppercase_id_set_by_filter'] );
     120
     121                $override_args = array(
     122                        'type' => 'theme_mod',
     123                        'capability' => 'edit_posts',
     124                        'transport' => 'postMessage',
     125                        'default' => array( 'title' => 'asd' ),
     126                        'sanitize_callback' => '__return_empty_array',
     127                        'sanitize_js_callback' => '__return_empty_array',
     128                );
     129                $args = $this->manager->widgets->get_setting_args( 'widget_bar[3]', $override_args );
     130                foreach ( $override_args as $key => $override_value ) {
     131                        $this->assertEquals( $override_value, $args[ $key ] );
     132                }
     133                $this->assertEquals( 'WIDGET_BAR[3]', $args['uppercase_id_set_by_filter'] );
     134
     135                $default_args = array(
     136                        'type' => 'option',
     137                        'capability' => 'edit_theme_options',
     138                        'transport' => 'refresh',
     139                        'default' => array(),
     140                        'sanitize_callback' => array( $this->manager->widgets, 'sanitize_sidebar_widgets' ),
     141                        'sanitize_js_callback' => array( $this->manager->widgets, 'sanitize_sidebar_widgets_js_instance' ),
     142                );
     143                $args = $this->manager->widgets->get_setting_args( 'sidebars_widgets[sidebar-1]' );
     144                foreach ( $default_args as $key => $default_value ) {
     145                        $this->assertEquals( $default_value, $args[ $key ] );
     146                }
     147                $this->assertEquals( 'SIDEBARS_WIDGETS[SIDEBAR-1]', $args['uppercase_id_set_by_filter'] );
     148
     149                $override_args = array(
     150                        'type' => 'theme_mod',
     151                        'capability' => 'edit_posts',
     152                        'transport' => 'postMessage',
     153                        'default' => array( 'title' => 'asd' ),
     154                        'sanitize_callback' => '__return_empty_array',
     155                        'sanitize_js_callback' => '__return_empty_array',
     156                );
     157                $args = $this->manager->widgets->get_setting_args( 'sidebars_widgets[sidebar-2]', $override_args );
     158                foreach ( $override_args as $key => $override_value ) {
     159                        $this->assertEquals( $override_value, $args[ $key ] );
     160                }
     161                $this->assertEquals( 'SIDEBARS_WIDGETS[SIDEBAR-2]', $args['uppercase_id_set_by_filter'] );
     162        }
     163
     164        function filter_widget_customizer_setting_args( $args, $id ) {
     165                $args['uppercase_id_set_by_filter'] = strtoupper( $id );
     166                return $args;
     167        }
     168
     169        /**
     170         * Test WP_Customize_Widgets::sanitize_widget_js_instance() and WP_Customize_Widgets::sanitize_widget_instance()
     171         */
     172        function test_sanitize_widget_js_instance() {
     173                $this->do_customize_boot_actions();
     174
     175                $new_categories_instance = array(
     176                        'title' => 'Taxonomies Brand New Value',
     177                        'count' => '1',
     178                        'hierarchical' => '1',
     179                        'dropdown' => '1',
     180                );
     181
     182                $sanitized_for_js = $this->manager->widgets->sanitize_widget_js_instance( $new_categories_instance );
     183                $this->assertArrayHasKey( 'encoded_serialized_instance', $sanitized_for_js );
     184                $this->assertTrue( is_serialized( base64_decode( $sanitized_for_js['encoded_serialized_instance'] ), true ) );
     185                $this->assertEquals( $new_categories_instance['title'], $sanitized_for_js['title'] );
     186                $this->assertTrue( $sanitized_for_js['is_widget_customizer_js_value'] );
     187                $this->assertArrayHasKey( 'instance_hash_key', $sanitized_for_js );
     188
     189                $corrupted_sanitized_for_js = $sanitized_for_js;
     190                $corrupted_sanitized_for_js['encoded_serialized_instance'] = base64_encode( serialize( array( 'title' => 'EVIL' ) ) );
     191                $this->assertNull( $this->manager->widgets->sanitize_widget_instance( $corrupted_sanitized_for_js ), 'Expected sanitize_widget_instance to reject corrupted data.' );
     192
     193                $unsanitized_from_js = $this->manager->widgets->sanitize_widget_instance( $sanitized_for_js );
     194                $this->assertEquals( $unsanitized_from_js, $new_categories_instance );
     195        }
     196}