* WordPress Customize Widgets classes
* Customize Widgets class.
* Implements widget management in the Customizer.
* @see WP_Customize_Manager
#[AllowDynamicProperties]
final class WP_Customize_Widgets {
* WP_Customize_Manager instance.
* @var WP_Customize_Manager
* All id_bases for widgets defined in core.
protected $core_widget_id_bases = array(
protected $rendered_sidebars = array();
protected $rendered_widgets = array();
protected $old_sidebars_widgets = array();
* Mapping of widget ID base to whether it supports selective refresh.
protected $selective_refreshable_widgets;
* Mapping of setting type to setting ID pattern.
protected $setting_id_patterns = array(
'widget_instance' => '/^widget_(?P<id_base>.+?)(?:\[(?P<widget_number>\d+)\])?$/',
'sidebar_widgets' => '/^sidebars_widgets\[(?P<sidebar_id>.+?)\]$/',
* @param WP_Customize_Manager $manager Customizer bootstrap instance.
public function __construct( $manager ) {
$this->manager = $manager;
// See https://github.com/xwp/wp-customize-snapshots/blob/962586659688a5b1fd9ae93618b7ce2d4e7a421c/php/class-customize-snapshot-manager.php#L420-L449
add_filter( 'customize_dynamic_setting_args', array( $this, 'filter_customize_dynamic_setting_args' ), 10, 2 );
add_action( 'widgets_init', array( $this, 'register_settings' ), 95 );
add_action( 'customize_register', array( $this, 'schedule_customize_register' ), 1 );
// Skip remaining hooks when the user can't manage widgets anyway.
if ( ! current_user_can( 'edit_theme_options' ) ) {
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_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_filter( 'customize_refresh_nonces', array( $this, 'refresh_nonces' ) );
add_filter( 'should_load_block_editor_scripts_and_styles', array( $this, 'should_load_block_editor_scripts_and_styles' ) );
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 );
add_filter( 'customize_dynamic_partial_args', array( $this, 'customize_dynamic_partial_args' ), 10, 2 );
add_action( 'customize_preview_init', array( $this, 'selective_refresh_init' ) );
* List whether each registered widget can be use selective refresh.
* If the theme does not support the customize-selective-refresh-widgets feature,
* then this will always return an empty array.
* @global WP_Widget_Factory $wp_widget_factory
* @return array Mapping of id_base to support. If theme doesn't support
* selective refresh, an empty array is returned.
public function get_selective_refreshable_widgets() {
global $wp_widget_factory;
if ( ! current_theme_supports( 'customize-selective-refresh-widgets' ) ) {
if ( ! isset( $this->selective_refreshable_widgets ) ) {
$this->selective_refreshable_widgets = array();
foreach ( $wp_widget_factory->widgets as $wp_widget ) {
$this->selective_refreshable_widgets[ $wp_widget->id_base ] = ! empty( $wp_widget->widget_options['customize_selective_refresh'] );
return $this->selective_refreshable_widgets;
* Determines if a widget supports selective refresh.
* @param string $id_base Widget ID Base.
* @return bool Whether the widget can be selective refreshed.
public function is_widget_selective_refreshable( $id_base ) {
$selective_refreshable_widgets = $this->get_selective_refreshable_widgets();
return ! empty( $selective_refreshable_widgets[ $id_base ] );
* Retrieves the widget setting type given a setting ID.
* @param string $setting_id Setting ID.
* @return string|void Setting type.
protected function get_setting_type( $setting_id ) {
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;
* Inspects the incoming customized data for any widget settings, and dynamically adds
* them up-front so widgets will be initialized properly.
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 ) );
if ( $this->manager->settings_previewed() ) {
foreach ( $settings as $setting ) {
* Determines the arguments for a dynamically-created setting.
* @param false|array $args The arguments to the WP_Customize_Setting constructor.
* @param string $setting_id ID for dynamic setting, usually coming from `$_POST['customized']`.
* @return array|false Setting arguments, false otherwise.
public function filter_customize_dynamic_setting_args( $args, $setting_id ) {
if ( $this->get_setting_type( $setting_id ) ) {
$args = $this->get_setting_args( $setting_id );
* Retrieves an unslashed post value or return a default.
* @param string $name Post value.
* @param mixed $default_value Default post value.
* @return mixed Unslashed post value or default value.
protected function get_post_value( $name, $default_value = null ) {
if ( ! isset( $_POST[ $name ] ) ) {
return wp_unslash( $_POST[ $name ] );
* Override sidebars_widgets for theme switch.
* When switching a theme via the Customizer, supply any previously-configured
* sidebars_widgets from the target theme as the initial sidebars_widgets
* setting. Also store the old theme's existing settings so that they can
* be passed along for storing in the sidebars_widgets theme_mod when the
* @global array $sidebars_widgets
* @global array $_wp_sidebars_widgets
public function override_sidebars_widgets_for_theme_switch() {
global $sidebars_widgets;
if ( $this->manager->doing_ajax() || $this->manager->is_theme_active() ) {
$this->old_sidebars_widgets = wp_get_sidebars_widgets();
add_filter( 'customize_value_old_sidebars_widgets_data', array( $this, 'filter_customize_value_old_sidebars_widgets_data' ) );
$this->manager->set_post_value( 'old_sidebars_widgets_data', $this->old_sidebars_widgets ); // Override any value cached in changeset.
// retrieve_widgets() looks at the global $sidebars_widgets.
$sidebars_widgets = $this->old_sidebars_widgets;
$sidebars_widgets = retrieve_widgets( 'customize' );
add_filter( 'option_sidebars_widgets', array( $this, 'filter_option_sidebars_widgets_for_theme_switch' ), 1 );
// Reset global cache var used by wp_get_sidebars_widgets().
unset( $GLOBALS['_wp_sidebars_widgets'] );
* Filters old_sidebars_widgets_data Customizer setting.
* When switching themes, filter the Customizer setting old_sidebars_widgets_data
* to supply initial $sidebars_widgets before they were overridden by retrieve_widgets().
* The value for old_sidebars_widgets_data gets set in the old theme's sidebars_widgets
* @see WP_Customize_Widgets::handle_theme_switch()
* @param array $old_sidebars_widgets
public function filter_customize_value_old_sidebars_widgets_data( $old_sidebars_widgets ) {
return $this->old_sidebars_widgets;
* Filters sidebars_widgets option for theme switch.
* When switching themes, the retrieve_widgets() function is run when the Customizer initializes,
* and then the new sidebars_widgets here get supplied as the default value for the sidebars_widgets
* @see WP_Customize_Widgets::handle_theme_switch()
* @global array $sidebars_widgets
* @param array $sidebars_widgets
public function filter_option_sidebars_widgets_for_theme_switch( $sidebars_widgets ) {
$sidebars_widgets = $GLOBALS['sidebars_widgets'];
$sidebars_widgets['array_version'] = 3;
return $sidebars_widgets;
* Ensures all widgets get loaded into the Customizer.
* Note: these actions are also fired in wp_ajax_update_widget().
public function customize_controls_init() {
/** This action is documented in wp-admin/includes/ajax-actions.php */
do_action( 'load-widgets.php' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
/** This action is documented in wp-admin/includes/ajax-actions.php */
do_action( 'widgets.php' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
/** This action is documented in wp-admin/widgets.php */
do_action( 'sidebar_admin_setup' );
* Ensures widgets are available for all types of previews.
* When in preview, hook to {@see 'customize_register'} for settings after WordPress is loaded
* so that all filters have been initialized (e.g. Widget Visibility).
public function schedule_customize_register() {
$this->customize_register();
add_action( 'wp', array( $this, 'customize_register' ) );
* Registers Customizer settings and controls for all sidebars and widgets.
* @global array $wp_registered_widgets
* @global array $wp_registered_widget_controls
* @global array $wp_registered_sidebars
public function customize_register() {
global $wp_registered_widgets, $wp_registered_widget_controls, $wp_registered_sidebars;
$use_widgets_block_editor = wp_use_widgets_block_editor();
add_filter( 'sidebars_widgets', array( $this, 'preview_sidebars_widgets' ), 1 );
$sidebars_widgets = array_merge(
array( 'wp_inactive_widgets' => array() ),
array_fill_keys( array_keys( $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 );
if ( ! $this->manager->get_setting( $setting_id ) ) {
$this->manager->add_setting( $setting_id, $setting_args );
$new_setting_ids[] = $setting_id;
* Add a setting which will be supplied for the theme's sidebars_widgets
* theme_mod when the theme is switched.
if ( ! $this->manager->is_theme_active() ) {
$setting_id = 'old_sidebars_widgets_data';
$setting_args = $this->get_setting_args(
'type' => 'global_variable',
$this->manager->add_setting( $setting_id, $setting_args );
$this->manager->add_panel(
'title' => __( 'Widgets' ),
'description' => __( 'Widgets are independent sections of content that can be placed into widgetized areas provided by your theme (commonly called sidebars).' ),
'active_callback' => array( $this, 'is_panel_active' ),
'auto_expand_sole_section' => true,
'theme_supports' => 'widgets',
foreach ( $sidebars_widgets as $sidebar_id => $sidebar_widget_ids ) {
if ( empty( $sidebar_widget_ids ) ) {
$sidebar_widget_ids = array();
$is_registered_sidebar = is_registered_sidebar( $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 );
if ( ! $this->manager->get_setting( $setting_id ) ) {
if ( ! $this->manager->is_theme_active() ) {
$setting_args['dirty'] = true;
$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 ) {
'title' => $wp_registered_sidebars[ $sidebar_id ]['name'],
'priority' => array_search( $sidebar_id, array_keys( $wp_registered_sidebars ), true ),
'sidebar_id' => $sidebar_id,
if ( $use_widgets_block_editor ) {
$section_args['description'] = '';
$section_args['description'] = $wp_registered_sidebars[ $sidebar_id ]['description'];
* Filters Customizer widget section arguments for a given sidebar.
* @param array $section_args Array of Customizer widget section arguments.
* @param string $section_id Customizer section ID.
* @param int|string $sidebar_id Sidebar ID.
$section_args = apply_filters( 'customizer_widgets_section_args', $section_args, $section_id, $sidebar_id );
$section = new WP_Customize_Sidebar_Section( $this->manager, $section_id, $section_args );
$this->manager->add_section( $section );
if ( $use_widgets_block_editor ) {
$control = new WP_Sidebar_Block_Editor_Control(
'section' => $section_id,
'sidebar_id' => $sidebar_id,
'label' => $section_args['title'],
'description' => $section_args['description'],
$control = new WP_Widget_Area_Customize_Control(
'section' => $section_id,
'sidebar_id' => $sidebar_id,
'priority' => count( $sidebar_widget_ids ), // place 'Add Widget' and 'Reorder' buttons at end.