WordPress.org

Make WordPress Core


Ignore:
Timestamp:
02/08/2015 11:10:05 PM (6 years ago)
Author:
ocean90
Message:

Customizer: Introduce an API to create WP_Customize_Settings for dynamically-created settings.

  • Introduce WP_Customize_Manager::add_dynamic_settings() to register dynamically-created settings.
  • Introduce customize_dynamic_setting_args filter to pass an array of args to a dynamic setting's constructor.
  • Add unit tests for WP_Customize_Manager and WP_Customize_Widgets.
  • See WP_Customize_Widgets as an example.

props westonruter.
fixes #30936.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/class-wp-customize-widgets.php

    r31226 r31370  
    3636     * @since 3.9.0
    3737     * @access protected
    38      * @var
    39      */
    40     protected $_customized;
     38     * @var array
     39     */
     40    protected $rendered_sidebars = array();
    4141
    4242    /**
     
    4545     * @var array
    4646     */
    47     protected $_prepreview_added_filters = array();
     47    protected $rendered_widgets = array();
    4848
    4949    /**
     
    5252     * @var array
    5353     */
    54     protected $rendered_sidebars = array();
    55 
    56     /**
    57      * @since 3.9.0
     54    protected $old_sidebars_widgets = array();
     55
     56    /**
     57     * Mapping of setting type to setting ID pattern.
     58     *
     59     * @since 4.2.0
    5860     * @access protected
    5961     * @var array
    6062     */
    61     protected $rendered_widgets = array();
    62 
    63     /**
    64      * @since 3.9.0
    65      * @access protected
    66      * @var array
    67      */
    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    /**
     
    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' ) );
     
    9695
    9796    /**
     97     * Get the widget setting type given a setting ID.
     98     *
     99     * @since 4.2.0
     100     *
     101     * @param $setting_id
     102     *
     103     * @return string|null
     104     */
     105    protected function get_setting_type( $setting_id ) {
     106        static $cache = array();
     107        if ( isset( $cache[ $setting_id ] ) ) {
     108            return $cache[ $setting_id ];
     109        }
     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;
     117    }
     118
     119    /**
     120     * Inspect the incoming customized data for any widget settings, and dynamically add them up-front so widgets will be initialized properly.
     121     *
     122     * @since 4.2.0
     123     */
     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;
     130            }
     131        }
     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        }
     135
     136        $settings = $this->manager->add_dynamic_settings( array_unique( $widget_setting_ids ) );
     137
     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();
     146            }
     147        }
     148    }
     149
     150    /**
     151     * Determine the arguments for a dynamically-created setting.
     152     *
     153     * @since 4.2.0
     154     *
     155     * @param false|array $args
     156     * @param string $setting_id
     157     * @return false|array
     158     */
     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 );
     162        }
     163        return $args;
     164    }
     165
     166    /**
    98167     * Get an unslashed post value or return a default.
    99168     *
     
    111180        }
    112181
    113         return wp_unslash( $_POST[$name] );
    114     }
    115 
    116     /**
    117      * Set up widget addition previews.
    118      *
    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
    126      */
    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();
    161             }
    162         }
    163 
    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                 }
    190 
    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() );
    197             }
    198         }
    199     }
    200 
    201     /**
    202      * Ensure that newly-added widgets will appear in the widgets_sidebars.
    203      *
    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.
    207      *
    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.
    213      */
    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             }
    220         }
    221         return $sidebars_widgets;
    222     }
    223 
    224     /**
    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.
    231      *
    232      * @since 3.9.0
    233      * @access public
    234      *
    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'.
    276      *
    277      * @since 3.9.0
    278      * @access public
    279      */
    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'] );
    283         }
    284         $this->_prepreview_added_filters = array();
     182        return wp_unslash( $_POST[ $name ] );
    285183    }
    286184
     
    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 {
     
    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        }
     
    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
     
    524417        }
    525418
    526         /*
    527          * We have to register these settings later than customize_preview_init
    528          * so that other filters have had a chance to run.
    529          */
    530         if ( did_action( 'customize_preview_init' ) ) {
     419        if ( ! $this->manager->doing_ajax( 'customize_save' ) ) {
    531420            foreach ( $new_setting_ids as $new_setting_id ) {
    532421                $this->manager->get_setting( $new_setting_id )->preview();
    533422            }
    534423        }
    535         $this->remove_prepreview_filters();
     424
     425        add_filter( 'sidebars_widgets', array( $this, 'preview_sidebars_widgets' ), 1 );
    536426    }
    537427
     
    805695            'default'    => array(),
    806696        );
     697
     698        if ( preg_match( $this->setting_id_patterns['sidebar_widgets'], $id, $matches ) ) {
     699            $args['sanitize_callback'] = array( $this, 'sanitize_sidebar_widgets' );
     700            $args['sanitize_js_callback'] = array( $this, 'sanitize_sidebar_widgets_js_instance' );
     701        } else if ( preg_match( $this->setting_id_patterns['widget_instance'], $id, $matches ) ) {
     702            $args['sanitize_callback'] = array( $this, 'sanitize_widget_instance' );
     703            $args['sanitize_js_callback'] = array( $this, 'sanitize_widget_js_instance' );
     704        }
     705
    807706        $args = array_merge( $args, $overrides );
    808707
     
    832731     */
    833732    public function sanitize_sidebar_widgets( $widget_ids ) {
    834         global $wp_registered_widgets;
    835 
    836         $widget_ids           = array_map( 'strval', (array) $widget_ids );
     733        $widget_ids = array_map( 'strval', (array) $widget_ids );
    837734        $sanitized_widget_ids = array();
    838 
    839735        foreach ( $widget_ids as $widget_id ) {
    840             if ( array_key_exists( $widget_id, $wp_registered_widgets ) ) {
    841                 $sanitized_widget_ids[] = $widget_id;
    842             }
     736            $sanitized_widget_ids[] = preg_replace( '/[^a-z0-9_\-]/', '', $widget_id );
    843737        }
    844738        return $sanitized_widget_ids;
     
    975869     */
    976870    public function customize_preview_init() {
    977         add_filter( 'sidebars_widgets',   array( $this, 'preview_sidebars_widgets' ), 1 );
    978871        add_action( 'wp_enqueue_scripts', array( $this, 'customize_preview_enqueue' ) );
    979872        add_action( 'wp_print_styles',    array( $this, 'print_preview_css' ), 1 );
     
    13161209        // Clean up any input vars that were manually added
    13171210        foreach ( $added_input_vars as $key ) {
    1318             unset( $_POST[$key] );
    1319             unset( $_REQUEST[$key] );
     1211            unset( $_POST[ $key ] );
     1212            unset( $_REQUEST[ $key ] );
    13201213        }
    13211214
     
    13341227        }
    13351228
     1229        // Obtain the widget instance.
     1230        $option = $this->get_captured_option( $option_name );
     1231        if ( null !== $parsed_id['number'] ) {
     1232            $instance = $option[ $parsed_id['number'] ];
     1233        } else {
     1234            $instance = $option;
     1235        }
     1236
     1237        /*
     1238         * Override the incoming $_POST['customized'] for a newly-created widget's
     1239         * setting with the new $instance so that the preview filter currently
     1240         * in place from WP_Customize_Setting::preview() will use this value
     1241         * instead of the default widget instance value (an empty array).
     1242         */
     1243        $setting_id = $this->get_setting_id( $widget_id );
     1244        $this->manager->set_post_value( $setting_id, $instance );
     1245
    13361246        // Obtain the widget control with the updated instance in place.
    13371247        ob_start();
    1338 
    1339         $form = $wp_registered_widget_controls[$widget_id];
     1248        $form = $wp_registered_widget_controls[ $widget_id ];
    13401249        if ( $form ) {
    13411250            call_user_func_array( $form['callback'], $form['params'] );
    13421251        }
    1343 
    13441252        $form = ob_get_clean();
    1345 
    1346         // Obtain the widget instance.
    1347         $option = get_option( $option_name );
    1348 
    1349         if ( null !== $parsed_id['number'] ) {
    1350             $instance = $option[$parsed_id['number']];
    1351         } else {
    1352             $instance = $option;
    1353         }
    13541253
    13551254        $this->stop_capturing_option_updates();
     
    13841283        }
    13851284
    1386         if ( ! isset( $_POST['widget-id'] ) ) {
    1387             wp_send_json_error();
     1285        if ( empty( $_POST['widget-id'] ) ) {
     1286            wp_send_json_error( 'missing_widget-id' );
    13881287        }
    13891288
     
    13991298        $widget_id = $this->get_post_value( 'widget-id' );
    14001299        $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();
     1300        $id_base = $parsed_id['id_base'];
     1301
     1302        $is_updating_widget_template = (
     1303            isset( $_POST[ 'widget-' . $id_base ] )
     1304            &&
     1305            is_array( $_POST[ 'widget-' . $id_base ] )
     1306            &&
     1307            preg_match( '/__i__|%i%/', key( $_POST[ 'widget-' . $id_base ] ) )
     1308        );
     1309        if ( $is_updating_widget_template ) {
     1310            wp_send_json_error( 'template_widget_not_updatable' );
    14051311        }
    14061312
    14071313        $updated_widget = $this->call_widget_update( $widget_id ); // => {instance,form}
    14081314        if ( is_wp_error( $updated_widget ) ) {
    1409             wp_send_json_error();
     1315            wp_send_json_error( $updated_widget->get_error_message() );
    14101316        }
    14111317
     
    14641370
    14651371    /**
     1372     * Get the option that was captured from being saved.
     1373     *
     1374     * @since 4.2.0
     1375     * @access protected
     1376     *
     1377     * @param string $option_name Option name.
     1378     * @param mixed  $default     Optional. Default value to return if the option does not exist.
     1379     * @return mixed Value set for the option.
     1380     */
     1381    protected function get_captured_option( $option_name, $default = false ) {
     1382        if ( array_key_exists( $option_name, $this->_captured_options ) ) {
     1383            $value = $this->_captured_options[ $option_name ];
     1384        } else {
     1385            $value = $default;
     1386        }
     1387        return $value;
     1388    }
     1389
     1390    /**
    14661391     * Get the number of captured widget option updates.
    14671392     *
     
    14971422     * @access public
    14981423     *
    1499      * @param mixed $new_value
    1500      * @param string $option_name
    1501      * @param mixed $old_value
    1502      * @return mixed
     1424     * @param mixed  $new_value   The new option value.
     1425     * @param string $option_name Name of the option.
     1426     * @param mixed  $old_value   The old option value.
     1427     * @return mixed Filtered option value.
    15031428     */
    15041429    public function capture_filter_pre_update_option( $new_value, $option_name, $old_value ) {
     
    15071432        }
    15081433
    1509         if ( ! isset( $this->_captured_options[$option_name] ) ) {
     1434        if ( ! isset( $this->_captured_options[ $option_name ] ) ) {
    15101435            add_filter( "pre_option_{$option_name}", array( $this, 'capture_filter_pre_get_option' ) );
    15111436        }
    15121437
    1513         $this->_captured_options[$option_name] = $new_value;
     1438        $this->_captured_options[ $option_name ] = $new_value;
    15141439
    15151440        return $old_value;
     
    15221447     * @access public
    15231448     *
    1524      * @param mixed $value Option
    1525      * @return mixed
     1449     * @param mixed $value Value to return instead of the option value.
     1450     * @return mixed Filtered option value.
    15261451     */
    15271452    public function capture_filter_pre_get_option( $value ) {
    15281453        $option_name = preg_replace( '/^pre_option_/', '', current_filter() );
    15291454
    1530         if ( isset( $this->_captured_options[$option_name] ) ) {
    1531             $value = $this->_captured_options[$option_name];
     1455        if ( isset( $this->_captured_options[ $option_name ] ) ) {
     1456            $value = $this->_captured_options[ $option_name ];
    15321457
    15331458            /** This filter is documented in wp-includes/option.php */
     
    15581483        $this->_is_capturing_option_updates = false;
    15591484    }
     1485
     1486    /**
     1487     * @since 3.9.0
     1488     * @deprecated 4.2.0 Deprecated in favor of customize_dynamic_setting_args filter.
     1489     */
     1490    public function setup_widget_addition_previews() {
     1491        _deprecated_function( __METHOD__, '4.2.0' );
     1492    }
     1493
     1494    /**
     1495     * @since 3.9.0
     1496     * @deprecated 4.2.0 Deprecated in favor of customize_dynamic_setting_args filter.
     1497     */
     1498    public function prepreview_added_sidebars_widgets() {
     1499        _deprecated_function( __METHOD__, '4.2.0' );
     1500    }
     1501
     1502    /**
     1503     * @since 3.9.0
     1504     * @deprecated 4.2.0 Deprecated in favor of customize_dynamic_setting_args filter.
     1505     */
     1506    public function prepreview_added_widget_instance() {
     1507        _deprecated_function( __METHOD__, '4.2.0' );
     1508    }
     1509
     1510    /**
     1511     * @since 3.9.0
     1512     * @deprecated 4.2.0 Deprecated in favor of customize_dynamic_setting_args filter.
     1513     */
     1514    public function remove_prepreview_filters() {
     1515        _deprecated_function( __METHOD__, '4.2.0' );
     1516    }
    15601517}
Note: See TracChangeset for help on using the changeset viewer.