diff --git a/src/wp-includes/class-wp-customize-manager.php b/src/wp-includes/class-wp-customize-manager.php index e620ca9ccf..4a776beb63 100644 --- a/src/wp-includes/class-wp-customize-manager.php +++ b/src/wp-includes/class-wp-customize-manager.php @@ -65,7 +65,7 @@ final class WP_Customize_Manager { /** * Unsanitized values for Customize Settings parsed from $_POST['customized']. * - * @var array|false + * @var array */ private $_post_values; @@ -102,6 +102,7 @@ final class WP_Customize_Manager { add_action( 'wp_ajax_customize_save', array( $this, 'save' ) ); add_action( 'customize_register', array( $this, 'register_controls' ) ); + add_action( 'customize_register', array( $this, 'register_dynamic_settings' ), 11 ); // allow code to create settings first add_action( 'customize_controls_init', array( $this, 'prepare_controls' ) ); add_action( 'customize_controls_enqueue_scripts', array( $this, 'enqueue_control_scripts' ) ); } @@ -110,11 +111,23 @@ final class WP_Customize_Manager { * Return true if it's an AJAX request. * * @since 3.4.0 + * @since 4.2.0 Added $action param. * + * @param string|null $action whether the supplied Ajax action is being run. * @return bool */ - public function doing_ajax() { - return isset( $_POST['customized'] ) || ( defined( 'DOING_AJAX' ) && DOING_AJAX ); + public function doing_ajax( $action = null ) { + $doing_ajax = ( defined( 'DOING_AJAX' ) && DOING_AJAX ); + if ( ! $doing_ajax ) { + return false; + } + + if ( ! $action ) { + return true; + } else { + // Note: we can't just use doing_action( "wp_ajax_{$action}" ) because we need to check before admin-ajax.php gets to that point + return isset( $_REQUEST['action'] ) && wp_unslash( $_REQUEST['action'] ) === $action; + } } /** @@ -411,8 +424,8 @@ final class WP_Customize_Manager { if ( isset( $_POST['customized'] ) ) { $this->_post_values = json_decode( wp_unslash( $_POST['customized'] ), true ); } - if ( empty( $this->_post_values ) ) { // if not isset or of JSON error - $this->_post_values = false; + if ( empty( $this->_post_values ) ) { // if not isset or if JSON error + $this->_post_values = array(); } } if ( empty( $this->_post_values ) ) { @@ -441,6 +454,19 @@ final class WP_Customize_Manager { } } + /** + * Override a setting's (unsanitized) value as found in any incoming $_POST['customized'] + * + * @since 4.2.0 + * + * @param string $setting_id The ID for the WP_Customize_Setting instance. + * @param mixed $value + */ + public function set_post_value( $setting_id, $value ) { + $this->unsanitized_post_values(); + $this->_post_values[ $setting_id ] = $value; + } + /** * Print JavaScript settings. * @@ -726,6 +752,65 @@ final class WP_Customize_Manager { $this->settings[ $setting->id ] = $setting; } + /** + * Register any dynamically-created settings, such as those from $_POST['customized'] that have no corresponding setting created. + * + * This is a mechanism to "wake up" settings that have been dynamically created + * on the frontend and have been sent to WordPress in $_POST['customized']. When WP + * loads, the dynamically-created settings then will get created and previewed + * even though they are not directly created statically with code. + * + * @since 4.2.0 + * + * @param string[] $setting_ids The setting IDs to add. + * @return WP_Customize_Setting[] The settings added. + */ + public function add_dynamic_settings( $setting_ids ) { + $new_settings = array(); + foreach ( $setting_ids as $setting_id ) { + // Skip settings already created + if ( $this->get_setting( $setting_id ) ) { + continue; + } + + $setting_args = false; + $setting_class = 'WP_Customize_Setting'; + + /** + * Filter a dynamic setting's constructor args. + * + * For a dynamic setting to be registered, this filter must be employed + * to override the default false value with an array of args to pass to + * the WP_Customize_Setting constructor. + * + * @since 4.2.0 + * + * @param false|array $setting_args The arguments to the WP_Customize_Setting constructor. + * @param string $setting_id ID for dynamic setting, usually coming from $_POST['customized']. + */ + $setting_args = apply_filters( 'customize_dynamic_setting_args', $setting_args, $setting_id ); + if ( false === $setting_args ) { + continue; + } + + /** + * Allow non-statically created settings to be constructed with custom WP_Customize_Setting subclass. + * + * @since 4.2.0 + * + * @param string $setting_class WP_Customize_Setting or a subclass. + * @param string $setting_id ID for dynamic setting, usually coming from $_POST['customized']. + * @param string $setting_args WP_Customize_Setting or a subclass. + */ + $setting_class = apply_filters( 'customize_dynamic_setting_class', $setting_class, $setting_id, $setting_args ); + + $setting = new $setting_class( $this, $setting_id, $setting_args ); + $this->add_setting( $setting ); + $new_settings[] = $setting; + } + return $new_settings; + } + /** * Retrieve a customize setting. * @@ -735,8 +820,9 @@ final class WP_Customize_Manager { * @return WP_Customize_Setting */ public function get_setting( $id ) { - if ( isset( $this->settings[ $id ] ) ) + if ( isset( $this->settings[ $id ] ) ) { return $this->settings[ $id ]; + } } /** @@ -1274,6 +1360,15 @@ final class WP_Customize_Manager { } } + /** + * Add settings from the POST data that were not added with code, e.g. dynamically-created settings for Widgets + * + * @since 4.2.0 + */ + public function register_dynamic_settings() { + $this->add_dynamic_settings( array_keys( $this->unsanitized_post_values() ) ); + } + /** * Callback for validating the header_textcolor value. * diff --git a/src/wp-includes/class-wp-customize-setting.php b/src/wp-includes/class-wp-customize-setting.php index 6cbaf3db62..1634c63992 100644 --- a/src/wp-includes/class-wp-customize-setting.php +++ b/src/wp-includes/class-wp-customize-setting.php @@ -54,14 +54,6 @@ class WP_Customize_Setting { protected $id_data = array(); - /** - * Cached and sanitized $_POST value for the setting. - * - * @access private - * @var mixed - */ - private $_post_value; - /** * Constructor. * @@ -163,7 +155,7 @@ class WP_Customize_Setting { */ public function _preview_filter( $original ) { $undefined = new stdClass(); // symbol hack - $post_value = $this->manager->post_value( $this, $undefined ); + $post_value = $this->post_value( $undefined ); if ( $undefined === $post_value ) { $value = $this->_original_value; } else { @@ -211,17 +203,7 @@ class WP_Customize_Setting { * @return mixed The default value on failure, otherwise the sanitized value. */ final public function post_value( $default = null ) { - // Check for a cached value - if ( isset( $this->_post_value ) ) - return $this->_post_value; - - // Call the manager for the post value - $result = $this->manager->post_value( $this ); - - if ( isset( $result ) ) - return $this->_post_value = $result; - else - return $default; + return $this->manager->post_value( $this, $default ); } /** diff --git a/src/wp-includes/class-wp-customize-widgets.php b/src/wp-includes/class-wp-customize-widgets.php index ba1482876e..22c51ea4d4 100644 --- a/src/wp-includes/class-wp-customize-widgets.php +++ b/src/wp-includes/class-wp-customize-widgets.php @@ -32,20 +32,6 @@ final class WP_Customize_Widgets { 'rss', 'search', 'tag_cloud', 'text', ); - /** - * @since 3.9.0 - * @access protected - * @var - */ - protected $_customized; - - /** - * @since 3.9.0 - * @access protected - * @var array - */ - protected $_prepreview_added_filters = array(); - /** * @since 3.9.0 * @access protected @@ -67,6 +53,18 @@ final class WP_Customize_Widgets { */ protected $old_sidebars_widgets = array(); + /** + * Mapping of setting type to setting ID pattern. + * + * @since 4.2.0 + * @access protected + * @var array + */ + protected $setting_id_patterns = array( + 'widget_instance' => '/^(widget_.+?)(?:\[(\d+)\])?$/', + 'sidebar_widgets' => '/^sidebars_widgets\[(.+?)\]$/', + ); + /** * Initial loader. * @@ -78,7 +76,8 @@ final class WP_Customize_Widgets { public function __construct( $manager ) { $this->manager = $manager; - add_action( 'after_setup_theme', array( $this, 'setup_widget_addition_previews' ) ); + add_filter( 'customize_dynamic_setting_args', array( $this, 'filter_customize_dynamic_setting_args' ), 10, 2 ); + add_action( 'after_setup_theme', array( $this, 'register_settings' ) ); add_action( 'wp_loaded', array( $this, 'override_sidebars_widgets_for_theme_switch' ) ); add_action( 'customize_controls_init', array( $this, 'customize_controls_init' ) ); add_action( 'customize_register', array( $this, 'schedule_customize_register' ), 1 ); @@ -94,6 +93,76 @@ final class WP_Customize_Widgets { add_filter( 'dynamic_sidebar_has_widgets', array( $this, 'tally_sidebars_via_dynamic_sidebar_calls' ), 10, 2 ); } + /** + * Get the widget setting type given a setting ID. + * + * @since 4.2.0 + * + * @param $setting_id + * + * @return string|null + */ + protected function get_setting_type( $setting_id ) { + static $cache = array(); + if ( isset( $cache[ $setting_id ] ) ) { + return $cache[ $setting_id ]; + } + foreach ( $this->setting_id_patterns as $type => $pattern ) { + if ( preg_match( $pattern, $setting_id ) ) { + $cache[ $setting_id ] = $type; + return $type; + } + } + return null; + } + + /** + * Inspect the incoming customized data for any widget settings, and dynamically add them up-front so widgets will be initialized properly. + * + * @since 4.2.0 + */ + public function register_settings() { + $widget_setting_ids = array(); + $incoming_setting_ids = array_keys( $this->manager->unsanitized_post_values() ); + foreach ( $incoming_setting_ids as $setting_id ) { + if ( ! is_null( $this->get_setting_type( $setting_id ) ) ) { + $widget_setting_ids[] = $setting_id; + } + } + if ( $this->manager->doing_ajax( 'update-widget' ) && isset( $_REQUEST['widget-id'] ) ) { + $widget_setting_ids[] = $this->get_setting_id( wp_unslash( $_REQUEST['widget-id'] ) ); + } + + $settings = $this->manager->add_dynamic_settings( array_unique( $widget_setting_ids ) ); + + /* + * Preview settings right away so that widgets and sidebars will get registered properly. + * But don't do this if a customize_save because this will cause WP to think there is nothing + * changed that needs to be saved. + */ + if ( ! $this->manager->doing_ajax( 'customize_save' ) ) { + foreach ( $settings as $setting ) { + $setting->preview(); + } + } + } + + /** + * Determine the arguments for a dynamically-created setting. + * + * @since 4.2.0 + * + * @param false|array $args + * @param string $setting_id + * @return false|array + */ + public function filter_customize_dynamic_setting_args( $args, $setting_id ) { + if ( $this->get_setting_type( $setting_id ) ) { + $args = $this->get_setting_args( $setting_id ); + } + return $args; + } + /** * Get an unslashed post value or return a default. * @@ -110,178 +179,7 @@ final class WP_Customize_Widgets { return $default; } - return wp_unslash( $_POST[$name] ); - } - - /** - * Set up widget addition previews. - * - * Since the widgets get registered on 'widgets_init' before the Customizer - * settings are set up on 'customize_register', we have to filter the options - * similarly to how the setting previewer will filter the options later. - * - * @since 3.9.0 - * - * @access public - */ - public function setup_widget_addition_previews() { - $is_customize_preview = false; - - if ( ! empty( $this->manager ) && ! is_admin() && 'on' === $this->get_post_value( 'wp_customize' ) ) { - $is_customize_preview = check_ajax_referer( 'preview-customize_' . $this->manager->get_stylesheet(), 'nonce', false ); - } - - $is_ajax_widget_update = false; - if ( $this->manager->doing_ajax() && 'update-widget' === $this->get_post_value( 'action' ) ) { - $is_ajax_widget_update = check_ajax_referer( 'update-widget', 'nonce', false ); - } - - $is_ajax_customize_save = false; - if ( $this->manager->doing_ajax() && 'customize_save' === $this->get_post_value( 'action' ) ) { - $is_ajax_customize_save = check_ajax_referer( 'save-customize_' . $this->manager->get_stylesheet(), 'nonce', false ); - } - - $is_valid_request = ( $is_ajax_widget_update || $is_customize_preview || $is_ajax_customize_save ); - if ( ! $is_valid_request ) { - return; - } - - // Input from Customizer preview. - if ( isset( $_POST['customized'] ) ) { - $this->_customized = json_decode( $this->get_post_value( 'customized' ), true ); - } else { // Input from ajax widget update request. - $this->_customized = array(); - $id_base = $this->get_post_value( 'id_base' ); - $widget_number = $this->get_post_value( 'widget_number', false ); - $option_name = 'widget_' . $id_base; - $this->_customized[ $option_name ] = array(); - if ( preg_match( '/^[0-9]+$/', $widget_number ) ) { - $option_name .= '[' . $widget_number . ']'; - $this->_customized[ $option_name ][ $widget_number ] = array(); - } - } - - $function = array( $this, 'prepreview_added_sidebars_widgets' ); - - $hook = 'option_sidebars_widgets'; - add_filter( $hook, $function ); - $this->_prepreview_added_filters[] = compact( 'hook', 'function' ); - - $hook = 'default_option_sidebars_widgets'; - add_filter( $hook, $function ); - $this->_prepreview_added_filters[] = compact( 'hook', 'function' ); - - $function = array( $this, 'prepreview_added_widget_instance' ); - foreach ( $this->_customized as $setting_id => $value ) { - if ( preg_match( '/^(widget_.+?)(?:\[(\d+)\])?$/', $setting_id, $matches ) ) { - $option = $matches[1]; - - $hook = sprintf( 'option_%s', $option ); - if ( ! has_filter( $hook, $function ) ) { - add_filter( $hook, $function ); - $this->_prepreview_added_filters[] = compact( 'hook', 'function' ); - } - - $hook = sprintf( 'default_option_%s', $option ); - if ( ! has_filter( $hook, $function ) ) { - add_filter( $hook, $function ); - $this->_prepreview_added_filters[] = compact( 'hook', 'function' ); - } - - /* - * Make sure the option is registered so that the update_option() - * won't fail due to the filters providing a default value, which - * causes the update_option() to get confused. - */ - add_option( $option, array() ); - } - } - } - - /** - * Ensure that newly-added widgets will appear in the widgets_sidebars. - * - * This is necessary because the Customizer's setting preview filters - * are added after the widgets_init action, which is too late for the - * widgets to be set up properly. - * - * @since 3.9.0 - * @access public - * - * @param array $sidebars_widgets Associative array of sidebars and their widgets. - * @return array Filtered array of sidebars and their widgets. - */ - public function prepreview_added_sidebars_widgets( $sidebars_widgets ) { - foreach ( $this->_customized as $setting_id => $value ) { - if ( preg_match( '/^sidebars_widgets\[(.+?)\]$/', $setting_id, $matches ) ) { - $sidebar_id = $matches[1]; - $sidebars_widgets[ $sidebar_id ] = $value; - } - } - return $sidebars_widgets; - } - - /** - * Ensure newly-added widgets have empty instances so they - * will be recognized. - * - * This is necessary because the Customizer's setting preview - * filters are added after the widgets_init action, which is - * too late for the widgets to be set up properly. - * - * @since 3.9.0 - * @access public - * - * @param array|bool|mixed $value Widget instance(s), false if open was empty. - * @return array|mixed Widget instance(s) with additions. - */ - public function prepreview_added_widget_instance( $value = false ) { - if ( ! preg_match( '/^(?:default_)?option_(widget_(.+))/', current_filter(), $matches ) ) { - return $value; - } - $id_base = $matches[2]; - - foreach ( $this->_customized as $setting_id => $setting ) { - $parsed_setting_id = $this->parse_widget_setting_id( $setting_id ); - if ( is_wp_error( $parsed_setting_id ) || $id_base !== $parsed_setting_id['id_base'] ) { - continue; - } - $widget_number = $parsed_setting_id['number']; - - if ( is_null( $widget_number ) ) { - // Single widget. - if ( false === $value ) { - $value = array(); - } - } else { - // Multi widget. - if ( empty( $value ) ) { - $value = array( '_multiwidget' => 1 ); - } - if ( ! isset( $value[ $widget_number ] ) ) { - $value[ $widget_number ] = array(); - } - } - } - - return $value; - } - - /** - * Remove pre-preview filters. - * - * Removes filters added in setup_widget_addition_previews() - * to ensure widgets are populating the options during - * 'widgets_init'. - * - * @since 3.9.0 - * @access public - */ - public function remove_prepreview_filters() { - foreach ( $this->_prepreview_added_filters as $prepreview_added_filter ) { - remove_filter( $prepreview_added_filter['hook'], $prepreview_added_filter['function'] ); - } - $this->_prepreview_added_filters = array(); + return wp_unslash( $_POST[ $name ] ); } /** @@ -380,7 +278,7 @@ final class WP_Customize_Widgets { * @access public */ public function schedule_customize_register() { - if ( is_admin() ) { // @todo for some reason, $wp_customize->is_preview() is true here? + if ( is_admin() ) { $this->customize_register(); } else { add_action( 'wp', array( $this, 'customize_register' ) ); @@ -412,12 +310,9 @@ final class WP_Customize_Widgets { foreach ( array_keys( $wp_registered_widgets ) as $widget_id ) { $setting_id = $this->get_setting_id( $widget_id ); $setting_args = $this->get_setting_args( $setting_id ); - - $setting_args['sanitize_callback'] = array( $this, 'sanitize_widget_instance' ); - $setting_args['sanitize_js_callback'] = array( $this, 'sanitize_widget_js_instance' ); - - $this->manager->add_setting( $setting_id, $setting_args ); - + if ( ! $this->manager->get_setting( $setting_id ) ) { + $this->manager->add_setting( $setting_id, $setting_args ); + } $new_setting_ids[] = $setting_id; } @@ -452,11 +347,9 @@ final class WP_Customize_Widgets { if ( $is_registered_sidebar || $is_inactive_widgets ) { $setting_id = sprintf( 'sidebars_widgets[%s]', $sidebar_id ); $setting_args = $this->get_setting_args( $setting_id ); - - $setting_args['sanitize_callback'] = array( $this, 'sanitize_sidebar_widgets' ); - $setting_args['sanitize_js_callback'] = array( $this, 'sanitize_sidebar_widgets_js_instance' ); - - $this->manager->add_setting( $setting_id, $setting_args ); + if ( ! $this->manager->get_setting( $setting_id ) ) { + $this->manager->add_setting( $setting_id, $setting_args ); + } $new_setting_ids[] = $setting_id; // Add section to contain controls. @@ -523,16 +416,13 @@ final class WP_Customize_Widgets { } } - /* - * We have to register these settings later than customize_preview_init - * so that other filters have had a chance to run. - */ - if ( did_action( 'customize_preview_init' ) ) { + if ( ! $this->manager->doing_ajax( 'customize_save' ) ) { foreach ( $new_setting_ids as $new_setting_id ) { $this->manager->get_setting( $new_setting_id )->preview(); } } - $this->remove_prepreview_filters(); + + add_filter( 'sidebars_widgets', array( $this, 'preview_sidebars_widgets' ), 1 ); } /** @@ -804,6 +694,15 @@ final class WP_Customize_Widgets { 'transport' => 'refresh', 'default' => array(), ); + + if ( preg_match( $this->setting_id_patterns['sidebar_widgets'], $id, $matches ) ) { + $args['sanitize_callback'] = array( $this, 'sanitize_sidebar_widgets' ); + $args['sanitize_js_callback'] = array( $this, 'sanitize_sidebar_widgets_js_instance' ); + } else if ( preg_match( $this->setting_id_patterns['widget_instance'], $id, $matches ) ) { + $args['sanitize_callback'] = array( $this, 'sanitize_widget_instance' ); + $args['sanitize_js_callback'] = array( $this, 'sanitize_widget_js_instance' ); + } + $args = array_merge( $args, $overrides ); /** @@ -831,15 +730,10 @@ final class WP_Customize_Widgets { * @return array Array of sanitized widget IDs. */ public function sanitize_sidebar_widgets( $widget_ids ) { - global $wp_registered_widgets; - - $widget_ids = array_map( 'strval', (array) $widget_ids ); + $widget_ids = array_map( 'strval', (array) $widget_ids ); $sanitized_widget_ids = array(); - foreach ( $widget_ids as $widget_id ) { - if ( array_key_exists( $widget_id, $wp_registered_widgets ) ) { - $sanitized_widget_ids[] = $widget_id; - } + $sanitized_widget_ids[] = preg_replace( '/[^a-z0-9_\-]/', '', $widget_id ); } return $sanitized_widget_ids; } @@ -974,7 +868,6 @@ final class WP_Customize_Widgets { * @access public */ public function customize_preview_init() { - add_filter( 'sidebars_widgets', array( $this, 'preview_sidebars_widgets' ), 1 ); add_action( 'wp_enqueue_scripts', array( $this, 'customize_preview_enqueue' ) ); add_action( 'wp_print_styles', array( $this, 'print_preview_css' ), 1 ); add_action( 'wp_footer', array( $this, 'export_preview_data' ), 20 ); @@ -1315,8 +1208,8 @@ final class WP_Customize_Widgets { // Clean up any input vars that were manually added foreach ( $added_input_vars as $key ) { - unset( $_POST[$key] ); - unset( $_REQUEST[$key] ); + unset( $_POST[ $key ] ); + unset( $_REQUEST[ $key ] ); } // Make sure the expected option was updated. @@ -1333,25 +1226,31 @@ final class WP_Customize_Widgets { } } - // Obtain the widget control with the updated instance in place. - ob_start(); - - $form = $wp_registered_widget_controls[$widget_id]; - if ( $form ) { - call_user_func_array( $form['callback'], $form['params'] ); - } - - $form = ob_get_clean(); - // Obtain the widget instance. - $option = get_option( $option_name ); - + $option = $this->get_captured_option( $option_name ); if ( null !== $parsed_id['number'] ) { - $instance = $option[$parsed_id['number']]; + $instance = $option[ $parsed_id['number'] ]; } else { $instance = $option; } + /* + * Override the incoming $_POST['customized'] for a newly-created widget's + * setting with the new $instance so that the preview filter currently + * in place from WP_Customize_Setting::preview() will use this value + * instead of the default widget instance value (an empty array). + */ + $setting_id = $this->get_setting_id( $widget_id ); + $this->manager->set_post_value( $setting_id, $instance ); + + // Obtain the widget control with the updated instance in place. + ob_start(); + $form = $wp_registered_widget_controls[ $widget_id ]; + if ( $form ) { + call_user_func_array( $form['callback'], $form['params'] ); + } + $form = ob_get_clean(); + $this->stop_capturing_option_updates(); return compact( 'instance', 'form' ); @@ -1383,8 +1282,8 @@ final class WP_Customize_Widgets { wp_die( -1 ); } - if ( ! isset( $_POST['widget-id'] ) ) { - wp_send_json_error(); + if ( empty( $_POST['widget-id'] ) ) { + wp_send_json_error( 'missing_widget-id' ); } /** This action is documented in wp-admin/includes/ajax-actions.php */ @@ -1398,15 +1297,22 @@ final class WP_Customize_Widgets { $widget_id = $this->get_post_value( 'widget-id' ); $parsed_id = $this->parse_widget_id( $widget_id ); - $id_base = $parsed_id['id_base']; + $id_base = $parsed_id['id_base']; - if ( isset( $_POST['widget-' . $id_base] ) && is_array( $_POST['widget-' . $id_base] ) && preg_match( '/__i__|%i%/', key( $_POST['widget-' . $id_base] ) ) ) { - wp_send_json_error(); + $is_updating_widget_template = ( + isset( $_POST[ 'widget-' . $id_base ] ) + && + is_array( $_POST[ 'widget-' . $id_base ] ) + && + preg_match( '/__i__|%i%/', key( $_POST[ 'widget-' . $id_base ] ) ) + ); + if ( $is_updating_widget_template ) { + wp_send_json_error( 'template_widget_not_updatable' ); } $updated_widget = $this->call_widget_update( $widget_id ); // => {instance,form} if ( is_wp_error( $updated_widget ) ) { - wp_send_json_error(); + wp_send_json_error( $updated_widget->get_error_message() ); } $form = $updated_widget['form']; @@ -1462,6 +1368,25 @@ final class WP_Customize_Widgets { return $this->_captured_options; } + /** + * Get the option that was captured from being saved. + * + * @since 4.2.0 + * @access protected + * + * @param string $option_name Option name. + * @param mixed $default Optional. Default value to return if the option does not exist. + * @return mixed Value set for the option. + */ + protected function get_captured_option( $option_name, $default = false ) { + if ( array_key_exists( $option_name, $this->_captured_options ) ) { + $value = $this->_captured_options[ $option_name ]; + } else { + $value = $default; + } + return $value; + } + /** * Get the number of captured widget option updates. * @@ -1496,21 +1421,21 @@ final class WP_Customize_Widgets { * @since 3.9.0 * @access public * - * @param mixed $new_value - * @param string $option_name - * @param mixed $old_value - * @return mixed + * @param mixed $new_value The new option value. + * @param string $option_name Name of the option. + * @param mixed $old_value The old option value. + * @return mixed Filtered option value. */ public function capture_filter_pre_update_option( $new_value, $option_name, $old_value ) { if ( $this->is_option_capture_ignored( $option_name ) ) { return; } - if ( ! isset( $this->_captured_options[$option_name] ) ) { + if ( ! isset( $this->_captured_options[ $option_name ] ) ) { add_filter( "pre_option_{$option_name}", array( $this, 'capture_filter_pre_get_option' ) ); } - $this->_captured_options[$option_name] = $new_value; + $this->_captured_options[ $option_name ] = $new_value; return $old_value; } @@ -1521,14 +1446,14 @@ final class WP_Customize_Widgets { * @since 3.9.0 * @access public * - * @param mixed $value Option - * @return mixed + * @param mixed $value Value to return instead of the option value. + * @return mixed Filtered option value. */ public function capture_filter_pre_get_option( $value ) { $option_name = preg_replace( '/^pre_option_/', '', current_filter() ); - if ( isset( $this->_captured_options[$option_name] ) ) { - $value = $this->_captured_options[$option_name]; + if ( isset( $this->_captured_options[ $option_name ] ) ) { + $value = $this->_captured_options[ $option_name ]; /** This filter is documented in wp-includes/option.php */ $value = apply_filters( 'option_' . $option_name, $value ); @@ -1557,4 +1482,36 @@ final class WP_Customize_Widgets { $this->_captured_options = array(); $this->_is_capturing_option_updates = false; } + + /** + * @since 3.9.0 + * @deprecated 4.2.0 Deprecated in favor of customize_dynamic_setting_args filter. + */ + public function setup_widget_addition_previews() { + _deprecated_function( __METHOD__, '4.2.0' ); + } + + /** + * @since 3.9.0 + * @deprecated 4.2.0 Deprecated in favor of customize_dynamic_setting_args filter. + */ + public function prepreview_added_sidebars_widgets() { + _deprecated_function( __METHOD__, '4.2.0' ); + } + + /** + * @since 3.9.0 + * @deprecated 4.2.0 Deprecated in favor of customize_dynamic_setting_args filter. + */ + public function prepreview_added_widget_instance() { + _deprecated_function( __METHOD__, '4.2.0' ); + } + + /** + * @since 3.9.0 + * @deprecated 4.2.0 Deprecated in favor of customize_dynamic_setting_args filter. + */ + public function remove_prepreview_filters() { + _deprecated_function( __METHOD__, '4.2.0' ); + } } diff --git a/tests/phpunit/tests/customize/manager.php b/tests/phpunit/tests/customize/manager.php index 41d7b78faa..f669d7f92f 100644 --- a/tests/phpunit/tests/customize/manager.php +++ b/tests/phpunit/tests/customize/manager.php @@ -32,7 +32,37 @@ class Tests_WP_Customize_Manager extends WP_UnitTestCase { } /** - * Test WP_Customize_Manager::unsanitized_post_values() + * Test WP_Customize_Manager::doing_ajax(). + * + * @group ajax + */ + function test_doing_ajax() { + if ( ! defined( 'DOING_AJAX' ) ) { + define( 'DOING_AJAX', true ); + } + + $manager = $this->instantiate(); + $this->assertTrue( $manager->doing_ajax() ); + + $_REQUEST['action'] = 'customize_save'; + $this->assertTrue( $manager->doing_ajax( 'customize_save' ) ); + $this->assertFalse( $manager->doing_ajax( 'update-widget' ) ); + } + + /** + * Test ! WP_Customize_Manager::doing_ajax(). + */ + function test_not_doing_ajax() { + if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) { + $this->markTestSkipped( 'Cannot test when DOING_AJAX' ); + } + + $manager = $this->instantiate(); + $this->assertFalse( $manager->doing_ajax() ); + } + + /** + * Test WP_Customize_Manager::unsanitized_post_values(). * * @ticket 30988 */ @@ -49,7 +79,7 @@ class Tests_WP_Customize_Manager extends WP_UnitTestCase { } /** - * Test the WP_Customize_Manager::post_value() method + * Test the WP_Customize_Manager::post_value() method. * * @ticket 30988 */ @@ -71,5 +101,78 @@ class Tests_WP_Customize_Manager extends WP_UnitTestCase { $this->assertEquals( 'post_value_bar_default', $manager->post_value( $bar_setting, 'post_value_bar_default' ), 'Expected post_value($bar_setting, $default) to return $default since no value supplied in $_POST[customized][bar]' ); } -} + /** + * Test the WP_Customize_Manager::add_dynamic_settings() method. + * + * @ticket 30936 + */ + function test_add_dynamic_settings() { + $manager = $this->instantiate(); + $setting_ids = array( 'foo', 'bar' ); + $manager->add_setting( 'foo', array( 'default' => 'foo_default' ) ); + $this->assertEmpty( $manager->get_setting( 'bar' ), 'Expected there to not be a bar setting up front.' ); + $manager->add_dynamic_settings( $setting_ids ); + $this->assertEmpty( $manager->get_setting( 'bar' ), 'Expected the bar setting to remain absent since filters not added.' ); + $this->action_customize_register_for_dynamic_settings(); + $manager->add_dynamic_settings( $setting_ids ); + $this->assertNotEmpty( $manager->get_setting( 'bar' ), 'Expected bar setting to be created since filters were added.' ); + $this->assertEquals( 'foo_default', $manager->get_setting( 'foo' )->default, 'Expected static foo setting to not get overridden by dynamic setting.' ); + $this->assertEquals( 'dynamic_bar_default', $manager->get_setting( 'bar' )->default, 'Expected dynamic setting bar to have default providd by filter.' ); + } + + /** + * Test the WP_Customize_Manager::register_dynamic_settings() method. + * + * This is similar to test_add_dynamic_settings, except the settings are passed via $_POST['customized']. + * + * @ticket 30936 + */ + function test_register_dynamic_settings() { + $posted_settings = array( + 'foo' => 'OOF', + 'bar' => 'RAB', + ); + $_POST['customized'] = wp_slash( wp_json_encode( $posted_settings ) ); + + add_action( 'customize_register', array( $this, 'action_customize_register_for_dynamic_settings' ) ); + + $manager = $this->instantiate(); + $manager->add_setting( 'foo', array( 'default' => 'foo_default' ) ); + + $this->assertEmpty( $manager->get_setting( 'bar' ), 'Expected dynamic setting "bar" to not be registered.' ); + do_action( 'customize_register', $manager ); + $this->assertNotEmpty( $manager->get_setting( 'bar' ), 'Expected dynamic setting "bar" to be automatically registered after customize_register action.' ); + $this->assertEmpty( $manager->get_setting( 'baz' ), 'Expected unrecognized dynamic setting "baz" to remain unregistered.' ); + } + + /** + * In lieu of closures, callback for customize_register action added in test_register_dynamic_settings(). + */ + function action_customize_register_for_dynamic_settings() { + add_filter( 'customize_dynamic_setting_args', array( $this, 'filter_customize_dynamic_setting_args_for_test_dynamic_settings' ), 10, 2 ); + add_filter( 'customize_dynamic_setting_class', array( $this, 'filter_customize_dynamic_setting_class_for_test_dynamic_settings' ), 10, 3 ); + } + + /** + * In lieu of closures, callback for customize_dynamic_setting_args filter added for test_register_dynamic_settings(). + */ + function filter_customize_dynamic_setting_args_for_test_dynamic_settings( $setting_args, $setting_id ) { + $this->assertEquals( false, $setting_args, 'Expected $setting_args to be false by default.' ); + $this->assertInternalType( 'string', $setting_id ); + if ( in_array( $setting_id, array( 'foo', 'bar' ) ) ) { + $setting_args = array( 'default' => "dynamic_{$setting_id}_default" ); + } + return $setting_args; + } + + /** + * In lieu of closures, callback for customize_dynamic_setting_class filter added for test_register_dynamic_settings(). + */ + function filter_customize_dynamic_setting_class_for_test_dynamic_settings( $setting_class, $setting_id, $setting_args ) { + $this->assertEquals( 'WP_Customize_Setting', $setting_class ); + $this->assertInternalType( 'string', $setting_id ); + $this->assertInternalType( 'array', $setting_args ); + return $setting_class; + } +} diff --git a/tests/phpunit/tests/customize/widgets.php b/tests/phpunit/tests/customize/widgets.php new file mode 100644 index 0000000000..a0fd532f88 --- /dev/null +++ b/tests/phpunit/tests/customize/widgets.php @@ -0,0 +1,197 @@ +manager = $GLOBALS['wp_customize']; + + unset( $GLOBALS['_wp_sidebars_widgets'] ); // clear out cache set by wp_get_sidebars_widgets() + $sidebars_widgets = wp_get_sidebars_widgets(); + $this->assertEqualSets( array( 'wp_inactive_widgets', 'sidebar-1' ), array_keys( wp_get_sidebars_widgets() ) ); + $this->assertContains( 'search-2', $sidebars_widgets['sidebar-1'] ); + $this->assertContains( 'categories-2', $sidebars_widgets['sidebar-1'] ); + $this->assertArrayHasKey( 2, get_option( 'widget_search' ) ); + $widget_categories = get_option( 'widget_categories' ); + $this->assertArrayHasKey( 2, $widget_categories ); + $this->assertEquals( '', $widget_categories[2]['title'] ); + + remove_action( 'after_setup_theme', 'twentyfifteen_setup' ); // @todo We should not be including a theme anyway + + $user_id = $this->factory->user->create( array( 'role' => 'administrator' ) ); + wp_set_current_user( $user_id ); + } + + function tearDown() { + parent::tearDown(); + $this->manager = null; + unset( $GLOBALS['wp_customize'] ); + } + + function set_customized_post_data( $customized ) { + $_POST['customized'] = wp_slash( wp_json_encode( $customized ) ); + } + + function do_customize_boot_actions() { + $_SERVER['REQUEST_METHOD'] = 'POST'; + do_action( 'setup_theme' ); + $_REQUEST['nonce'] = wp_create_nonce( 'preview-customize_' . $this->manager->theme()->get_stylesheet() ); + do_action( 'after_setup_theme' ); + do_action( 'init' ); + do_action( 'wp_loaded' ); + do_action( 'wp', $GLOBALS['wp'] ); + } + + /** + * Test WP_Customize_Widgets::__construct() + */ + function test_construct() { + $this->assertInstanceOf( 'WP_Customize_Widgets', $this->manager->widgets ); + $this->assertEquals( $this->manager, $this->manager->widgets->manager ); + } + + /** + * Test WP_Customize_Widgets::register_settings() + * + * @ticket 30988 + */ + function test_register_settings() { + + $raw_widget_customized = array( + 'widget_categories[2]' => array( + 'title' => 'Taxonomies Brand New Value', + 'count' => 0, + 'hierarchical' => 0, + 'dropdown' => 0, + ), + 'widget_search[3]' => array( + 'title' => 'Not as good as Google!', + ), + ); + $customized = array(); + foreach ( $raw_widget_customized as $setting_id => $instance ) { + $customized[ $setting_id ] = $this->manager->widgets->sanitize_widget_js_instance( $instance ); + } + + $this->set_customized_post_data( $customized ); + $this->do_customize_boot_actions(); + $this->assertTrue( is_customize_preview() ); + + $this->assertNotEmpty( $this->manager->get_setting( 'widget_categories[2]' ), 'Expected setting for pre-existing widget category-2, being customized.' ); + $this->assertNotEmpty( $this->manager->get_setting( 'widget_search[2]' ), 'Expected setting for pre-existing widget search-2, not being customized.' ); + $this->assertNotEmpty( $this->manager->get_setting( 'widget_search[3]' ), 'Expected dynamic setting for non-existing widget search-3, being customized.' ); + + $widget_categories = get_option( 'widget_categories' ); + $this->assertEquals( $raw_widget_customized['widget_categories[2]'], $widget_categories[2], 'Expected $wp_customize->get_setting(widget_categories[2])->preview() to have been called.' ); + } + + /** + * Test WP_Customize_Widgets::get_setting_args() + */ + function test_get_setting_args() { + + add_filter( 'widget_customizer_setting_args', array( $this, 'filter_widget_customizer_setting_args' ), 10, 2 ); + + $default_args = array( + 'type' => 'option', + 'capability' => 'edit_theme_options', + 'transport' => 'refresh', + 'default' => array(), + 'sanitize_callback' => array( $this->manager->widgets, 'sanitize_widget_instance' ), + 'sanitize_js_callback' => array( $this->manager->widgets, 'sanitize_widget_js_instance' ), + ); + + $args = $this->manager->widgets->get_setting_args( 'widget_foo[2]' ); + foreach ( $default_args as $key => $default_value ) { + $this->assertEquals( $default_value, $args[ $key ] ); + } + $this->assertEquals( 'WIDGET_FOO[2]', $args['uppercase_id_set_by_filter'] ); + + $override_args = array( + 'type' => 'theme_mod', + 'capability' => 'edit_posts', + 'transport' => 'postMessage', + 'default' => array( 'title' => 'asd' ), + 'sanitize_callback' => '__return_empty_array', + 'sanitize_js_callback' => '__return_empty_array', + ); + $args = $this->manager->widgets->get_setting_args( 'widget_bar[3]', $override_args ); + foreach ( $override_args as $key => $override_value ) { + $this->assertEquals( $override_value, $args[ $key ] ); + } + $this->assertEquals( 'WIDGET_BAR[3]', $args['uppercase_id_set_by_filter'] ); + + $default_args = array( + 'type' => 'option', + 'capability' => 'edit_theme_options', + 'transport' => 'refresh', + 'default' => array(), + 'sanitize_callback' => array( $this->manager->widgets, 'sanitize_sidebar_widgets' ), + 'sanitize_js_callback' => array( $this->manager->widgets, 'sanitize_sidebar_widgets_js_instance' ), + ); + $args = $this->manager->widgets->get_setting_args( 'sidebars_widgets[sidebar-1]' ); + foreach ( $default_args as $key => $default_value ) { + $this->assertEquals( $default_value, $args[ $key ] ); + } + $this->assertEquals( 'SIDEBARS_WIDGETS[SIDEBAR-1]', $args['uppercase_id_set_by_filter'] ); + + $override_args = array( + 'type' => 'theme_mod', + 'capability' => 'edit_posts', + 'transport' => 'postMessage', + 'default' => array( 'title' => 'asd' ), + 'sanitize_callback' => '__return_empty_array', + 'sanitize_js_callback' => '__return_empty_array', + ); + $args = $this->manager->widgets->get_setting_args( 'sidebars_widgets[sidebar-2]', $override_args ); + foreach ( $override_args as $key => $override_value ) { + $this->assertEquals( $override_value, $args[ $key ] ); + } + $this->assertEquals( 'SIDEBARS_WIDGETS[SIDEBAR-2]', $args['uppercase_id_set_by_filter'] ); + } + + function filter_widget_customizer_setting_args( $args, $id ) { + $args['uppercase_id_set_by_filter'] = strtoupper( $id ); + return $args; + } + + /** + * Test WP_Customize_Widgets::sanitize_widget_js_instance() and WP_Customize_Widgets::sanitize_widget_instance() + */ + function test_sanitize_widget_js_instance() { + $this->do_customize_boot_actions(); + + $new_categories_instance = array( + 'title' => 'Taxonomies Brand New Value', + 'count' => '1', + 'hierarchical' => '1', + 'dropdown' => '1', + ); + + $sanitized_for_js = $this->manager->widgets->sanitize_widget_js_instance( $new_categories_instance ); + $this->assertArrayHasKey( 'encoded_serialized_instance', $sanitized_for_js ); + $this->assertTrue( is_serialized( base64_decode( $sanitized_for_js['encoded_serialized_instance'] ), true ) ); + $this->assertEquals( $new_categories_instance['title'], $sanitized_for_js['title'] ); + $this->assertTrue( $sanitized_for_js['is_widget_customizer_js_value'] ); + $this->assertArrayHasKey( 'instance_hash_key', $sanitized_for_js ); + + $corrupted_sanitized_for_js = $sanitized_for_js; + $corrupted_sanitized_for_js['encoded_serialized_instance'] = base64_encode( serialize( array( 'title' => 'EVIL' ) ) ); + $this->assertNull( $this->manager->widgets->sanitize_widget_instance( $corrupted_sanitized_for_js ), 'Expected sanitize_widget_instance to reject corrupted data.' ); + + $unsanitized_from_js = $this->manager->widgets->sanitize_widget_instance( $sanitized_for_js ); + $this->assertEquals( $unsanitized_from_js, $new_categories_instance ); + } +}