WordPress/wp-includes/class-wp-customize-widgets.php
Dominik Schilling cde6d602ea Widget Customizer: Improve plugin compatibility.
Some plugins are using custom scripts and styles for there widgets. These are available on the Widgets screens, but not in the Customizer yet.
Scripts and styles can be enqueued via: 
* `admin_enqueue_scripts`
* `admin_print_scripts` and `admin_print_scripts-widgets.php`
* `admin_print_styles` and `admin_print_styles-widgets.php`
* `admin_print_footer_scripts` and `admin_footer-widgets.php`
All this hooks are now called in the Customizer too.

Previously we have add the `#widgets-right` ID to a container div via jQuery. Remember: `#widgets-right` exists on the Widgets screen and is used by many plugins to do event delegation from that element.
But since our script files are loaded in the footer, the JavaScript way is a bit late for some plugins.
We have decided to add a `div#widgets-right` container element to customizer. "Less hacky hack."

props westonruter, ocean90. Thanks dpe415 for testing.
fixes #27619.
Built from https://develop.svn.wordpress.org/trunk@27907


git-svn-id: http://core.svn.wordpress.org/trunk@27738 1a063a9b-81f0-0310-95a4-ce76da25c4cd
2014-04-02 17:04:14 +00:00

1439 lines
44 KiB
PHP

