: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
'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( $serialized ),
if ( $id_base && wp_use_widgets_block_editor() ) {
$widget_object = $wp_widget_factory->get_widget_object( $id_base );
if ( ! empty( $widget_object->widget_options['show_instance_in_rest'] ) ) {
$js_value['raw_instance'] = (object) $value;
* Strips 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.
* @global array $wp_registered_widgets
* @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 ) ) );
* Finds and invokes the widget update and control callbacks.
* Requires that `$_POST` be populated with the instance data.
* @global array $wp_registered_widget_updates
* @global array $wp_registered_widget_controls
* @param string $widget_id Widget ID.
* @return array|WP_Error 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;
$setting_id = $this->get_setting_id( $widget_id );
* Make sure that other setting changes have previewed since this widget
* may depend on them (e.g. Menus being present for Navigation Menu widget).
if ( ! did_action( 'customize_preview_init' ) ) {
foreach ( $this->manager->settings() as $setting ) {
if ( $setting->id !== $setting_id ) {
$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 ( false === $sanitized_widget_setting ) {
$this->stop_capturing_option_updates();
return new WP_Error( 'widget_setting_malformed' );
$instance = $this->sanitize_widget_instance( $sanitized_widget_setting, $parsed_id['id_base'] );
if ( is_null( $instance ) ) {
$this->stop_capturing_option_updates();
return new WP_Error( 'widget_setting_unsanitized' );
if ( ! is_null( $parsed_id['number'] ) ) {
$value[ $parsed_id['number'] ] = $instance;
$key = 'widget-' . $parsed_id['id_base'];
$_REQUEST[ $key ] = wp_slash( $value );
$_POST[ $key ] = $_REQUEST[ $key ];
$added_input_vars[] = $key;
foreach ( $instance as $key => $value ) {
$_REQUEST[ $key ] = wp_slash( $value );
$_POST[ $key ] = $_REQUEST[ $key ];
$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'] ) ) {
call_user_func_array( $control['callback'], $control['params'] );
// Clean up any input vars that were manually added.
foreach ( $added_input_vars as $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 instance.
$option = $this->get_captured_option( $option_name );
if ( null !== $parsed_id['number'] ) {
$instance = $option[ $parsed_id['number'] ];
* Override the incoming $_POST['customized'] for a newly-created widget's
* setting with the new $instance so that the preview filter currently
* in place from WP_Customize_Setting::preview() will use this value
* instead of the default widget instance value (an empty array).
$this->manager->set_post_value( $setting_id, $this->sanitize_widget_js_instance( $instance, $parsed_id['id_base'] ) );
// Obtain the widget control with the updated instance in place.
$form = $wp_registered_widget_controls[ $widget_id ];
call_user_func_array( $form['callback'], $form['params'] );
$this->stop_capturing_option_updates();
return compact( 'instance', 'form' );
* Updates 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().
* @see wp_ajax_save_widget()
public function wp_ajax_update_widget() {
if ( ! is_user_logged_in() ) {
check_ajax_referer( 'update-widget', 'nonce' );
if ( ! current_user_can( 'edit_theme_options' ) ) {
if ( empty( $_POST['widget-id'] ) ) {
wp_send_json_error( 'missing_widget-id' );
/** 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' );
$widget_id = $this->get_post_value( 'widget-id' );
$parsed_id = $this->parse_widget_id( $widget_id );
$id_base = $parsed_id['id_base'];
$is_updating_widget_template = (
isset( $_POST[ 'widget-' . $id_base ] )
is_array( $_POST[ 'widget-' . $id_base ] )
preg_match( '/__i__|%i%/', key( $_POST[ 'widget-' . $id_base ] ) )
if ( $is_updating_widget_template ) {
wp_send_json_error( 'template_widget_not_updatable' );
$updated_widget = $this->call_widget_update( $widget_id ); // => {instance,form}
if ( is_wp_error( $updated_widget ) ) {
wp_send_json_error( $updated_widget->get_error_code() );
$form = $updated_widget['form'];
$instance = $this->sanitize_widget_js_instance( $updated_widget['instance'], $id_base );
wp_send_json_success( compact( 'form', 'instance' ) );
* Selective Refresh Methods
* Filters arguments for dynamic widget partials.
* @param array|false $partial_args Partial arguments.
* @param string $partial_id Partial ID.
* @return array (Maybe) modified partial arguments.
public function customize_dynamic_partial_args( $partial_args, $partial_id ) {
if ( ! current_theme_supports( 'customize-selective-refresh-widgets' ) ) {
if ( preg_match( '/^widget\[(?P<widget_id>.+)\]$/', $partial_id, $matches ) ) {
if ( false === $partial_args ) {
$partial_args = array_merge(
'render_callback' => array( $this, 'render_widget_partial' ),
'container_inclusive' => true,
'settings' => array( $this->get_setting_id( $matches['widget_id'] ) ),
'capability' => 'edit_theme_options',
* Adds hooks for selective refresh.
public function selective_refresh_init() {
if ( ! current_theme_supports( 'customize-selective-refresh-widgets' ) ) {
add_filter( 'dynamic_sidebar_params', array( $this, 'filter_dynamic_sidebar_params' ) );
add_filter( 'wp_kses_allowed_html', array( $this, 'filter_wp_kses_allowed_data_attributes' ) );
add_action( 'dynamic_sidebar_before', array( $this, 'start_dynamic_sidebar' ) );
add_action( 'dynamic_sidebar_after', array( $this, 'end_dynamic_sidebar' ) );
* Inject selective refresh data attributes into widget container elements.
* Dynamic sidebar params.
* @type array $args Sidebar args.
* @type array $widget_args Widget args.
* @see WP_Customize_Nav_Menus::filter_wp_nav_menu_args()
public function filter_dynamic_sidebar_params( $params ) {
$sidebar_args = array_merge(
// Skip widgets not in a registered sidebar or ones which lack a proper wrapper element to attach the data-* attributes to.
isset( $sidebar_args['id'] )
is_registered_sidebar( $sidebar_args['id'] )
( isset( $this->current_dynamic_sidebar_id_stack[0] ) && $this->current_dynamic_sidebar_id_stack[0] === $sidebar_args['id'] )
preg_match( '#^<(?P<tag_name>\w+)#', $sidebar_args['before_widget'], $matches )
$this->before_widget_tags_seen[ $matches['tag_name'] ] = true;
'sidebar_id' => $sidebar_args['id'],
if ( isset( $this->context_sidebar_instance_number ) ) {
$context['sidebar_instance_number'] = $this->context_sidebar_instance_number;
} elseif ( isset( $sidebar_args['id'] ) && isset( $this->sidebar_instance_count[ $sidebar_args['id'] ] ) ) {
$context['sidebar_instance_number'] = $this->sidebar_instance_count[ $sidebar_args['id'] ];
$attributes = sprintf( ' data-customize-partial-id="%s"', esc_attr( 'widget[' . $sidebar_args['widget_id'] . ']' ) );
$attributes .= ' data-customize-partial-type="widget"';
$attributes .= sprintf( ' data-customize-partial-placement-context="%s"', esc_attr( wp_json_encode( $context ) ) );
$attributes .= sprintf( ' data-customize-widget-id="%s"', esc_attr( $sidebar_args['widget_id'] ) );
$sidebar_args['before_widget'] = preg_replace( '#^(<\w+)#', '$1 ' . $attributes, $sidebar_args['before_widget'] );
$params[0] = $sidebar_args;
* List of the tag names seen for before_widget strings.
* This is used in the {@see 'filter_wp_kses_allowed_html'} filter to ensure that the
* data-* attributes can be allowed.
protected $before_widget_tags_seen = array();
* Ensures the HTML data-* attributes for selective refresh are allowed by kses.
* This is needed in case the `$before_widget` is run through wp_kses() when printed.
* @param array $allowed_html Allowed HTML.
* @return array (Maybe) modified allowed HTML.
public function filter_wp_kses_allowed_data_attributes( $allowed_html ) {
foreach ( array_keys( $this->before_widget_tags_seen ) as $tag_name ) {
if ( ! isset( $allowed_html[ $tag_name ] ) ) {
$allowed_html[ $tag_name ] = array();
$allowed_html[ $tag_name ] = array_merge(
$allowed_html[ $tag_name ],
'data-customize-partial-id',
'data-customize-partial-type',
'data-customize-partial-placement-context',
'data-customize-partial-widget-id',
'data-customize-partial-options',
* Keep track of the number of times that dynamic_sidebar() was called for a given sidebar index.
* This helps facilitate the uncommon scenario where a single sidebar is rendered multiple times on a template.
protected $sidebar_instance_count = array();
* The current request's sidebar_instance_number context.
protected $context_sidebar_instance_number;
* Current sidebar ID being rendered.
protected $current_dynamic_sidebar_id_stack = array();
* Begins keeping track of the current sidebar being rendered.
* Insert marker before widgets are rendered in a dynamic sidebar.
* @param int|string $index Index, name, or ID of the dynamic sidebar.
public function start_dynamic_sidebar( $index ) {
array_unshift( $this->current_dynamic_sidebar_id_stack, $index );
if ( ! isset( $this->sidebar_instance_count[ $index ] ) ) {
$this->sidebar_instance_count[ $index ] = 0;
$this->sidebar_instance_count[ $index ] += 1;
if ( ! $this->manager->selective_refresh->is_render_partials_request() ) {
printf( "\n<!--dynamic_sidebar_before:%s:%d-->\n", esc_html( $index ), (int) $this->sidebar_instance_count[ $index ] );
* Finishes keeping track of the current sidebar being rendered.
* Inserts a marker after widgets are rendered in a dynamic sidebar.
* @param int|string $index Index, name, or ID of the dynamic sidebar.
public function end_dynamic_sidebar( $index ) {
array_shift( $this->current_dynamic_sidebar_id_stack );
if ( ! $this->manager->selective_refresh->is_render_partials_request() ) {
printf( "\n<!--dynamic_sidebar_after:%s:%d-->\n", esc_html( $index ), (int) $this->sidebar_instance_count[ $index ] );
* Current sidebar being rendered.
protected $rendering_widget_id;
* Current widget being rendered.
protected $rendering_sidebar_id;
* Filters sidebars_widgets to ensure the currently-rendered widget is the only widget in the current sidebar.
* @param array $sidebars_widgets Sidebars widgets.
* @return array Filtered sidebars widgets.
public function filter_sidebars_widgets_for_rendering_widget( $sidebars_widgets ) {
$sidebars_widgets[ $this->rendering_sidebar_id ] = array( $this->rendering_widget_id );
return $sidebars_widgets;
* Renders a specific widget using the supplied sidebar arguments.
* @param WP_Customize_Partial $partial Partial.
* @param array $context {
* Sidebar args supplied as container context.
* @type string $sidebar_id ID for sidebar for widget to render into.
* @type int $sidebar_instance_number Disambiguating instance number.
public function render_widget_partial( $partial, $context ) {
$id_data = $partial->id_data();
$widget_id = array_shift( $id_data['keys'] );
if ( ! is_array( $context )
|| empty( $context['sidebar_id'] )
|| ! is_registered_sidebar( $context['sidebar_id'] )