2014-03-05 21:41:14 +01:00
< ? php
/**
* Widget customizer manager class .
*/
class WP_Customize_Widgets {
const UPDATE_WIDGET_AJAX_ACTION = 'update-widget' ;
const UPDATE_WIDGET_NONCE_POST_KEY = 'update-sidebar-widgets-nonce' ;
/**
* All id_bases for widgets defined in core
*
* @ var array
*/
protected static $core_widget_id_bases = array (
'archives' ,
'calendar' ,
'categories' ,
'links' ,
'meta' ,
'nav_menu' ,
'pages' ,
'recent-comments' ,
'recent-posts' ,
'rss' ,
'search' ,
'tag_cloud' ,
'text' ,
);
/**
* Initial loader .
*/
static function setup () {
add_action ( 'after_setup_theme' , array ( __CLASS__ , 'setup_widget_addition_previews' ) );
add_action ( 'customize_controls_init' , array ( __CLASS__ , 'customize_controls_init' ) );
add_action ( 'customize_register' , array ( __CLASS__ , 'schedule_customize_register' ), 1 );
add_action ( 'customize_controls_enqueue_scripts' , array ( __CLASS__ , 'customize_controls_enqueue_deps' ) );
add_action ( 'customize_controls_print_footer_scripts' , array ( __CLASS__ , 'output_widget_control_templates' ) );
add_action ( 'customize_preview_init' , array ( __CLASS__ , 'customize_preview_init' ) );
add_action ( 'dynamic_sidebar' , array ( __CLASS__ , 'tally_rendered_widgets' ) );
add_action ( 'dynamic_sidebar' , array ( __CLASS__ , 'tally_sidebars_via_dynamic_sidebar_actions' ) );
add_filter ( 'temp_is_active_sidebar' , array ( __CLASS__ , 'tally_sidebars_via_is_active_sidebar_calls' ), 10 , 2 );
add_filter ( 'temp_dynamic_sidebar_has_widgets' , array ( __CLASS__ , 'tally_sidebars_via_dynamic_sidebar_calls' ), 10 , 2 );
/**
* Special filter for Settings Revisions plugin until it can handle
* dynamically creating settings . Normally this should be handled by
* a setting ' s sanitize_js_callback , but when restoring an old revision
* it may include settings which do not currently exist , and so they
* do not have the opportunity to be sanitized as needed . Furthermore ,
* we have to add this filter here because the customizer is not
* initialized in WP Ajax , which is where Settings Revisions currently
* needs to apply this filter at times .
*/
add_filter ( 'temp_customize_sanitize_js' , array ( __CLASS__ , 'temp_customize_sanitize_js' ), 10 , 2 );
}
/**
* Get an unslashed post value , or return a default
*
* @ param string $name
* @ param mixed $default
* @ return mixed
*/
static function get_post_value ( $name , $default = null ) {
if ( ! isset ( $_POST [ $name ] ) ) {
return $default ;
}
return wp_unslash ( $_POST [ $name ] );
}
protected static $_customized ;
protected static $_prepreview_added_filters = array ();
/**
* Since the widgets get registered ( widgets_init ) before the customizer settings are set up ( customize_register ),
* we have to filter the options similarly to how the setting previewer will filter the options later .
*
* @ action after_setup_theme
*/
static function setup_widget_addition_previews () {
global $wp_customize ;
$is_customize_preview = (
( ! empty ( $wp_customize ) )
&&
( ! is_admin () )
&&
( 'on' === self :: get_post_value ( 'wp_customize' ) )
&&
check_ajax_referer ( 'preview-customize_' . $wp_customize -> get_stylesheet (), 'nonce' , false )
);
$is_ajax_widget_update = (
( defined ( 'DOING_AJAX' ) && DOING_AJAX )
&&
self :: get_post_value ( 'action' ) === self :: UPDATE_WIDGET_AJAX_ACTION
&&
check_ajax_referer ( self :: UPDATE_WIDGET_AJAX_ACTION , self :: UPDATE_WIDGET_NONCE_POST_KEY , false )
);
$is_ajax_customize_save = (
( defined ( 'DOING_AJAX' ) && DOING_AJAX )
&&
self :: get_post_value ( 'action' ) === 'customize_save'
&&
check_ajax_referer ( 'save-customize_' . $wp_customize -> get_stylesheet (), 'nonce' )
);
$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 ( self :: get_post_value ( 'customized' ), true );
}
// Input from ajax widget update request
else {
$customized = array ();
$id_base = self :: get_post_value ( 'id_base' );
$widget_number = ( int ) self :: 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 ( __CLASS__ , 'prepreview_added_sidebars_widgets' );
$hook = 'option_sidebars_widgets' ;
add_filter ( $hook , $function );
self :: $_prepreview_added_filters [] = compact ( 'hook' , 'function' );
$hook = 'default_option_sidebars_widgets' ;
add_filter ( $hook , $function );
self :: $_prepreview_added_filters [] = compact ( 'hook' , 'function' );
foreach ( $customized as $setting_id => $value ) {
if ( preg_match ( '/^(widget_.+?)(\[(\d+)\])?$/' , $setting_id , $matches ) ) {
$body = sprintf ( 'return %s::prepreview_added_widget_instance( $value, %s );' , __CLASS__ , var_export ( $setting_id , true ) );
$function = create_function ( '$value' , $body );
$option = $matches [ 1 ];
$hook = sprintf ( 'option_%s' , $option );
add_filter ( $hook , $function );
self :: $_prepreview_added_filters [] = compact ( 'hook' , 'function' );
$hook = sprintf ( 'default_option_%s' , $option );
add_filter ( $hook , $function );
self :: $_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 () );
}
}
self :: $_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 .
*
* @ param array $sidebars_widgets
* @ return array
*/
static function prepreview_added_sidebars_widgets ( $sidebars_widgets ) {
foreach ( self :: $_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 that newly - added widgets will have empty instances so that 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 .
*
* @ param array $instance
* @ param string $setting_id
* @ return array
*/
static function prepreview_added_widget_instance ( $instance , $setting_id ) {
if ( isset ( self :: $_customized [ $setting_id ] ) ) {
$parsed_setting_id = self :: 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 ();
}
}
// Multi widget
else if ( false === $instance || ! isset ( $instance [ $widget_number ] ) ) {
if ( empty ( $instance ) ) {
$instance = array ( '_multiwidget' => 1 );
}
if ( ! isset ( $instance [ $widget_number ] ) ) {
$instance [ $widget_number ] = array ();
}
}
}
return $instance ;
}
/**
* Remove filters added in setup_widget_addition_previews () which ensure that
* widgets are populating the options during widgets_init
*
* @ action wp_loaded
*/
static function remove_prepreview_filters () {
foreach ( self :: $_prepreview_added_filters as $prepreview_added_filter ) {
remove_filter ( $prepreview_added_filter [ 'hook' ], $prepreview_added_filter [ 'function' ] );
}
self :: $_prepreview_added_filters = array ();
}
/**
* Make sure that all widgets get loaded into customizer ; these actions are also done in the wp_ajax_save_widget ()
*
* @ see wp_ajax_save_widget ()
* @ action customize_controls_init
*/
static function customize_controls_init () {
do_action ( 'load-widgets.php' );
do_action ( 'widgets.php' );
do_action ( 'sidebar_admin_setup' );
}
/**
* When in preview , invoke customize_register for settings after WordPress is
* loaded so that all filters have been initialized ( e . g . Widget Visibility )
*/
static function schedule_customize_register ( $wp_customize ) {
if ( is_admin () ) { // @todo for some reason, $wp_customize->is_preview() is true here?
self :: customize_register ( $wp_customize );
} else {
add_action ( 'wp' , array ( __CLASS__ , 'customize_register' ) );
}
}
/**
* Register customizer settings and controls for all sidebars and widgets
*
* @ action customize_register
*/
static function customize_register ( $wp_customize = null ) {
global $wp_registered_widgets , $wp_registered_widget_controls ;
if ( ! ( $wp_customize instanceof WP_Customize_Manager ) ) {
$wp_customize = $GLOBALS [ 'wp_customize' ];
}
$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 = self :: get_setting_id ( $widget_id );
$setting_args = self :: get_setting_args ( $setting_id );
$setting_args [ 'sanitize_callback' ] = array ( __CLASS__ , 'sanitize_widget_instance' );
$setting_args [ 'sanitize_js_callback' ] = array ( __CLASS__ , 'sanitize_widget_js_instance' );
$wp_customize -> 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 = self :: get_setting_args ( $setting_id );
$setting_args [ 'sanitize_callback' ] = array ( __CLASS__ , 'sanitize_sidebar_widgets' );
$setting_args [ 'sanitize_js_callback' ] = array ( __CLASS__ , 'sanitize_sidebar_widgets_js_instance' );
$wp_customize -> 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 (
'title' => sprintf ( __ ( 'Widgets: %s' ), $GLOBALS [ 'wp_registered_sidebars' ][ $sidebar_id ][ 'name' ] ),
'description' => $GLOBALS [ 'wp_registered_sidebars' ][ $sidebar_id ][ 'description' ],
);
$section_args = apply_filters ( 'customizer_widgets_section_args' , $section_args , $section_id , $sidebar_id );
$wp_customize -> add_section ( $section_id , $section_args );
$control = new WP_Widget_Area_Customize_Control (
$wp_customize ,
$setting_id ,
array (
'section' => $section_id ,
'sidebar_id' => $sidebar_id ,
2014-03-13 19:38:14 +01:00
'priority' => count ( $sidebar_widget_ids ), // place Add Widget & Reorder buttons at end
2014-03-05 21:41:14 +01:00
)
);
$new_setting_ids [] = $setting_id ;
$wp_customize -> 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 = self :: 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 (
$wp_customize ,
$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' => self :: is_wide_widget ( $widget_id ),
)
);
$wp_customize -> add_control ( $control );
}
}
/**
* We have to register these settings later than customize_preview_init so that other
* filters have had a chance to run .
* @ see self :: schedule_customize_register ()
*/
if ( did_action ( 'customize_preview_init' ) ) {
foreach ( $new_setting_ids as $new_setting_id ) {
$wp_customize -> get_setting ( $new_setting_id ) -> preview ();
}
}
self :: remove_prepreview_filters ();
}
/**
* Covert a widget_id into its corresponding customizer setting id ( option name )
*
* @ param string $widget_id
* @ see _get_widget_id_base ()
* @ return string
*/
static function get_setting_id ( $widget_id ) {
$parsed_widget_id = self :: 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 ;
}
/**
* 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 .
*
* @ param string $widget_id
* @ return bool
*/
static function is_wide_widget ( $widget_id ) {
global $wp_registered_widget_controls ;
$parsed_widget_id = self :: parse_widget_id ( $widget_id );
$width = $wp_registered_widget_controls [ $widget_id ][ 'width' ];
$is_core = in_array ( $parsed_widget_id [ 'id_base' ], self :: $core_widget_id_bases );
$is_wide = ( $width > 250 && ! $is_core );
$is_wide = apply_filters ( 'is_wide_widget_in_customizer' , $is_wide , $widget_id );
return $is_wide ;
}
/**
* Covert a widget ID into its id_base and number components
*
* @ param string $widget_id
* @ return array
*/
static 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
*
* @ throws Widget_Customizer_Exception
* @ throws Exception
*
* @ param string $setting_id
* @ param array
* @ return array
*/
static function parse_widget_setting_id ( $setting_id ) {
if ( ! preg_match ( '/^(widget_(.+?))(?:\[(\d+)\])?$/' , $setting_id , $matches ) ) {
throw new Widget_Customizer_Exception ( sprintf ( 'Invalid widget setting ID: %s' , $setting_id ) );
}
$id_base = $matches [ 2 ];
$number = isset ( $matches [ 3 ] ) ? intval ( $matches [ 3 ] ) : null ;
return compact ( 'id_base' , 'number' );
}
/**
* Enqueue scripts and styles for customizer panel and export data to JS
*
* @ action customize_controls_enqueue_scripts
*/
static function customize_controls_enqueue_deps () {
wp_enqueue_script ( 'jquery-ui-sortable' );
wp_enqueue_script ( 'jquery-ui-droppable' );
wp_enqueue_style (
'widget-customizer' ,
admin_url ( 'css/customize-widgets.css' )
);
wp_enqueue_script (
'widget-customizer' ,
admin_url ( 'js/customize-widgets.js' ),
array ( 'jquery' , 'wp-backbone' , 'wp-util' , 'customize-controls' )
);
// Export available widgets with control_tpl removed from model
// since plugins need templates to be in the DOM
$available_widgets = array ();
foreach ( self :: 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" title="%1$s">%2$s</span><span class="move-widget-down" tabindex="0" title="%3$s">%4$s</span><span class="move-widget-up" tabindex="0" title="%5$s">%6$s</span></div>' ,
esc_attr__ ( 'Move to another area...' ),
esc_html__ ( 'Move to another area...' ),
esc_attr__ ( 'Move down' ),
esc_html__ ( 'Move down' ),
esc_attr__ ( 'Move up' ),
esc_html__ ( 'Move up' )
);
$move_widget_area_tpl = str_replace (
array ( '{description}' , '{btn}' ),
array (
esc_html__ ( 'Select an area to move this widget into:' ),
esc_html__ ( 'Move' ),
),
'
< 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 (
'update_widget_ajax_action' => self :: UPDATE_WIDGET_AJAX_ACTION ,
'update_widget_nonce_value' => wp_create_nonce ( self :: UPDATE_WIDGET_AJAX_ACTION ),
'update_widget_nonce_post_key' => self :: UPDATE_WIDGET_NONCE_POST_KEY ,
'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' => _x ( 'Apply' , 'button to save changes to a widget' ),
'save_btn_tooltip' => _x ( 'Save and preview changes before publishing them.' , 'tooltip on the widget save button' ),
'remove_btn_label' => _x ( 'Remove' , 'link to move a widget to the inactive widgets sidebar' ),
'remove_btn_tooltip' => _x ( 'Trash widget by moving it to the inactive widgets sidebar.' , 'tooltip on btn a widget to move it to the inactive widgets sidebar' ),
),
'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 (
'widget-customizer' ,
'data' ,
sprintf ( 'var WidgetCustomizer_exports = %s;' , json_encode ( $exports ) )
);
}
/**
* Render the widget form control templates into the DOM so that plugin scripts can manipulate them
*
* @ action customize_controls_print_footer_scripts
*/
static 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 " >
< input type = " search " placeholder = " <?php esc_attr_e( 'Find widgets…' ) ?> " >
</ div >
< ? php foreach ( self :: 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
}
/**
* Get common arguments to supply when constructing a customizer setting
*
* @ param string $id
* @ param array [ $overrides ]
* @ return array
*/
static function get_setting_args ( $id , $overrides = array () ) {
$args = array (
'type' => 'option' ,
'capability' => 'edit_theme_options' ,
'transport' => 'refresh' ,
'default' => array (),
);
$args = array_merge ( $args , $overrides );
$args = apply_filters ( 'widget_customizer_setting_args' , $args , $id );
return $args ;
}
/**
* Make sure that a sidebars_widgets [ x ] only ever consists of actual widget IDs .
* Used as sanitize_callback for each sidebars_widgets setting .
*
* @ param array $widget_ids
* @ return array
*/
static 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 ;
}
/**
* Special filter for Settings Revisions plugin until it can handle
* dynamically creating settings .
*
* @ param mixed $value
* @ param stdClass | WP_Customize_Setting $setting
* @ return mixed
*/
static function temp_customize_sanitize_js ( $value , $setting ) {
if ( preg_match ( '/^widget_/' , $setting -> id ) && $setting -> type === 'option' ) {
$value = self :: sanitize_widget_js_instance ( $value );
}
return $value ;
}
/**
* Build up an index of all available widgets for use in Backbone models
*
* @ see wp_list_widgets ()
* @ return array
*/
static 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 ( __CLASS__ , '_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 = self :: 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' => self :: is_wide_widget ( $widget [ 'id' ] ),
)
);
$available_widgets [] = $available_widget ;
}
return $available_widgets ;
}
/**
* Replace with inline closure once on PHP 5.3 :
* sort ( $array , function ( $a , $b ) { return strnatcasecmp ( $a [ 'name' ], $b [ 'name' ] ); } );
*
* @ access private
*/
static function _sort_name_callback ( $a , $b ) {
return strnatcasecmp ( $a [ 'name' ], $b [ 'name' ] );
}
/**
* Invoke wp_widget_control () but capture the output buffer and transform the markup
* so that it can be used in the customizer .
*
* @ see wp_widget_control ()
* @ param array $args
* @ return string
*/
static 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
*
* @ action customize_preview_init
*/
static function customize_preview_init () {
add_filter ( 'sidebars_widgets' , array ( __CLASS__ , 'preview_sidebars_widgets' ), 1 );
add_action ( 'wp_enqueue_scripts' , array ( __CLASS__ , 'customize_preview_enqueue_deps' ) );
add_action ( 'wp_footer' , array ( __CLASS__ , 'export_preview_data' ), 9999 );
}
/**
* 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 .
*
* @ filter sidebars_widgets
*/
static 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
*
* @ action wp_enqueue_scripts
*/
static function customize_preview_enqueue_deps () {
wp_enqueue_script (
'customize-preview-widgets' ,
includes_url ( 'js/customize-preview-widgets.js' ),
array ( 'jquery' , 'wp-util' , 'customize-preview' )
);
/*
wp_enqueue_style (
'widget-customizer-preview' ,
'widget-customizer-preview.css'
);
*/
// Why not wp_localize_script? Because we're not localizing, and it forces values into strings
global $wp_scripts ;
$exports = array (
'registered_sidebars' => array_values ( $GLOBALS [ 'wp_registered_sidebars' ] ),
'registered_widgets' => $GLOBALS [ 'wp_registered_widgets' ],
'i18n' => array (
'widget_tooltip' => __ ( 'Press shift and then click to edit widget in customizer...' ),
),
'request_uri' => wp_unslash ( $_SERVER [ 'REQUEST_URI' ] ),
);
foreach ( $exports [ 'registered_widgets' ] as & $registered_widget ) {
unset ( $registered_widget [ 'callback' ] ); // may not be JSON-serializeable
}
$wp_scripts -> add_data (
'customize-preview-widgets' ,
'data' ,
sprintf ( 'var WidgetCustomizerPreview_exports = %s;' , json_encode ( $exports ) )
);
}
/**
* At the very end of the page , at the very end of the wp_footer , communicate the sidebars that appeared on the page
*
* @ action wp_footer
*/
static function export_preview_data () {
wp_print_scripts ( array ( 'customize-preview-widgets' ) );
?>
< script >
( function () {
/*global WidgetCustomizerPreview */
WidgetCustomizerPreview . rendered_sidebars = < ? php echo json_encode ( array_fill_keys ( array_unique ( self :: $rendered_sidebars ), true ) ) ?> ;
WidgetCustomizerPreview . rendered_widgets = < ? php echo json_encode ( array_fill_keys ( array_keys ( self :: $rendered_widgets ), true ) ); ?> ;
}());
</ script >
< ? php
}
static protected $rendered_sidebars = array ();
static protected $rendered_widgets = array ();
/**
* Keep track of the widgets that were rendered
*
* @ action dynamic_sidebar
*/
static function tally_rendered_widgets ( $widget ) {
self :: $rendered_widgets [ $widget [ 'id' ]] = true ;
}
/**
* This is hacky . It is too bad that dynamic_sidebar is not just called once with the $sidebar_id supplied
* This does not get called for a sidebar which lacks widgets .
* See core patch which addresses the problem .
*
* @ link http :// core . trac . wordpress . org / ticket / 25368
* @ action dynamic_sidebar
*/
static function tally_sidebars_via_dynamic_sidebar_actions ( $widget ) {
global $sidebars_widgets ;
foreach ( $sidebars_widgets as $sidebar_id => $widget_ids ) {
if ( in_array ( $sidebar_id , self :: $rendered_sidebars ) ) {
continue ;
}
if ( isset ( $GLOBALS [ 'wp_registered_sidebars' ][ $sidebar_id ] ) && is_array ( $widget_ids ) && in_array ( $widget [ 'id' ], $widget_ids ) ) {
self :: $rendered_sidebars [] = $sidebar_id ;
}
}
}
/**
* 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 .
*
* @ see http :// core . trac . wordpress . org / ticket / 25368
* @ filter temp_is_active_sidebar
*/
static function tally_sidebars_via_is_active_sidebar_calls ( $is_active , $sidebar_id ) {
if ( isset ( $GLOBALS [ 'wp_registered_sidebars' ][ $sidebar_id ] ) ) {
self :: $rendered_sidebars [] = $sidebar_id ;
}
// We may need to force this to true, and also force-true the value for temp_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 ;
}
/**
* Keep track of the times that dynamic_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 .
*
* @ see http :// core . trac . wordpress . org / ticket / 25368
* @ filter temp_dynamic_sidebar_has_widgets
*/
static function tally_sidebars_via_dynamic_sidebar_calls ( $has_widgets , $sidebar_id ) {
if ( isset ( $GLOBALS [ 'wp_registered_sidebars' ][ $sidebar_id ] ) ) {
self :: $rendered_sidebars [] = $sidebar_id ;
}
// We may need to force this to true, and also force-true the value for temp_is_active_sidebar
// if we want to ensure that there is an area to drop widgets into, if the sidebar is empty.
return $has_widgets ;
}
/**
* 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 .
*
* @ param array $instance
* @ return string
*/
protected static function get_instance_hash_key ( $instance ) {
$hash = md5 ( AUTH_KEY . serialize ( $instance ) );
return $hash ;
}
/**
* Unserialize the JS - instance for storing in the options . It ' s important
* that this filter only get applied to an instance once .
*
* @ see Widget_Customizer :: sanitize_widget_js_instance ()
*
* @ param array $value
* @ return array
*/
static function sanitize_widget_instance ( $value ) {
if ( $value === array () ) {
return $value ;
}
$invalid = (
empty ( $value [ 'is_widget_customizer_js_value' ] )
||
empty ( $value [ 'instance_hash_key' ] )
||
empty ( $value [ 'encoded_serialized_instance' ] )
);
if ( $invalid ) {
return null ;
}
$decoded = base64_decode ( $value [ 'encoded_serialized_instance' ], true );
if ( false === $decoded ) {
return null ;
}
$instance = unserialize ( $decoded );
if ( false === $instance ) {
return null ;
}
if ( self :: get_instance_hash_key ( $instance ) !== $value [ 'instance_hash_key' ] ) {
return null ;
}
return $instance ;
}
/**
* Convert widget instance into JSON - representable format
*
* @ see Widget_Customizer :: sanitize_widget_instance ()
*
* @ param array $value
* @ return array
*/
static 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' => self :: get_instance_hash_key ( $value ),
);
}
return $value ;
}
/**
* Strip out widget IDs for widgets which are no longer registered , such
* as the case when a plugin orphans a widget in a sidebar when it is deactivated .
*
* @ param array $widget_ids
* @ return array
*/
static 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 .
*
* @ throws Widget_Customizer_Exception
* @ throws Exception
*
* @ param string $widget_id
* @ return array
*/
static function call_widget_update ( $widget_id ) {
global $wp_registered_widget_updates , $wp_registered_widget_controls ;
$options_transaction = new Options_Transaction ();
try {
$options_transaction -> start ();
$parsed_id = self :: 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 ( self :: get_post_value ( 'sanitized_widget_setting' ), true );
if ( empty ( $sanitized_widget_setting ) ) {
throw new Widget_Customizer_Exception ( 'Malformed sanitized_widget_setting' );
}
$instance = self :: sanitize_widget_instance ( $sanitized_widget_setting );
if ( is_null ( $instance ) ) {
throw new Widget_Customizer_Exception ( 'Unsanitary sanitized_widget_setting' );
}
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 !== $options_transaction -> count () ) {
if ( count ( $options_transaction -> options ) > 1 ) {
throw new Widget_Customizer_Exception ( sprintf ( 'Widget %1$s unexpectedly updated more than one option.' , $widget_id ) );
}
$updated_option_name = key ( $options_transaction -> options );
if ( $updated_option_name !== $option_name ) {
throw new Widget_Customizer_Exception ( sprintf ( 'Widget %1$s updated option "%2$s", but expected "%3$s".' , $widget_id , $updated_option_name , $option_name ) );
}
}
/**
* 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 ;
}
$options_transaction -> rollback ();
return compact ( 'instance' , 'form' );
}
catch ( Exception $e ) {
$options_transaction -> rollback ();
throw $e ;
}
}
/**
* Allow 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 ()
*
* @ see wp_ajax_save_widget
* @ todo Reuse wp_ajax_save_widget now that we have option transactions ?
* @ action wp_ajax_update_widget
*/
static function wp_ajax_update_widget () {
$generic_error = __ ( 'An error has occurred. Please reload the page and try again.' );
try {
if ( ! check_ajax_referer ( self :: UPDATE_WIDGET_AJAX_ACTION , self :: UPDATE_WIDGET_NONCE_POST_KEY , false ) ) {
throw new Widget_Customizer_Exception ( __ ( 'Nonce check failed. Reload and try again?' ) );
}
if ( ! current_user_can ( 'edit_theme_options' ) ) {
throw new Widget_Customizer_Exception ( __ ( 'Current user cannot!' ) );
}
if ( ! isset ( $_POST [ 'widget-id' ] ) ) {
throw new Widget_Customizer_Exception ( __ ( 'Incomplete request' ) );
}
unset ( $_POST [ self :: UPDATE_WIDGET_NONCE_POST_KEY ], $_POST [ 'action' ] );
do_action ( 'load-widgets.php' );
do_action ( 'widgets.php' );
do_action ( 'sidebar_admin_setup' );
$widget_id = self :: get_post_value ( 'widget-id' );
$parsed_id = self :: 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 ] ) ) ) {
throw new Widget_Customizer_Exception ( 'Cannot pass widget templates to create new instances; apply template vars in JS' );
}
$updated_widget = self :: call_widget_update ( $widget_id ); // => {instance,form}
$form = $updated_widget [ 'form' ];
$instance = self :: sanitize_widget_js_instance ( $updated_widget [ 'instance' ] );
wp_send_json_success ( compact ( 'form' , 'instance' ) );
}
catch ( Exception $e ) {
if ( $e instanceof Widget_Customizer_Exception ) {
$message = $e -> getMessage ();
} else {
error_log ( sprintf ( '%s in %s: %s' , get_class ( $e ), __FUNCTION__ , $e -> getMessage () ) );
$message = $generic_error ;
}
wp_send_json_error ( compact ( 'message' ) );
}
}
}
class Widget_Customizer_Exception extends Exception {}
class Options_Transaction {
/**
* @ var array $options values updated while transaction is open
*/
public $options = array ();
protected $_ignore_transients = true ;
protected $_is_current = false ;
protected $_operations = array ();
function __construct ( $ignore_transients = true ) {
$this -> _ignore_transients = $ignore_transients ;
}
/**
* Determine whether or not the transaction is open
* @ return bool
*/
function is_current () {
return $this -> _is_current ;
}
/**
* @ param $option_name
* @ return boolean
*/
function is_option_ignored ( $option_name ) {
return ( $this -> _ignore_transients && 0 === strpos ( $option_name , '_transient_' ) );
}
/**
* Get the number of operations performed in the transaction
* @ return bool
*/
function count () {
return count ( $this -> _operations );
}
/**
* Start keeping track of changes to options , and cache their new values
*/
function start () {
$this -> _is_current = true ;
add_action ( 'added_option' , array ( $this , '_capture_added_option' ), 10 , 2 );
add_action ( 'updated_option' , array ( $this , '_capture_updated_option' ), 10 , 3 );
add_action ( 'delete_option' , array ( $this , '_capture_pre_deleted_option' ), 10 , 1 );
add_action ( 'deleted_option' , array ( $this , '_capture_deleted_option' ), 10 , 1 );
}
/**
* @ action added_option
* @ param $option_name
* @ param $new_value
*/
function _capture_added_option ( $option_name , $new_value ) {
if ( $this -> is_option_ignored ( $option_name ) ) {
return ;
}
$this -> options [ $option_name ] = $new_value ;
$operation = 'add' ;
$this -> _operations [] = compact ( 'operation' , 'option_name' , 'new_value' );
}
/**
* @ action updated_option
* @ param string $option_name
* @ param mixed $old_value
* @ param mixed $new_value
*/
function _capture_updated_option ( $option_name , $old_value , $new_value ) {
if ( $this -> is_option_ignored ( $option_name ) ) {
return ;
}
$this -> options [ $option_name ] = $new_value ;
$operation = 'update' ;
$this -> _operations [] = compact ( 'operation' , 'option_name' , 'old_value' , 'new_value' );
}
protected $_pending_delete_option_autoload ;
protected $_pending_delete_option_value ;
/**
* It 's too bad the old_value and autoload aren' t passed into the deleted_option action
* @ action delete_option
* @ param string $option_name
*/
function _capture_pre_deleted_option ( $option_name ) {
if ( $this -> is_option_ignored ( $option_name ) ) {
return ;
}
global $wpdb ;
$autoload = $wpdb -> get_var ( $wpdb -> prepare ( " SELECT autoload FROM $wpdb->options WHERE option_name = %s " , $option_name ) ); // db call ok; no-cache ok
$this -> _pending_delete_option_autoload = $autoload ;
$this -> _pending_delete_option_value = get_option ( $option_name );
}
/**
* @ action deleted_option
* @ param string $option_name
*/
function _capture_deleted_option ( $option_name ) {
if ( $this -> is_option_ignored ( $option_name ) ) {
return ;
}
unset ( $this -> options [ $option_name ] );
$operation = 'delete' ;
$old_value = $this -> _pending_delete_option_value ;
$autoload = $this -> _pending_delete_option_autoload ;
$this -> _operations [] = compact ( 'operation' , 'option_name' , 'old_value' , 'autoload' );
}
/**
* Undo any changes to the options since start () was called
*/
function rollback () {
remove_action ( 'updated_option' , array ( $this , '_capture_updated_option' ), 10 , 3 );
remove_action ( 'added_option' , array ( $this , '_capture_added_option' ), 10 , 2 );
remove_action ( 'delete_option' , array ( $this , '_capture_pre_deleted_option' ), 10 , 1 );
remove_action ( 'deleted_option' , array ( $this , '_capture_deleted_option' ), 10 , 1 );
while ( 0 !== count ( $this -> _operations ) ) {
$option_operation = array_pop ( $this -> _operations );
if ( 'add' === $option_operation [ 'operation' ] ) {
delete_option ( $option_operation [ 'option_name' ] );
}
else if ( 'delete' === $option_operation [ 'operation' ] ) {
add_option ( $option_operation [ 'option_name' ], $option_operation [ 'old_value' ], null , $option_operation [ 'autoload' ] );
}
else if ( 'update' === $option_operation [ 'operation' ] ) {
update_option ( $option_operation [ 'option_name' ], $option_operation [ 'old_value' ] );
}
else {
throw new Exception ( 'Unexpected operation' );
}
}
$this -> _is_current = false ;
}
}