<?php
/**
* Customize Widgets Class
*
* Implements widget management in the Customizer.
*
* @package WordPress
* @subpackage Customize
* @since 3.9.0
*/
final class WP_Customize_Widgets {
/**
* WP_Customize_Manager instance.
*
* @since 3.9.0
* @access public
* @var WP_Customize_Manager
*/
public $manager;
/**
* All id_bases for widgets defined in core.
*
* @since 3.9.0
* @access protected
* @var array
*/
protected $core_widget_id_bases = array(
'archives', 'calendar', 'categories', 'links', 'meta',
'nav_menu', 'pages', 'recent-comments', 'recent-posts',
'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
* @var array
*/
protected $rendered_sidebars = array();
/**
* @since 3.9.0
* @access protected
* @var array
*/
protected $rendered_widgets = array();
/**
* Initial loader.
*
* @since 3.9.0
* @access public
*
* @param WP_Customize_Manager $manager Customize manager bootstrap instance.
*/
public function __construct( WP_Customize_Manager $manager ) {
$this->manager = $manager;
add_action( 'after_setup_theme', array( $this, 'setup_widget_addition_previews' ) );
add_action( 'customize_controls_init', array( $this, 'customize_controls_init' ) );
add_action( 'customize_register', array( $this, 'schedule_customize_register' ), 1 );
add_action( 'customize_controls_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
add_action( 'customize_controls_print_styles', array( $this, 'print_styles' ) );
add_action( 'customize_controls_print_scripts', array( $this, 'print_scripts' ) );
add_action( 'customize_controls_print_footer_scripts', array( $this, 'print_footer_scripts' ) );
add_action( 'customize_controls_print_footer_scripts', array( $this, 'output_widget_control_templates' ) );
add_action( 'customize_preview_init', array( $this, 'customize_preview_init' ) );
add_action( 'dynamic_sidebar', array( $this, 'tally_rendered_widgets' ) );
add_filter( 'is_active_sidebar', array( $this, 'tally_sidebars_via_is_active_sidebar_calls' ), 10, 2 );
add_filter( 'dynamic_sidebar_has_widgets', array( $this, 'tally_sidebars_via_dynamic_sidebar_calls' ), 10, 2 );
}
/**
* Get an unslashed post value or return a default.
*
* @since 3.9.0
*
* @access protected
*
* @param string $name Post value.
* @param mixed $default Default post value.
* @return mixed Unslashed post value or default value.
*/
protected function get_post_value( $name, $default = null ) {
if ( ! isset( $_POST[ $name ] ) ) {
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
* @global WP_Customize_Manager $wp_customize Customizer instance.
*/
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 ( defined( 'DOING_AJAX' ) && 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 ( defined( 'DOING_AJAX' ) && 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'] ) ) {
$customized = json_decode( $this->get_post_value( 'customized' ), true );
} else { // Input from ajax widget update request.
$customized = array();
$id_base = $this->get_post_value( 'id_base' );
$widget_number = (int) $this->get_post_value( 'widget_number' );
$option_name = 'widget_' . $id_base;
$customized[$option_name] = array();
if ( false !== $widget_number ) {
$option_name .= '[' . $widget_number . ']';
$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' );
foreach ( $customized as $setting_id => $value ) {
if ( preg_match( '/^(widget_.+?)(\[(\d+)\])?$/', $setting_id, $matches ) ) {
/*
* @todo Replace the next two lines with the following once WordPress supports PHP 5.3.
*
* $self = $this; // not needed in PHP 5.4
*
* $function = function ( $value ) use ( $self, $setting_id ) {
* return $self->manager->widgets->prepreview_added_widget_instance( $value, $setting_id );
* };
*/
$body = sprintf( 'global $wp_customize; return $wp_customize->widgets->prepreview_added_widget_instance( $value, %s );', var_export( $setting_id, true ) );
$function = create_function( '$value', $body );
$option = $matches[1];
$hook = sprintf( 'option_%s', $option );
add_filter( $hook, $function );
$this->_prepreview_added_filters[] = compact( 'hook', 'function' );
$hook = sprintf( 'default_option_%s', $option );
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() );
}
}
$this->_customized = $customized;
}
/**
* 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 $instance Widget instance.
* @param string $setting_id Widget setting ID.
* @return array Parsed widget instance.
*/
public function prepreview_added_widget_instance( $instance, $setting_id ) {
if ( isset( $this->_customized[$setting_id] ) ) {
$parsed_setting_id = $this->parse_widget_setting_id( $setting_id );
$widget_number = $parsed_setting_id['number'];
// Single widget.
if ( is_null( $widget_number ) ) {
if ( false === $instance && empty( $value ) ) {
$instance = array();
}
} else if ( false === $instance || ! isset( $instance[$widget_number] ) ) { // Multi widget
if ( empty( $instance ) ) {
$instance = array( '_multiwidget' => 1 );
}
if ( ! isset( $instance[$widget_number] ) ) {
$instance[$widget_number] = array();
}
}
}
return $instance;
}
/**
* 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();
}
/**
* Make sure all widgets get loaded into the Customizer.
*
* Note: these actions are also fired in wp_ajax_update_widget().
*
* @since 3.9.0
* @access public
*/
public function customize_controls_init() {
/** This action is documented in wp-admin/includes/ajax-actions.php */
do_action( 'load-widgets.php' );
/** This action is documented in wp-admin/includes/ajax-actions.php */
do_action( 'widgets.php' );
/** This action is documented in wp-admin/widgets.php */
do_action( 'sidebar_admin_setup' );
}
/**
* Ensure widgets are available for all types of previews.
*
* When in preview, hook to 'customize_register' for settings
* after WordPress is loaded so that all filters have been
* initialized (e.g. Widget Visibility).
*
* @since 3.9.0
* @access public
*/
public function schedule_customize_register() {
if ( is_admin() ) { // @todo for some reason, $wp_customize->is_preview() is true here?
$this->customize_register();
} else {
add_action( 'wp', array( $this, 'customize_register' ) );
}
}
/**
* Register customizer settings and controls for all sidebars and widgets.
*
* @since 3.9.0
* @access public
*/
public function customize_register() {
global $wp_registered_widgets, $wp_registered_widget_controls, $wp_registered_sidebars;
$sidebars_widgets = array_merge(
array( 'wp_inactive_widgets' => array() ),
array_fill_keys( array_keys( $GLOBALS['wp_registered_sidebars'] ), array() ),
wp_get_sidebars_widgets()
);
$new_setting_ids = array();
/*
* Register a setting for all widgets, including those which are active,
* inactive, and orphaned since a widget may get suppressed from a sidebar
* via a plugin (like Widget Visibility).
*/
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 );
$new_setting_ids[] = $setting_id;
}
foreach ( $sidebars_widgets as $sidebar_id => $sidebar_widget_ids ) {
if ( empty( $sidebar_widget_ids ) ) {
$sidebar_widget_ids = array();
}
$is_registered_sidebar = isset( $GLOBALS['wp_registered_sidebars'][$sidebar_id] );
$is_inactive_widgets = ( 'wp_inactive_widgets' === $sidebar_id );
$is_active_sidebar = ( $is_registered_sidebar && ! $is_inactive_widgets );
// Add setting for managing the sidebar's 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 );
$new_setting_ids[] = $setting_id;
// Add section to contain controls.
$section_id = sprintf( 'sidebar-widgets-%s', $sidebar_id );
if ( $is_active_sidebar ) {
$section_args = array(
/* translators: %s: sidebar name */
'title' => sprintf( __( 'Widgets: %s' ), $GLOBALS['wp_registered_sidebars'][$sidebar_id]['name'] ),
'description' => $GLOBALS['wp_registered_sidebars'][$sidebar_id]['description'],
'priority' => 1000 + array_search( $sidebar_id, array_keys( $wp_registered_sidebars ) ),
);
$section_args = apply_filters( 'customizer_widgets_section_args', $section_args, $section_id, $sidebar_id );
$this->manager->add_section( $section_id, $section_args );
$control = new WP_Widget_Area_Customize_Control( $this->manager, $setting_id, array(
'section' => $section_id,
'sidebar_id' => $sidebar_id,
'priority' => count( $sidebar_widget_ids ), // place 'Add Widget' and 'Reorder' buttons at end.
) );
$new_setting_ids[] = $setting_id;
$this->manager->add_control( $control );
}
}
// Add a control for each active widget (located in a sidebar).
foreach ( $sidebar_widget_ids as $i => $widget_id ) {
// Skip widgets that may have gone away due to a plugin being deactivated.
if ( ! $is_active_sidebar || ! isset( $GLOBALS['wp_registered_widgets'][$widget_id] ) ) {
continue;
}
$registered_widget = $GLOBALS['wp_registered_widgets'][$widget_id];
$setting_id = $this->get_setting_id( $widget_id );
$id_base = $GLOBALS['wp_registered_widget_controls'][$widget_id]['id_base'];
assert( false !== is_active_widget( $registered_widget['callback'], $registered_widget['id'], false, false ) );
$control = new WP_Widget_Form_Customize_Control( $this->manager, $setting_id, array(
'label' => $registered_widget['name'],
'section' => $section_id,
'sidebar_id' => $sidebar_id,
'widget_id' => $widget_id,
'widget_id_base' => $id_base,
'priority' => $i,
'width' => $wp_registered_widget_controls[$widget_id]['width'],
'height' => $wp_registered_widget_controls[$widget_id]['height'],
'is_wide' => $this->is_wide_widget( $widget_id ),
) );
$this->manager->add_control( $control );
}
}
/*
* 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' ) ) {
foreach ( $new_setting_ids as $new_setting_id ) {
$this->manager->get_setting( $new_setting_id )->preview();
}
}
$this->remove_prepreview_filters();
}
/**
* Covert a widget_id into its corresponding customizer setting ID (option name).
*
* @since 3.9.0
* @access public
*
* @param string $widget_id Widget ID.
* @return string Maybe-parsed widget ID.
*/
public function get_setting_id( $widget_id ) {
$parsed_widget_id = $this->parse_widget_id( $widget_id );
$setting_id = sprintf( 'widget_%s', $parsed_widget_id['id_base'] );
if ( ! is_null( $parsed_widget_id['number'] ) ) {
$setting_id .= sprintf( '[%d]', $parsed_widget_id['number'] );
}
return $setting_id;
}
/**
* Determine whether the widget is considered "wide".
*
* Core widgets which may have controls wider than 250, but can
* still be shown in the narrow customizer panel. The RSS and Text
* widgets in Core, for example, have widths of 400 and yet they
* still render fine in the customizer panel. This method will
* return all Core widgets as being not wide, but this can be
* overridden with the is_wide_widget_in_customizer filter.
*
* @since 3.9.0
* @access public
*
* @param string $widget_id Widget ID.
* @return bool Whether or not the widget is a "wide" widget.
*/
public function is_wide_widget( $widget_id ) {
global $wp_registered_widget_controls;
$parsed_widget_id = $this->parse_widget_id( $widget_id );
$width = $wp_registered_widget_controls[$widget_id]['width'];
$is_core = in_array( $parsed_widget_id['id_base'], $this->core_widget_id_bases );
$is_wide = ( $width > 250 && ! $is_core );
/**
* Filter whether the given widget is considered "wide".
*
* @since 3.9.0
*
* @param bool $is_wide Whether the widget is wide, Default false.
* @param string $widget_id Widget ID.
*/
return apply_filters( 'is_wide_widget_in_customizer', $is_wide, $widget_id );
}
/**
* Covert a widget ID into its id_base and number components.
*
* @since 3.9.0
* @access public
*
* @param string $widget_id Widget ID.
* @return array Array containing a widget's id_base and number components.
*/
public function parse_widget_id( $widget_id ) {
$parsed = array(
'number' => null,
'id_base' => null,
);
if ( preg_match( '/^(.+)-(\d+)$/', $widget_id, $matches ) ) {
$parsed['id_base'] = $matches[1];
$parsed['number'] = intval( $matches[2] );
} else {
// likely an old single widget
$parsed['id_base'] = $widget_id;
}
return $parsed;
}
/**
* Convert a widget setting ID (option path) to its id_base and number components.
*
* @since 3.9.0
* @access public
*
* @param string $setting_id Widget setting ID.
* @return WP_Error|array Array containing a widget's id_base and number components,
* or a WP_Error object.
*/
public function parse_widget_setting_id( $setting_id ) {
if ( ! preg_match( '/^(widget_(.+?))(?:\[(\d+)\])?$/', $setting_id, $matches ) ) {
return new WP_Error( 'widget_setting_invalid_id' );
}
$id_base = $matches[2];
$number = isset( $matches[3] ) ? intval( $matches[3] ) : null;
return compact( 'id_base', 'number' );
}
/**
* Call admin_print_styles-widgets.php and admin_print_styles hooks to
* allow custom styles from plugins.
*
* @since 3.9.0
* @access public
*/
public function print_styles() {
/** This action is documented in wp-admin/admin-header.php */
do_action( 'admin_print_styles-widgets.php' );
/** This action is documented in wp-admin/admin-header.php */
do_action( 'admin_print_styles' );
}
/**
* Call admin_print_scripts-widgets.php and admin_print_scripts hooks to
* allow custom scripts from plugins.
*
* @since 3.9.0
* @access public
*/
public function print_scripts() {
/** This action is documented in wp-admin/admin-header.php */
do_action( 'admin_print_scripts-widgets.php' );
/** This action is documented in wp-admin/admin-header.php */
do_action( 'admin_print_scripts' );
}
/**
* Enqueue scripts and styles for customizer panel and export data to JavaScript.
*
* @since 3.9.0
* @access public
*/
public function enqueue_scripts() {
wp_enqueue_style( 'customize-widgets' );
wp_enqueue_script( 'customize-widgets' );
/** This action is documented in wp-admin/admin-header.php */
do_action( 'admin_enqueue_scripts', 'widgets.php' );
/*
* Export available widgets with control_tpl removed from model
* since plugins need templates to be in the DOM.
*/
$available_widgets = array();
foreach ( $this->get_available_widgets() as $available_widget ) {
unset( $available_widget['control_tpl'] );
$available_widgets[] = $available_widget;
}
$widget_reorder_nav_tpl = sprintf(
'<div class="widget-reorder-nav"><span class="move-widget" tabindex="0">%1$s</span><span class="move-widget-down" tabindex="0">%2$s</span><span class="move-widget-up" tabindex="0">%3$s</span></div>',
__( 'Move to another area&hellip;' ),
__( 'Move down' ),
__( 'Move up' )
);
$move_widget_area_tpl = str_replace(
array( '{description}', '{btn}' ),
array(
( 'Select an area to move this widget into:' ), // @todo translate
esc_html_x( 'Move', 'move widget' ),
),
'
<div class="move-widget-area">
<p class="description">{description}</p>
<ul class="widget-area-select">
<% _.each( sidebars, function ( sidebar ){ %>
<li class="" data-id="<%- sidebar.id %>" title="<%- sidebar.description %>" tabindex="0"><%- sidebar.name %></li>
<% }); %>
</ul>
<div class="move-widget-actions">
<button class="move-widget-btn button-secondary" type="button">{btn}</button>
</div>
</div>
'
);
/*
* Why not wp_localize_script? Because we're not localizing,
* and it forces values into strings.
*/
global $wp_scripts;
$exports = array(
'nonce' => wp_create_nonce( 'update-widget' ),
'registered_sidebars' => array_values( $GLOBALS['wp_registered_sidebars'] ),
'registered_widgets' => $GLOBALS['wp_registered_widgets'],
'available_widgets' => $available_widgets, // @todo Merge this with registered_widgets
'i18n' => array(
'save_btn_label' => __( 'Apply' ),
// @todo translate? do we want these tooltips?
'save_btn_tooltip' => ( 'Save and preview changes before publishing them.' ),
'remove_btn_label' => __( 'Remove' ),
'remove_btn_tooltip' => ( 'Trash widget by moving it to the inactive widgets sidebar.' ),
'error' => __( 'An error has occurred. Please reload the page and try again.' ),
),
'tpl' => array(
'widget_reorder_nav' => $widget_reorder_nav_tpl,
'move_widget_area' => $move_widget_area_tpl,
),
);
foreach ( $exports['registered_widgets'] as &$registered_widget ) {
unset( $registered_widget['callback'] ); // may not be JSON-serializeable
}
$wp_scripts->add_data(
'customize-widgets',
'data',
sprintf( 'var WidgetCustomizer_exports = %s;', json_encode( $exports ) )
);
}
/**
* Render the widget form control templates into the DOM.
*
* @since 3.9.0
* @access public
*/
public function output_widget_control_templates() {
?>
<div id="widgets-left"><!-- compatibility with JS which looks for widget templates here -->
<div id="available-widgets">
<div id="available-widgets-filter">
<label class="screen-reader-text" for="widgets-search"><?php _e( 'Find Widgets' ); ?></label>
<input type="search" id="widgets-search" placeholder="<?php esc_attr_e( 'Find widgets&hellip;' ) ?>" />
</div>
<?php foreach ( $this->get_available_widgets() as $available_widget ): ?>
<div id="widget-tpl-<?php echo esc_attr( $available_widget['id'] ) ?>" data-widget-id="<?php echo esc_attr( $available_widget['id'] ) ?>" class="widget-tpl <?php echo esc_attr( $available_widget['id'] ) ?>" tabindex="0">
<?php echo $available_widget['control_tpl']; // xss ok ?>
</div>
<?php endforeach; ?>
</div><!-- #available-widgets -->
</div><!-- #widgets-left -->
<?php
}
/**
* Call admin_print_footer_scripts and admin_print_scripts hooks to
* allow custom scripts from plugins.
*
* @since 3.9.0
* @access public
*/
public function print_footer_scripts() {
/** This action is documented in wp-admin/admin-footer.php */
do_action( 'admin_print_footer_scripts' );
/** This action is documented in wp-admin/admin-footer.php */
do_action( 'admin_footer-widgets.php' );
}
/**
* Get common arguments to supply when constructing a Customizer setting.
*
* @since 3.9.0
* @access public
*
* @param string $id Widget setting ID.
* @param array $overrides Array of setting overrides.
* @return array Possibly modified setting arguments.
*/
public function get_setting_args( $id, $overrides = array() ) {
$args = array(
'type' => 'option',
'capability' => 'edit_theme_options',
'transport' => 'refresh',
'default' => array(),
);
$args = array_merge( $args, $overrides );
/**
* Filter the common arguments supplied when constructing a Customizer setting.
*
* @since 3.9.0
*
* @see WP_Customize_Setting
*
* @param array $args Array of Customizer setting arguments.
* @param string $id Widget setting ID.
*/
return apply_filters( 'widget_customizer_setting_args', $args, $id );
}
/**
* Make sure that sidebar widget arrays only ever contain widget IDS.
*
* Used as the 'sanitize_callback' for each $sidebars_widgets setting.
*
* @since 3.9.0
* @access public
*
* @param array $widget_ids Array of widget IDs.
* @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 );
$sanitized_widget_ids = array();
foreach ( $widget_ids as $widget_id ) {
if ( array_key_exists( $widget_id, $wp_registered_widgets ) ) {
$sanitized_widget_ids[] = $widget_id;
}
}
return $sanitized_widget_ids;
}
/**
* Build up an index of all available widgets for use in Backbone models.
*
* @since 3.9.0
* @access public
*
* @see wp_list_widgets()
*
* @return array List of available widgets.
*/
public function get_available_widgets() {
static $available_widgets = array();
if ( ! empty( $available_widgets ) ) {
return $available_widgets;
}
global $wp_registered_widgets, $wp_registered_widget_controls;
require_once ABSPATH . '/wp-admin/includes/widgets.php'; // for next_widget_id_number()
$sort = $wp_registered_widgets;
usort( $sort, array( $this, '_sort_name_callback' ) );
$done = array();
foreach ( $sort as $widget ) {
if ( in_array( $widget['callback'], $done, true ) ) { // We already showed this multi-widget
continue;
}
$sidebar = is_active_widget( $widget['callback'], $widget['id'], false, false );
$done[] = $widget['callback'];
if ( ! isset( $widget['params'][0] ) ) {
$widget['params'][0] = array();
}
$available_widget = $widget;
unset( $available_widget['callback'] ); // not serializable to JSON
$args = array(
'widget_id' => $widget['id'],
'widget_name' => $widget['name'],
'_display' => 'template',
);
$is_disabled = false;
$is_multi_widget = ( isset( $wp_registered_widget_controls[$widget['id']]['id_base'] ) && isset( $widget['params'][0]['number'] ) );
if ( $is_multi_widget ) {
$id_base = $wp_registered_widget_controls[$widget['id']]['id_base'];
$args['_temp_id'] = "$id_base-__i__";
$args['_multi_num'] = next_widget_id_number( $id_base );
$args['_add'] = 'multi';
} else {
$args['_add'] = 'single';
if ( $sidebar && 'wp_inactive_widgets' !== $sidebar ) {
$is_disabled = true;
}
$id_base = $widget['id'];
}
$list_widget_controls_args = wp_list_widget_controls_dynamic_sidebar( array( 0 => $args, 1 => $widget['params'][0] ) );
$control_tpl = $this->get_widget_control( $list_widget_controls_args );
// The properties here are mapped to the Backbone Widget model.
$available_widget = array_merge( $available_widget, array(
'temp_id' => isset( $args['_temp_id'] ) ? $args['_temp_id'] : null,
'is_multi' => $is_multi_widget,
'control_tpl' => $control_tpl,
'multi_number' => ( $args['_add'] === 'multi' ) ? $args['_multi_num'] : false,
'is_disabled' => $is_disabled,
'id_base' => $id_base,
'transport' => 'refresh',
'width' => $wp_registered_widget_controls[$widget['id']]['width'],
'height' => $wp_registered_widget_controls[$widget['id']]['height'],
'is_wide' => $this->is_wide_widget( $widget['id'] ),
) );
$available_widgets[] = $available_widget;
}
return $available_widgets;
}
/**
* Naturally order available widgets by name.
*
* @since 3.9.0
* @static
* @access protected
*
* @param array $widget_a The first widget to compare.
* @param array $widget_b The second widget to compare.
* @return int Reorder position for the current widget comparison.
*/
protected function _sort_name_callback( $widget_a, $widget_b ) {
return strnatcasecmp( $widget_a['name'], $widget_b['name'] );
}
/**
* Get the widget control markup.
*
* @since 3.9.0
* @access public
*
* @param array $args Widget control arguments.
* @return string Widget control form HTML markup.
*/
public function get_widget_control( $args ) {
ob_start();
call_user_func_array( 'wp_widget_control', $args );
$replacements = array(
'<form action="" method="post">' => '<div class="form">',
'</form>' => '</div><!-- .form -->',
);
$control_tpl = ob_get_clean();
$control_tpl = str_replace( array_keys( $replacements ), array_values( $replacements ), $control_tpl );
return $control_tpl;
}
/**
* Add hooks for the customizer preview.
*
* @since 3.9.0
* @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, 'inject_preview_css' ), 1 );
add_action( 'wp_footer', array( $this, 'export_preview_data' ), 20 );
}
/**
* When previewing, make sure the proper previewing widgets are used.
*
* Because wp_get_sidebars_widgets() gets called early at init
* (via wp_convert_widget_settings()) and can set global variable
* $_wp_sidebars_widgets to the value of get_option( 'sidebars_widgets' )
* before the customizer preview filter is added, we have to reset
* it after the filter has been added.
*
* @since 3.9.0
* @access public
*
* @param array $sidebars_widgets List of widgets for the current sidebar.
*/
public function preview_sidebars_widgets( $sidebars_widgets ) {
$sidebars_widgets = get_option( 'sidebars_widgets' );
unset( $sidebars_widgets['array_version'] );
return $sidebars_widgets;
}
/**
* Enqueue scripts for the Customizer preview.
*
* @since 3.9.0
* @access public
*/
public function customize_preview_enqueue() {
wp_enqueue_script( 'customize-preview-widgets' );
}
/**
* Insert default style for highlighted widget at early point so theme
* stylesheet can override.
*
* @since 3.9.0
* @access public
*
* @action wp_print_styles
*/
public function inject_preview_css() {
?>
<style>
.widget-customizer-highlighted-widget {
outline: none;
-webkit-box-shadow: 0 0 2px rgba(30,140,190,0.8);
box-shadow: 0 0 2px rgba(30,140,190,0.8);
position: relative;
z-index: 1;
}
</style>
<?php
}
/**
* At the very end of the page, at the very end of the wp_footer,
* communicate the sidebars that appeared on the page.
*
* @since 3.9.0
* @access public
*/
public function export_preview_data() {
// Prepare customizer settings to pass to Javascript.
$settings = array(
'renderedSidebars' => array_fill_keys( array_unique( $this->rendered_sidebars ), true ),
'renderedWidgets' => array_fill_keys( array_keys( $this->rendered_widgets ), true ),
'registeredSidebars' => array_values( $GLOBALS['wp_registered_sidebars'] ),
'registeredWidgets' => $GLOBALS['wp_registered_widgets'],
'l10n' => array(
'widgetTooltip' => ( 'Shift-click to edit this widget.' ),
),
);
foreach ( $settings['registeredWidgets'] as &$registered_widget ) {
unset( $registered_widget['callback'] ); // may not be JSON-serializeable
}
?>
<script type="text/javascript">
var _wpWidgetCustomizerPreviewSettings = <?php echo json_encode( $settings ); ?>;
</script>
<?php
}
/**
* Keep track of the widgets that were rendered.
*
* @since 3.9.0
* @access public
*
* @param array $widget Rendered widget to tally.
*/
public function tally_rendered_widgets( $widget ) {
$this->rendered_widgets[$widget['id']] = true;
}
/**
* Tally the sidebars rendered via is_active_sidebar().
*
* Keep track of the times that is_active_sidebar() is called
* in the template, and assume that this means that the sidebar
* would be rendered on the template if there were widgets
* populating it.
*
* @since 3.9.0
* @access public
*
* @param bool $is_active Whether the sidebar is active.
* @pasram string $sidebar_id Sidebar ID.
*/
public function tally_sidebars_via_is_active_sidebar_calls( $is_active, $sidebar_id ) {
if ( isset( $GLOBALS['wp_registered_sidebars'][$sidebar_id] ) ) {
$this->rendered_sidebars[] = $sidebar_id;
}
/*
* We may need to force this to true, and also force-true the value
* for 'dynamic_sidebar_has_widgets' if we want to ensure that there
* is an area to drop widgets into, if the sidebar is empty.
*/
return $is_active;
}
/**
* Tally the sidebars rendered via dynamic_sidebar().
*
* Keep track of the times that dynamic_sidebar() is called in the template,
* and assume this means the sidebar would be rendered on the template if
* there were widgets populating it.
*
* @since 3.9.0
* @access public
*
* @param bool $has_widgets Whether the current sidebar has widgets.
* @param string $sidebar_id Sidebar ID.
*/
public function tally_sidebars_via_dynamic_sidebar_calls( $has_widgets, $sidebar_id ) {
if ( isset( $GLOBALS['wp_registered_sidebars'][$sidebar_id] ) ) {
$this->rendered_sidebars[] = $sidebar_id;
}
/*
* We may need to force this to true, and also force-true the value
* for 'is_active_sidebar' if we want to ensure there is an area to
* drop widgets into, if the sidebar is empty.
*/
return $has_widgets;
}
/**
* Get a widget instance's hash key.
*
* Serialize an instance and hash it with the AUTH_KEY; when a JS value is
* posted back to save, this instance hash key is used to ensure that the
* serialized_instance was not tampered with, but that it had originated
* from WordPress and so is sanitized.
*
* @since 3.9.0
* @access protected
*
* @param array $instance Widget instance.
* @return string Widget instance's hash key.
*/
protected function get_instance_hash_key( $instance ) {
$hash = md5( AUTH_KEY . serialize( $instance ) );
return $hash;
}
/**
* Sanitize a widget instance.
*
* Unserialize the JS-instance for storing in the options. It's important
* that this filter only get applied to an instance once.
*
* @since 3.9.0
* @access public
*
* @param array $value Widget instance to sanitize.
* @return array Sanitized widget instance.
*/
public function sanitize_widget_instance( $value ) {
if ( $value === array() ) {
return $value;
}
if ( empty( $value['is_widget_customizer_js_value'] )
|| empty( $value['instance_hash_key'] )
|| empty( $value['encoded_serialized_instance'] ) )
{
return null;
}
$decoded = base64_decode( $value['encoded_serialized_instance'], true );
if ( false === $decoded ) {
return null;
}
$instance = unserialize( $decoded );
if ( false === $instance ) {
return null;
}
if ( $this->get_instance_hash_key( $instance ) !== $value['instance_hash_key'] ) {
return null;
}
return $instance;
}
/**
* Convert widget instance into JSON-representable format.
*
* @since 3.9.0
* @access public
*
* @param array $value Widget instance to convert to JSON.
* @return array JSON-converted widget instance.
*/
public function sanitize_widget_js_instance( $value ) {
if ( empty( $value['is_widget_customizer_js_value'] ) ) {
$serialized = serialize( $value );
$value = array(
'encoded_serialized_instance' => base64_encode( $serialized ),
'title' => empty( $value['title'] ) ? '' : $value['title'],
'is_widget_customizer_js_value' => true,
'instance_hash_key' => $this->get_instance_hash_key( $value ),
);
}
return $value;
}
/**
* Strip out widget IDs for widgets which are no longer registered.
*
* One example where this might happen is when a plugin orphans a widget
* in a sidebar upon deactivation.
*
* @since 3.9.0
* @access public
*
* @param array $widget_ids List of widget IDs.
* @return array Parsed list of widget IDs.
*/
public function sanitize_sidebar_widgets_js_instance( $widget_ids ) {
global $wp_registered_widgets;
$widget_ids = array_values( array_intersect( $widget_ids, array_keys( $wp_registered_widgets ) ) );
return $widget_ids;
}
/**
* Find and invoke the widget update and control callbacks.
*
* Requires that $_POST be populated with the instance data.
*
* @since 3.9.0
* @access public
*
* @param string $widget_id Widget ID.
* @return WP_Error|array Array containing the updated widget information.
* A WP_Error object, otherwise.
*/
public function call_widget_update( $widget_id ) {
global $wp_registered_widget_updates, $wp_registered_widget_controls;
$this->start_capturing_option_updates();
$parsed_id = $this->parse_widget_id( $widget_id );
$option_name = 'widget_' . $parsed_id['id_base'];
/*
* If a previously-sanitized instance is provided, populate the input vars
* with its values so that the widget update callback will read this instance
*/
$added_input_vars = array();
if ( ! empty( $_POST['sanitized_widget_setting'] ) ) {
$sanitized_widget_setting = json_decode( $this->get_post_value( 'sanitized_widget_setting' ), true );
if ( empty( $sanitized_widget_setting ) ) {
$this->stop_capturing_option_updates();
return new WP_Error( 'widget_setting_malformed' );
}
$instance = $this->sanitize_widget_instance( $sanitized_widget_setting );
if ( is_null( $instance ) ) {
$this->stop_capturing_option_updates();
return new WP_Error( 'widget_setting_unsanitized' );
}
if ( ! is_null( $parsed_id['number'] ) ) {
$value = array();
$value[$parsed_id['number']] = $instance;
$key = 'widget-' . $parsed_id['id_base'];
$_REQUEST[$key] = $_POST[$key] = wp_slash( $value );
$added_input_vars[] = $key;
} else {
foreach ( $instance as $key => $value ) {
$_REQUEST[$key] = $_POST[$key] = wp_slash( $value );
$added_input_vars[] = $key;
}
}
}
// Invoke the widget update callback.
foreach ( (array) $wp_registered_widget_updates as $name => $control ) {
if ( $name === $parsed_id['id_base'] && is_callable( $control['callback'] ) ) {
ob_start();
call_user_func_array( $control['callback'], $control['params'] );
ob_end_clean();
break;
}
}
// Clean up any input vars that were manually added
foreach ( $added_input_vars as $key ) {
unset( $_POST[$key] );
unset( $_REQUEST[$key] );
}
// Make sure the expected option was updated.
if ( 0 !== $this->count_captured_options() ) {
if ( $this->count_captured_options() > 1 ) {
$this->stop_capturing_option_updates();
return new WP_Error( 'widget_setting_too_many_options' );
}
$updated_option_name = key( $this->get_captured_options() );
if ( $updated_option_name !== $option_name ) {
$this->stop_capturing_option_updates();
return new WP_Error( 'widget_setting_unexpected_option' );
}
}
// 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 );
if ( null !== $parsed_id['number'] ) {
$instance = $option[$parsed_id['number']];
} else {
$instance = $option;
}
$this->stop_capturing_option_updates();
return compact( 'instance', 'form' );
}
/**
* Update widget settings asynchronously.
*
* Allows the Customizer to update a widget using its form, but return the new
* instance info via Ajax instead of saving it to the options table.
*
* Most code here copied from wp_ajax_save_widget()
*
* @since 3.9.0
* @access public
*
* @see wp_ajax_save_widget()
*
* @todo Reuse wp_ajax_save_widget now that we have option transactions?
*/
public function wp_ajax_update_widget() {
if ( ! is_user_logged_in() ) {
wp_die( 0 );
}
check_ajax_referer( 'update-widget', 'nonce' );
if ( ! current_user_can( 'edit_theme_options' ) ) {
wp_die( -1 );
}
if ( ! isset( $_POST['widget-id'] ) ) {
wp_send_json_error();
}
/** This action is documented in wp-admin/includes/ajax-actions.php */
do_action( 'load-widgets.php' );
/** This action is documented in wp-admin/includes/ajax-actions.php */
do_action( 'widgets.php' );
/** This action is documented in wp-admin/widgets.php */
do_action( 'sidebar_admin_setup' );
$widget_id = $this->get_post_value( 'widget-id' );
$parsed_id = $this->parse_widget_id( $widget_id );
$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();
}
$updated_widget = $this->call_widget_update( $widget_id ); // => {instance,form}
if ( is_wp_error( $updated_widget ) ) {
wp_send_json_error();
}
$form = $updated_widget['form'];
$instance = $this->sanitize_widget_js_instance( $updated_widget['instance'] );
wp_send_json_success( compact( 'form', 'instance' ) );
}
/***************************************************************************
* Option Update Capturing
***************************************************************************/
/**
* List of captured widget option updates.
*
* @since 3.9.0
* @access protected
* @var array $_captured_options Values updated while option capture is happening.
*/
protected $_captured_options = array();
/**
* Whether option capture is currently happening.
*
* @since 3.9.0
* @access protected
* @var bool $_is_current Whether option capture is currently happening or not.
*/
protected $_is_capturing_option_updates = false;
/**
* Determine whether the captured option update should be ignored.
*
* @since 3.9.0
* @access protected
*
* @param string $option_name Option name.
* @return boolean Whether the option capture is ignored.
*/
protected function is_option_capture_ignored( $option_name ) {
return ( 0 === strpos( $option_name, '_transient_' ) );
}
/**
* Retrieve captured widget option updates.
*
* @since 3.9.0
* @access protected
*
* @return array Array of captured options.
*/
protected function get_captured_options() {
return $this->_captured_options;
}
/**
* Get the number of captured widget option updates.
*
* @since 3.9.0
* @access protected
*
* @return int Number of updated options.
*/
protected function count_captured_options() {
return count( $this->_captured_options );
}
/**
* Start keeping track of changes to widget options, caching new values.
*
* @since 3.9.0
* @access protected
*/
protected function start_capturing_option_updates() {
if ( $this->_is_capturing_option_updates ) {
return;
}
$this->_is_capturing_option_updates = true;
add_filter( 'pre_update_option', array( $this, '_capture_filter_pre_update_option' ), 10, 3 );
}
/**
* Pre-filter captured option values before updating.
*
* @since 3.9.0
* @access public
*
* @param mixed $new_value
* @param string $option_name
* @param mixed $old_value
* @return mixed
*/
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] ) ) {
add_filter( "pre_option_{$option_name}", array( $this, '_capture_filter_pre_get_option' ) );
}
$this->_captured_options[$option_name] = $new_value;
return $old_value;
}
/**
* Pre-filter captured option values before retrieving.
*
* @since 3.9.0
* @access public
*
* @param mixed $value Option
* @return mixed
*/
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];
$value = apply_filters( 'option_' . $option_name, $value );
}
return $value;
}
/**
* Undo any changes to the options since options capture began.
*
* @since 3.9.0
* @access protected
*/
protected function stop_capturing_option_updates() {
if ( ! $this->_is_capturing_option_updates ) {
return;
}
remove_filter( '_capture_filter_pre_update_option', array( $this, '_capture_filter_pre_update_option' ), 10, 3 );
foreach ( array_keys( $this->_captured_options ) as $option_name ) {
remove_filter( "pre_option_{$option_name}", array( $this, '_capture_filter_pre_get_option' ) );
}
$this->_captured_options = array();
$this->_is_capturing_option_updates = false;
}
}