angelovcom.net

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

class-wp-customize-widgets.php (71193B)


      1 <?php
      2 /**
      3  * WordPress Customize Widgets classes
      4  *
      5  * @package WordPress
      6  * @subpackage Customize
      7  * @since 3.9.0
      8  */
      9 
     10 /**
     11  * Customize Widgets class.
     12  *
     13  * Implements widget management in the Customizer.
     14  *
     15  * @since 3.9.0
     16  *
     17  * @see WP_Customize_Manager
     18  */
     19 final class WP_Customize_Widgets {
     20 
     21 	/**
     22 	 * WP_Customize_Manager instance.
     23 	 *
     24 	 * @since 3.9.0
     25 	 * @var WP_Customize_Manager
     26 	 */
     27 	public $manager;
     28 
     29 	/**
     30 	 * All id_bases for widgets defined in core.
     31 	 *
     32 	 * @since 3.9.0
     33 	 * @var array
     34 	 */
     35 	protected $core_widget_id_bases = array(
     36 		'archives',
     37 		'calendar',
     38 		'categories',
     39 		'custom_html',
     40 		'links',
     41 		'media_audio',
     42 		'media_image',
     43 		'media_video',
     44 		'meta',
     45 		'nav_menu',
     46 		'pages',
     47 		'recent-comments',
     48 		'recent-posts',
     49 		'rss',
     50 		'search',
     51 		'tag_cloud',
     52 		'text',
     53 	);
     54 
     55 	/**
     56 	 * @since 3.9.0
     57 	 * @var array
     58 	 */
     59 	protected $rendered_sidebars = array();
     60 
     61 	/**
     62 	 * @since 3.9.0
     63 	 * @var array
     64 	 */
     65 	protected $rendered_widgets = array();
     66 
     67 	/**
     68 	 * @since 3.9.0
     69 	 * @var array
     70 	 */
     71 	protected $old_sidebars_widgets = array();
     72 
     73 	/**
     74 	 * Mapping of widget ID base to whether it supports selective refresh.
     75 	 *
     76 	 * @since 4.5.0
     77 	 * @var array
     78 	 */
     79 	protected $selective_refreshable_widgets;
     80 
     81 	/**
     82 	 * Mapping of setting type to setting ID pattern.
     83 	 *
     84 	 * @since 4.2.0
     85 	 * @var array
     86 	 */
     87 	protected $setting_id_patterns = array(
     88 		'widget_instance' => '/^widget_(?P<id_base>.+?)(?:\[(?P<widget_number>\d+)\])?$/',
     89 		'sidebar_widgets' => '/^sidebars_widgets\[(?P<sidebar_id>.+?)\]$/',
     90 	);
     91 
     92 	/**
     93 	 * Initial loader.
     94 	 *
     95 	 * @since 3.9.0
     96 	 *
     97 	 * @param WP_Customize_Manager $manager Customizer bootstrap instance.
     98 	 */
     99 	public function __construct( $manager ) {
    100 		$this->manager = $manager;
    101 
    102 		// See https://github.com/xwp/wp-customize-snapshots/blob/962586659688a5b1fd9ae93618b7ce2d4e7a421c/php/class-customize-snapshot-manager.php#L420-L449
    103 		add_filter( 'customize_dynamic_setting_args', array( $this, 'filter_customize_dynamic_setting_args' ), 10, 2 );
    104 		add_action( 'widgets_init', array( $this, 'register_settings' ), 95 );
    105 		add_action( 'customize_register', array( $this, 'schedule_customize_register' ), 1 );
    106 
    107 		// Skip remaining hooks when the user can't manage widgets anyway.
    108 		if ( ! current_user_can( 'edit_theme_options' ) ) {
    109 			return;
    110 		}
    111 
    112 		add_action( 'wp_loaded', array( $this, 'override_sidebars_widgets_for_theme_switch' ) );
    113 		add_action( 'customize_controls_init', array( $this, 'customize_controls_init' ) );
    114 		add_action( 'customize_controls_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
    115 		add_action( 'customize_controls_print_styles', array( $this, 'print_styles' ) );
    116 		add_action( 'customize_controls_print_scripts', array( $this, 'print_scripts' ) );
    117 		add_action( 'customize_controls_print_footer_scripts', array( $this, 'print_footer_scripts' ) );
    118 		add_action( 'customize_controls_print_footer_scripts', array( $this, 'output_widget_control_templates' ) );
    119 		add_action( 'customize_preview_init', array( $this, 'customize_preview_init' ) );
    120 		add_filter( 'customize_refresh_nonces', array( $this, 'refresh_nonces' ) );
    121 		add_filter( 'should_load_block_editor_scripts_and_styles', array( $this, 'should_load_block_editor_scripts_and_styles' ) );
    122 
    123 		add_action( 'dynamic_sidebar', array( $this, 'tally_rendered_widgets' ) );
    124 		add_filter( 'is_active_sidebar', array( $this, 'tally_sidebars_via_is_active_sidebar_calls' ), 10, 2 );
    125 		add_filter( 'dynamic_sidebar_has_widgets', array( $this, 'tally_sidebars_via_dynamic_sidebar_calls' ), 10, 2 );
    126 
    127 		// Selective Refresh.
    128 		add_filter( 'customize_dynamic_partial_args', array( $this, 'customize_dynamic_partial_args' ), 10, 2 );
    129 		add_action( 'customize_preview_init', array( $this, 'selective_refresh_init' ) );
    130 	}
    131 
    132 	/**
    133 	 * List whether each registered widget can be use selective refresh.
    134 	 *
    135 	 * If the theme does not support the customize-selective-refresh-widgets feature,
    136 	 * then this will always return an empty array.
    137 	 *
    138 	 * @since 4.5.0
    139 	 *
    140 	 * @global WP_Widget_Factory $wp_widget_factory
    141 	 *
    142 	 * @return array Mapping of id_base to support. If theme doesn't support
    143 	 *               selective refresh, an empty array is returned.
    144 	 */
    145 	public function get_selective_refreshable_widgets() {
    146 		global $wp_widget_factory;
    147 		if ( ! current_theme_supports( 'customize-selective-refresh-widgets' ) ) {
    148 			return array();
    149 		}
    150 		if ( ! isset( $this->selective_refreshable_widgets ) ) {
    151 			$this->selective_refreshable_widgets = array();
    152 			foreach ( $wp_widget_factory->widgets as $wp_widget ) {
    153 				$this->selective_refreshable_widgets[ $wp_widget->id_base ] = ! empty( $wp_widget->widget_options['customize_selective_refresh'] );
    154 			}
    155 		}
    156 		return $this->selective_refreshable_widgets;
    157 	}
    158 
    159 	/**
    160 	 * Determines if a widget supports selective refresh.
    161 	 *
    162 	 * @since 4.5.0
    163 	 *
    164 	 * @param string $id_base Widget ID Base.
    165 	 * @return bool Whether the widget can be selective refreshed.
    166 	 */
    167 	public function is_widget_selective_refreshable( $id_base ) {
    168 		$selective_refreshable_widgets = $this->get_selective_refreshable_widgets();
    169 		return ! empty( $selective_refreshable_widgets[ $id_base ] );
    170 	}
    171 
    172 	/**
    173 	 * Retrieves the widget setting type given a setting ID.
    174 	 *
    175 	 * @since 4.2.0
    176 	 *
    177 	 * @param string $setting_id Setting ID.
    178 	 * @return string|void Setting type.
    179 	 */
    180 	protected function get_setting_type( $setting_id ) {
    181 		static $cache = array();
    182 		if ( isset( $cache[ $setting_id ] ) ) {
    183 			return $cache[ $setting_id ];
    184 		}
    185 		foreach ( $this->setting_id_patterns as $type => $pattern ) {
    186 			if ( preg_match( $pattern, $setting_id ) ) {
    187 				$cache[ $setting_id ] = $type;
    188 				return $type;
    189 			}
    190 		}
    191 	}
    192 
    193 	/**
    194 	 * Inspects the incoming customized data for any widget settings, and dynamically adds
    195 	 * them up-front so widgets will be initialized properly.
    196 	 *
    197 	 * @since 4.2.0
    198 	 */
    199 	public function register_settings() {
    200 		$widget_setting_ids   = array();
    201 		$incoming_setting_ids = array_keys( $this->manager->unsanitized_post_values() );
    202 		foreach ( $incoming_setting_ids as $setting_id ) {
    203 			if ( ! is_null( $this->get_setting_type( $setting_id ) ) ) {
    204 				$widget_setting_ids[] = $setting_id;
    205 			}
    206 		}
    207 		if ( $this->manager->doing_ajax( 'update-widget' ) && isset( $_REQUEST['widget-id'] ) ) {
    208 			$widget_setting_ids[] = $this->get_setting_id( wp_unslash( $_REQUEST['widget-id'] ) );
    209 		}
    210 
    211 		$settings = $this->manager->add_dynamic_settings( array_unique( $widget_setting_ids ) );
    212 
    213 		if ( $this->manager->settings_previewed() ) {
    214 			foreach ( $settings as $setting ) {
    215 				$setting->preview();
    216 			}
    217 		}
    218 	}
    219 
    220 	/**
    221 	 * Determines the arguments for a dynamically-created setting.
    222 	 *
    223 	 * @since 4.2.0
    224 	 *
    225 	 * @param false|array $args       The arguments to the WP_Customize_Setting constructor.
    226 	 * @param string      $setting_id ID for dynamic setting, usually coming from `$_POST['customized']`.
    227 	 * @return array|false Setting arguments, false otherwise.
    228 	 */
    229 	public function filter_customize_dynamic_setting_args( $args, $setting_id ) {
    230 		if ( $this->get_setting_type( $setting_id ) ) {
    231 			$args = $this->get_setting_args( $setting_id );
    232 		}
    233 		return $args;
    234 	}
    235 
    236 	/**
    237 	 * Retrieves an unslashed post value or return a default.
    238 	 *
    239 	 * @since 3.9.0
    240 	 *
    241 	 * @param string $name    Post value.
    242 	 * @param mixed  $default Default post value.
    243 	 * @return mixed Unslashed post value or default value.
    244 	 */
    245 	protected function get_post_value( $name, $default = null ) {
    246 		if ( ! isset( $_POST[ $name ] ) ) {
    247 			return $default;
    248 		}
    249 
    250 		return wp_unslash( $_POST[ $name ] );
    251 	}
    252 
    253 	/**
    254 	 * Override sidebars_widgets for theme switch.
    255 	 *
    256 	 * When switching a theme via the Customizer, supply any previously-configured
    257 	 * sidebars_widgets from the target theme as the initial sidebars_widgets
    258 	 * setting. Also store the old theme's existing settings so that they can
    259 	 * be passed along for storing in the sidebars_widgets theme_mod when the
    260 	 * theme gets switched.
    261 	 *
    262 	 * @since 3.9.0
    263 	 *
    264 	 * @global array $sidebars_widgets
    265 	 * @global array $_wp_sidebars_widgets
    266 	 */
    267 	public function override_sidebars_widgets_for_theme_switch() {
    268 		global $sidebars_widgets;
    269 
    270 		if ( $this->manager->doing_ajax() || $this->manager->is_theme_active() ) {
    271 			return;
    272 		}
    273 
    274 		$this->old_sidebars_widgets = wp_get_sidebars_widgets();
    275 		add_filter( 'customize_value_old_sidebars_widgets_data', array( $this, 'filter_customize_value_old_sidebars_widgets_data' ) );
    276 		$this->manager->set_post_value( 'old_sidebars_widgets_data', $this->old_sidebars_widgets ); // Override any value cached in changeset.
    277 
    278 		// retrieve_widgets() looks at the global $sidebars_widgets.
    279 		$sidebars_widgets = $this->old_sidebars_widgets;
    280 		$sidebars_widgets = retrieve_widgets( 'customize' );
    281 		add_filter( 'option_sidebars_widgets', array( $this, 'filter_option_sidebars_widgets_for_theme_switch' ), 1 );
    282 		// Reset global cache var used by wp_get_sidebars_widgets().
    283 		unset( $GLOBALS['_wp_sidebars_widgets'] );
    284 	}
    285 
    286 	/**
    287 	 * Filters old_sidebars_widgets_data Customizer setting.
    288 	 *
    289 	 * When switching themes, filter the Customizer setting old_sidebars_widgets_data
    290 	 * to supply initial $sidebars_widgets before they were overridden by retrieve_widgets().
    291 	 * The value for old_sidebars_widgets_data gets set in the old theme's sidebars_widgets
    292 	 * theme_mod.
    293 	 *
    294 	 * @since 3.9.0
    295 	 *
    296 	 * @see WP_Customize_Widgets::handle_theme_switch()
    297 	 *
    298 	 * @param array $old_sidebars_widgets
    299 	 * @return array
    300 	 */
    301 	public function filter_customize_value_old_sidebars_widgets_data( $old_sidebars_widgets ) {
    302 		return $this->old_sidebars_widgets;
    303 	}
    304 
    305 	/**
    306 	 * Filters sidebars_widgets option for theme switch.
    307 	 *
    308 	 * When switching themes, the retrieve_widgets() function is run when the Customizer initializes,
    309 	 * and then the new sidebars_widgets here get supplied as the default value for the sidebars_widgets
    310 	 * option.
    311 	 *
    312 	 * @since 3.9.0
    313 	 *
    314 	 * @see WP_Customize_Widgets::handle_theme_switch()
    315 	 * @global array $sidebars_widgets
    316 	 *
    317 	 * @param array $sidebars_widgets
    318 	 * @return array
    319 	 */
    320 	public function filter_option_sidebars_widgets_for_theme_switch( $sidebars_widgets ) {
    321 		$sidebars_widgets                  = $GLOBALS['sidebars_widgets'];
    322 		$sidebars_widgets['array_version'] = 3;
    323 		return $sidebars_widgets;
    324 	}
    325 
    326 	/**
    327 	 * Ensures all widgets get loaded into the Customizer.
    328 	 *
    329 	 * Note: these actions are also fired in wp_ajax_update_widget().
    330 	 *
    331 	 * @since 3.9.0
    332 	 */
    333 	public function customize_controls_init() {
    334 		/** This action is documented in wp-admin/includes/ajax-actions.php */
    335 		do_action( 'load-widgets.php' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
    336 
    337 		/** This action is documented in wp-admin/includes/ajax-actions.php */
    338 		do_action( 'widgets.php' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
    339 
    340 		/** This action is documented in wp-admin/widgets.php */
    341 		do_action( 'sidebar_admin_setup' );
    342 	}
    343 
    344 	/**
    345 	 * Ensures widgets are available for all types of previews.
    346 	 *
    347 	 * When in preview, hook to {@see 'customize_register'} for settings after WordPress is loaded
    348 	 * so that all filters have been initialized (e.g. Widget Visibility).
    349 	 *
    350 	 * @since 3.9.0
    351 	 */
    352 	public function schedule_customize_register() {
    353 		if ( is_admin() ) {
    354 			$this->customize_register();
    355 		} else {
    356 			add_action( 'wp', array( $this, 'customize_register' ) );
    357 		}
    358 	}
    359 
    360 	/**
    361 	 * Registers Customizer settings and controls for all sidebars and widgets.
    362 	 *
    363 	 * @since 3.9.0
    364 	 *
    365 	 * @global array $wp_registered_widgets
    366 	 * @global array $wp_registered_widget_controls
    367 	 * @global array $wp_registered_sidebars
    368 	 */
    369 	public function customize_register() {
    370 		global $wp_registered_widgets, $wp_registered_widget_controls, $wp_registered_sidebars;
    371 
    372 		$use_widgets_block_editor = wp_use_widgets_block_editor();
    373 
    374 		add_filter( 'sidebars_widgets', array( $this, 'preview_sidebars_widgets' ), 1 );
    375 
    376 		$sidebars_widgets = array_merge(
    377 			array( 'wp_inactive_widgets' => array() ),
    378 			array_fill_keys( array_keys( $wp_registered_sidebars ), array() ),
    379 			wp_get_sidebars_widgets()
    380 		);
    381 
    382 		$new_setting_ids = array();
    383 
    384 		/*
    385 		 * Register a setting for all widgets, including those which are active,
    386 		 * inactive, and orphaned since a widget may get suppressed from a sidebar
    387 		 * via a plugin (like Widget Visibility).
    388 		 */
    389 		foreach ( array_keys( $wp_registered_widgets ) as $widget_id ) {
    390 			$setting_id   = $this->get_setting_id( $widget_id );
    391 			$setting_args = $this->get_setting_args( $setting_id );
    392 			if ( ! $this->manager->get_setting( $setting_id ) ) {
    393 				$this->manager->add_setting( $setting_id, $setting_args );
    394 			}
    395 			$new_setting_ids[] = $setting_id;
    396 		}
    397 
    398 		/*
    399 		 * Add a setting which will be supplied for the theme's sidebars_widgets
    400 		 * theme_mod when the theme is switched.
    401 		 */
    402 		if ( ! $this->manager->is_theme_active() ) {
    403 			$setting_id   = 'old_sidebars_widgets_data';
    404 			$setting_args = $this->get_setting_args(
    405 				$setting_id,
    406 				array(
    407 					'type'  => 'global_variable',
    408 					'dirty' => true,
    409 				)
    410 			);
    411 			$this->manager->add_setting( $setting_id, $setting_args );
    412 		}
    413 
    414 		$this->manager->add_panel(
    415 			'widgets',
    416 			array(
    417 				'type'                     => 'widgets',
    418 				'title'                    => __( 'Widgets' ),
    419 				'description'              => __( 'Widgets are independent sections of content that can be placed into widgetized areas provided by your theme (commonly called sidebars).' ),
    420 				'priority'                 => 110,
    421 				'active_callback'          => array( $this, 'is_panel_active' ),
    422 				'auto_expand_sole_section' => true,
    423 			)
    424 		);
    425 
    426 		foreach ( $sidebars_widgets as $sidebar_id => $sidebar_widget_ids ) {
    427 			if ( empty( $sidebar_widget_ids ) ) {
    428 				$sidebar_widget_ids = array();
    429 			}
    430 
    431 			$is_registered_sidebar = is_registered_sidebar( $sidebar_id );
    432 			$is_inactive_widgets   = ( 'wp_inactive_widgets' === $sidebar_id );
    433 			$is_active_sidebar     = ( $is_registered_sidebar && ! $is_inactive_widgets );
    434 
    435 			// Add setting for managing the sidebar's widgets.
    436 			if ( $is_registered_sidebar || $is_inactive_widgets ) {
    437 				$setting_id   = sprintf( 'sidebars_widgets[%s]', $sidebar_id );
    438 				$setting_args = $this->get_setting_args( $setting_id );
    439 				if ( ! $this->manager->get_setting( $setting_id ) ) {
    440 					if ( ! $this->manager->is_theme_active() ) {
    441 						$setting_args['dirty'] = true;
    442 					}
    443 					$this->manager->add_setting( $setting_id, $setting_args );
    444 				}
    445 				$new_setting_ids[] = $setting_id;
    446 
    447 				// Add section to contain controls.
    448 				$section_id = sprintf( 'sidebar-widgets-%s', $sidebar_id );
    449 				if ( $is_active_sidebar ) {
    450 
    451 					$section_args = array(
    452 						'title'      => $wp_registered_sidebars[ $sidebar_id ]['name'],
    453 						'priority'   => array_search( $sidebar_id, array_keys( $wp_registered_sidebars ), true ),
    454 						'panel'      => 'widgets',
    455 						'sidebar_id' => $sidebar_id,
    456 					);
    457 
    458 					if ( $use_widgets_block_editor ) {
    459 						$section_args['description'] = '';
    460 					} else {
    461 						$section_args['description'] = $wp_registered_sidebars[ $sidebar_id ]['description'];
    462 					}
    463 
    464 					/**
    465 					 * Filters Customizer widget section arguments for a given sidebar.
    466 					 *
    467 					 * @since 3.9.0
    468 					 *
    469 					 * @param array      $section_args Array of Customizer widget section arguments.
    470 					 * @param string     $section_id   Customizer section ID.
    471 					 * @param int|string $sidebar_id   Sidebar ID.
    472 					 */
    473 					$section_args = apply_filters( 'customizer_widgets_section_args', $section_args, $section_id, $sidebar_id );
    474 
    475 					$section = new WP_Customize_Sidebar_Section( $this->manager, $section_id, $section_args );
    476 					$this->manager->add_section( $section );
    477 
    478 					if ( $use_widgets_block_editor ) {
    479 						$control = new WP_Sidebar_Block_Editor_Control(
    480 							$this->manager,
    481 							$setting_id,
    482 							array(
    483 								'section'     => $section_id,
    484 								'sidebar_id'  => $sidebar_id,
    485 								'label'       => $section_args['title'],
    486 								'description' => $section_args['description'],
    487 							)
    488 						);
    489 					} else {
    490 						$control = new WP_Widget_Area_Customize_Control(
    491 							$this->manager,
    492 							$setting_id,
    493 							array(
    494 								'section'    => $section_id,
    495 								'sidebar_id' => $sidebar_id,
    496 								'priority'   => count( $sidebar_widget_ids ), // place 'Add Widget' and 'Reorder' buttons at end.
    497 							)
    498 						);
    499 					}
    500 
    501 					$this->manager->add_control( $control );
    502 
    503 					$new_setting_ids[] = $setting_id;
    504 				}
    505 			}
    506 
    507 			if ( ! $use_widgets_block_editor ) {
    508 				// Add a control for each active widget (located in a sidebar).
    509 				foreach ( $sidebar_widget_ids as $i => $widget_id ) {
    510 
    511 					// Skip widgets that may have gone away due to a plugin being deactivated.
    512 					if ( ! $is_active_sidebar || ! isset( $wp_registered_widgets[ $widget_id ] ) ) {
    513 						continue;
    514 					}
    515 
    516 					$registered_widget = $wp_registered_widgets[ $widget_id ];
    517 					$setting_id        = $this->get_setting_id( $widget_id );
    518 					$id_base           = $wp_registered_widget_controls[ $widget_id ]['id_base'];
    519 
    520 					$control = new WP_Widget_Form_Customize_Control(
    521 						$this->manager,
    522 						$setting_id,
    523 						array(
    524 							'label'          => $registered_widget['name'],
    525 							'section'        => $section_id,
    526 							'sidebar_id'     => $sidebar_id,
    527 							'widget_id'      => $widget_id,
    528 							'widget_id_base' => $id_base,
    529 							'priority'       => $i,
    530 							'width'          => $wp_registered_widget_controls[ $widget_id ]['width'],
    531 							'height'         => $wp_registered_widget_controls[ $widget_id ]['height'],
    532 							'is_wide'        => $this->is_wide_widget( $widget_id ),
    533 						)
    534 					);
    535 					$this->manager->add_control( $control );
    536 				}
    537 			}
    538 		}
    539 
    540 		if ( $this->manager->settings_previewed() ) {
    541 			foreach ( $new_setting_ids as $new_setting_id ) {
    542 				$this->manager->get_setting( $new_setting_id )->preview();
    543 			}
    544 		}
    545 	}
    546 
    547 	/**
    548 	 * Determines whether the widgets panel is active, based on whether there are sidebars registered.
    549 	 *
    550 	 * @since 4.4.0
    551 	 *
    552 	 * @see WP_Customize_Panel::$active_callback
    553 	 *
    554 	 * @global array $wp_registered_sidebars
    555 	 * @return bool Active.
    556 	 */
    557 	public function is_panel_active() {
    558 		global $wp_registered_sidebars;
    559 		return ! empty( $wp_registered_sidebars );
    560 	}
    561 
    562 	/**
    563 	 * Converts a widget_id into its corresponding Customizer setting ID (option name).
    564 	 *
    565 	 * @since 3.9.0
    566 	 *
    567 	 * @param string $widget_id Widget ID.
    568 	 * @return string Maybe-parsed widget ID.
    569 	 */
    570 	public function get_setting_id( $widget_id ) {
    571 		$parsed_widget_id = $this->parse_widget_id( $widget_id );
    572 		$setting_id       = sprintf( 'widget_%s', $parsed_widget_id['id_base'] );
    573 
    574 		if ( ! is_null( $parsed_widget_id['number'] ) ) {
    575 			$setting_id .= sprintf( '[%d]', $parsed_widget_id['number'] );
    576 		}
    577 		return $setting_id;
    578 	}
    579 
    580 	/**
    581 	 * Determines whether the widget is considered "wide".
    582 	 *
    583 	 * Core widgets which may have controls wider than 250, but can still be shown
    584 	 * in the narrow Customizer panel. The RSS and Text widgets in Core, for example,
    585 	 * have widths of 400 and yet they still render fine in the Customizer panel.
    586 	 *
    587 	 * This method will return all Core widgets as being not wide, but this can be
    588 	 * overridden with the {@see 'is_wide_widget_in_customizer'} filter.
    589 	 *
    590 	 * @since 3.9.0
    591 	 *
    592 	 * @global array $wp_registered_widget_controls
    593 	 *
    594 	 * @param string $widget_id Widget ID.
    595 	 * @return bool Whether or not the widget is a "wide" widget.
    596 	 */
    597 	public function is_wide_widget( $widget_id ) {
    598 		global $wp_registered_widget_controls;
    599 
    600 		$parsed_widget_id = $this->parse_widget_id( $widget_id );
    601 		$width            = $wp_registered_widget_controls[ $widget_id ]['width'];
    602 		$is_core          = in_array( $parsed_widget_id['id_base'], $this->core_widget_id_bases, true );
    603 		$is_wide          = ( $width > 250 && ! $is_core );
    604 
    605 		/**
    606 		 * Filters whether the given widget is considered "wide".
    607 		 *
    608 		 * @since 3.9.0
    609 		 *
    610 		 * @param bool   $is_wide   Whether the widget is wide, Default false.
    611 		 * @param string $widget_id Widget ID.
    612 		 */
    613 		return apply_filters( 'is_wide_widget_in_customizer', $is_wide, $widget_id );
    614 	}
    615 
    616 	/**
    617 	 * Converts a widget ID into its id_base and number components.
    618 	 *
    619 	 * @since 3.9.0
    620 	 *
    621 	 * @param string $widget_id Widget ID.
    622 	 * @return array Array containing a widget's id_base and number components.
    623 	 */
    624 	public function parse_widget_id( $widget_id ) {
    625 		$parsed = array(
    626 			'number'  => null,
    627 			'id_base' => null,
    628 		);
    629 
    630 		if ( preg_match( '/^(.+)-(\d+)$/', $widget_id, $matches ) ) {
    631 			$parsed['id_base'] = $matches[1];
    632 			$parsed['number']  = (int) $matches[2];
    633 		} else {
    634 			// Likely an old single widget.
    635 			$parsed['id_base'] = $widget_id;
    636 		}
    637 		return $parsed;
    638 	}
    639 
    640 	/**
    641 	 * Converts a widget setting ID (option path) to its id_base and number components.
    642 	 *
    643 	 * @since 3.9.0
    644 	 *
    645 	 * @param string $setting_id Widget setting ID.
    646 	 * @return array|WP_Error Array containing a widget's id_base and number components,
    647 	 *                        or a WP_Error object.
    648 	 */
    649 	public function parse_widget_setting_id( $setting_id ) {
    650 		if ( ! preg_match( '/^(widget_(.+?))(?:\[(\d+)\])?$/', $setting_id, $matches ) ) {
    651 			return new WP_Error( 'widget_setting_invalid_id' );
    652 		}
    653 
    654 		$id_base = $matches[2];
    655 		$number  = isset( $matches[3] ) ? (int) $matches[3] : null;
    656 
    657 		return compact( 'id_base', 'number' );
    658 	}
    659 
    660 	/**
    661 	 * Calls admin_print_styles-widgets.php and admin_print_styles hooks to
    662 	 * allow custom styles from plugins.
    663 	 *
    664 	 * @since 3.9.0
    665 	 */
    666 	public function print_styles() {
    667 		/** This action is documented in wp-admin/admin-header.php */
    668 		do_action( 'admin_print_styles-widgets.php' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
    669 
    670 		/** This action is documented in wp-admin/admin-header.php */
    671 		do_action( 'admin_print_styles' );
    672 	}
    673 
    674 	/**
    675 	 * Calls admin_print_scripts-widgets.php and admin_print_scripts hooks to
    676 	 * allow custom scripts from plugins.
    677 	 *
    678 	 * @since 3.9.0
    679 	 */
    680 	public function print_scripts() {
    681 		/** This action is documented in wp-admin/admin-header.php */
    682 		do_action( 'admin_print_scripts-widgets.php' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
    683 
    684 		/** This action is documented in wp-admin/admin-header.php */
    685 		do_action( 'admin_print_scripts' );
    686 	}
    687 
    688 	/**
    689 	 * Enqueues scripts and styles for Customizer panel and export data to JavaScript.
    690 	 *
    691 	 * @since 3.9.0
    692 	 *
    693 	 * @global WP_Scripts $wp_scripts
    694 	 * @global array $wp_registered_sidebars
    695 	 * @global array $wp_registered_widgets
    696 	 */
    697 	public function enqueue_scripts() {
    698 		global $wp_scripts, $wp_registered_sidebars, $wp_registered_widgets;
    699 
    700 		wp_enqueue_style( 'customize-widgets' );
    701 		wp_enqueue_script( 'customize-widgets' );
    702 
    703 		/** This action is documented in wp-admin/admin-header.php */
    704 		do_action( 'admin_enqueue_scripts', 'widgets.php' );
    705 
    706 		/*
    707 		 * Export available widgets with control_tpl removed from model
    708 		 * since plugins need templates to be in the DOM.
    709 		 */
    710 		$available_widgets = array();
    711 
    712 		foreach ( $this->get_available_widgets() as $available_widget ) {
    713 			unset( $available_widget['control_tpl'] );
    714 			$available_widgets[] = $available_widget;
    715 		}
    716 
    717 		$widget_reorder_nav_tpl = sprintf(
    718 			'<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>',
    719 			__( 'Move to another area&hellip;' ),
    720 			__( 'Move down' ),
    721 			__( 'Move up' )
    722 		);
    723 
    724 		$move_widget_area_tpl = str_replace(
    725 			array( '{description}', '{btn}' ),
    726 			array(
    727 				__( 'Select an area to move this widget into:' ),
    728 				_x( 'Move', 'Move widget' ),
    729 			),
    730 			'<div class="move-widget-area">
    731 				<p class="description">{description}</p>
    732 				<ul class="widget-area-select">
    733 					<% _.each( sidebars, function ( sidebar ){ %>
    734 						<li class="" data-id="<%- sidebar.id %>" title="<%- sidebar.description %>" tabindex="0"><%- sidebar.name %></li>
    735 					<% }); %>
    736 				</ul>
    737 				<div class="move-widget-actions">
    738 					<button class="move-widget-btn button" type="button">{btn}</button>
    739 				</div>
    740 			</div>'
    741 		);
    742 
    743 		/*
    744 		 * Gather all strings in PHP that may be needed by JS on the client.
    745 		 * Once JS i18n is implemented (in #20491), this can be removed.
    746 		 */
    747 		$some_non_rendered_areas_messages    = array();
    748 		$some_non_rendered_areas_messages[1] = html_entity_decode(
    749 			__( 'Your theme has 1 other widget area, but this particular page doesn&#8217;t display it.' ),
    750 			ENT_QUOTES,
    751 			get_bloginfo( 'charset' )
    752 		);
    753 		$registered_sidebar_count            = count( $wp_registered_sidebars );
    754 		for ( $non_rendered_count = 2; $non_rendered_count < $registered_sidebar_count; $non_rendered_count++ ) {
    755 			$some_non_rendered_areas_messages[ $non_rendered_count ] = html_entity_decode(
    756 				sprintf(
    757 					/* translators: %s: The number of other widget areas registered but not rendered. */
    758 					_n(
    759 						'Your theme has %s other widget area, but this particular page doesn&#8217;t display it.',
    760 						'Your theme has %s other widget areas, but this particular page doesn&#8217;t display them.',
    761 						$non_rendered_count
    762 					),
    763 					number_format_i18n( $non_rendered_count )
    764 				),
    765 				ENT_QUOTES,
    766 				get_bloginfo( 'charset' )
    767 			);
    768 		}
    769 
    770 		if ( 1 === $registered_sidebar_count ) {
    771 			$no_areas_shown_message = html_entity_decode(
    772 				sprintf(
    773 					__( 'Your theme has 1 widget area, but this particular page doesn&#8217;t display it.' )
    774 				),
    775 				ENT_QUOTES,
    776 				get_bloginfo( 'charset' )
    777 			);
    778 		} else {
    779 			$no_areas_shown_message = html_entity_decode(
    780 				sprintf(
    781 					/* translators: %s: The total number of widget areas registered. */
    782 					_n(
    783 						'Your theme has %s widget area, but this particular page doesn&#8217;t display it.',
    784 						'Your theme has %s widget areas, but this particular page doesn&#8217;t display them.',
    785 						$registered_sidebar_count
    786 					),
    787 					number_format_i18n( $registered_sidebar_count )
    788 				),
    789 				ENT_QUOTES,
    790 				get_bloginfo( 'charset' )
    791 			);
    792 		}
    793 
    794 		$settings = array(
    795 			'registeredSidebars'          => array_values( $wp_registered_sidebars ),
    796 			'registeredWidgets'           => $wp_registered_widgets,
    797 			'availableWidgets'            => $available_widgets, // @todo Merge this with registered_widgets.
    798 			'l10n'                        => array(
    799 				'saveBtnLabel'     => __( 'Apply' ),
    800 				'saveBtnTooltip'   => __( 'Save and preview changes before publishing them.' ),
    801 				'removeBtnLabel'   => __( 'Remove' ),
    802 				'removeBtnTooltip' => __( 'Keep widget settings and move it to the inactive widgets' ),
    803 				'error'            => __( 'An error has occurred. Please reload the page and try again.' ),
    804 				'widgetMovedUp'    => __( 'Widget moved up' ),
    805 				'widgetMovedDown'  => __( 'Widget moved down' ),
    806 				'navigatePreview'  => __( 'You can navigate to other pages on your site while using the Customizer to view and edit the widgets displayed on those pages.' ),
    807 				'someAreasShown'   => $some_non_rendered_areas_messages,
    808 				'noAreasShown'     => $no_areas_shown_message,
    809 				'reorderModeOn'    => __( 'Reorder mode enabled' ),
    810 				'reorderModeOff'   => __( 'Reorder mode closed' ),
    811 				'reorderLabelOn'   => esc_attr__( 'Reorder widgets' ),
    812 				/* translators: %d: The number of widgets found. */
    813 				'widgetsFound'     => __( 'Number of widgets found: %d' ),
    814 				'noWidgetsFound'   => __( 'No widgets found.' ),
    815 			),
    816 			'tpl'                         => array(
    817 				'widgetReorderNav' => $widget_reorder_nav_tpl,
    818 				'moveWidgetArea'   => $move_widget_area_tpl,
    819 			),
    820 			'selectiveRefreshableWidgets' => $this->get_selective_refreshable_widgets(),
    821 		);
    822 
    823 		foreach ( $settings['registeredWidgets'] as &$registered_widget ) {
    824 			unset( $registered_widget['callback'] ); // May not be JSON-serializeable.
    825 		}
    826 
    827 		$wp_scripts->add_data(
    828 			'customize-widgets',
    829 			'data',
    830 			sprintf( 'var _wpCustomizeWidgetsSettings = %s;', wp_json_encode( $settings ) )
    831 		);
    832 
    833 		/*
    834 		 * TODO: Update 'wp-customize-widgets' to not rely so much on things in
    835 		 * 'customize-widgets'. This will let us skip most of the above and not
    836 		 * enqueue 'customize-widgets' which saves bytes.
    837 		 */
    838 
    839 		if ( wp_use_widgets_block_editor() ) {
    840 			$block_editor_context = new WP_Block_Editor_Context();
    841 
    842 			$editor_settings = get_block_editor_settings(
    843 				get_legacy_widget_block_editor_settings(),
    844 				$block_editor_context
    845 			);
    846 
    847 			wp_add_inline_script(
    848 				'wp-customize-widgets',
    849 				sprintf(
    850 					'wp.domReady( function() {
    851 					   wp.customizeWidgets.initialize( "widgets-customizer", %s );
    852 					} );',
    853 					wp_json_encode( $editor_settings )
    854 				)
    855 			);
    856 
    857 			// Preload server-registered block schemas.
    858 			wp_add_inline_script(
    859 				'wp-blocks',
    860 				'wp.blocks.unstable__bootstrapServerSideBlockDefinitions(' . wp_json_encode( get_block_editor_server_block_settings() ) . ');'
    861 			);
    862 
    863 			wp_add_inline_script(
    864 				'wp-blocks',
    865 				sprintf( 'wp.blocks.setCategories( %s );', wp_json_encode( get_block_categories( $block_editor_context ) ) ),
    866 				'after'
    867 			);
    868 
    869 			wp_enqueue_script( 'wp-customize-widgets' );
    870 			wp_enqueue_style( 'wp-customize-widgets' );
    871 
    872 			/** This action is documented in edit-form-blocks.php */
    873 			do_action( 'enqueue_block_editor_assets' );
    874 		}
    875 	}
    876 
    877 	/**
    878 	 * Renders the widget form control templates into the DOM.
    879 	 *
    880 	 * @since 3.9.0
    881 	 */
    882 	public function output_widget_control_templates() {
    883 		?>
    884 		<div id="widgets-left"><!-- compatibility with JS which looks for widget templates here -->
    885 		<div id="available-widgets">
    886 			<div class="customize-section-title">
    887 				<button class="customize-section-back" tabindex="-1">
    888 					<span class="screen-reader-text"><?php _e( 'Back' ); ?></span>
    889 				</button>
    890 				<h3>
    891 					<span class="customize-action">
    892 					<?php
    893 						/* translators: &#9656; is the unicode right-pointing triangle. %s: Section title in the Customizer. */
    894 						printf( __( 'Customizing &#9656; %s' ), esc_html( $this->manager->get_panel( 'widgets' )->title ) );
    895 					?>
    896 					</span>
    897 					<?php _e( 'Add a Widget' ); ?>
    898 				</h3>
    899 			</div>
    900 			<div id="available-widgets-filter">
    901 				<label class="screen-reader-text" for="widgets-search"><?php _e( 'Search Widgets' ); ?></label>
    902 				<input type="text" id="widgets-search" placeholder="<?php esc_attr_e( 'Search widgets&hellip;' ); ?>" aria-describedby="widgets-search-desc" />
    903 				<div class="search-icon" aria-hidden="true"></div>
    904 				<button type="button" class="clear-results"><span class="screen-reader-text"><?php _e( 'Clear Results' ); ?></span></button>
    905 				<p class="screen-reader-text" id="widgets-search-desc"><?php _e( 'The search results will be updated as you type.' ); ?></p>
    906 			</div>
    907 			<div id="available-widgets-list">
    908 			<?php foreach ( $this->get_available_widgets() as $available_widget ) : ?>
    909 				<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">
    910 					<?php echo $available_widget['control_tpl']; ?>
    911 				</div>
    912 			<?php endforeach; ?>
    913 			<p class="no-widgets-found-message"><?php _e( 'No widgets found.' ); ?></p>
    914 			</div><!-- #available-widgets-list -->
    915 		</div><!-- #available-widgets -->
    916 		</div><!-- #widgets-left -->
    917 		<?php
    918 	}
    919 
    920 	/**
    921 	 * Calls admin_print_footer_scripts and admin_print_scripts hooks to
    922 	 * allow custom scripts from plugins.
    923 	 *
    924 	 * @since 3.9.0
    925 	 */
    926 	public function print_footer_scripts() {
    927 		/** This action is documented in wp-admin/admin-footer.php */
    928 		do_action( 'admin_print_footer_scripts-widgets.php' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
    929 
    930 		/** This action is documented in wp-admin/admin-footer.php */
    931 		do_action( 'admin_print_footer_scripts' );
    932 
    933 		/** This action is documented in wp-admin/admin-footer.php */
    934 		do_action( 'admin_footer-widgets.php' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
    935 	}
    936 
    937 	/**
    938 	 * Retrieves common arguments to supply when constructing a Customizer setting.
    939 	 *
    940 	 * @since 3.9.0
    941 	 *
    942 	 * @param string $id        Widget setting ID.
    943 	 * @param array  $overrides Array of setting overrides.
    944 	 * @return array Possibly modified setting arguments.
    945 	 */
    946 	public function get_setting_args( $id, $overrides = array() ) {
    947 		$args = array(
    948 			'type'       => 'option',
    949 			'capability' => 'edit_theme_options',
    950 			'default'    => array(),
    951 		);
    952 
    953 		if ( preg_match( $this->setting_id_patterns['sidebar_widgets'], $id, $matches ) ) {
    954 			$args['sanitize_callback']    = array( $this, 'sanitize_sidebar_widgets' );
    955 			$args['sanitize_js_callback'] = array( $this, 'sanitize_sidebar_widgets_js_instance' );
    956 			$args['transport']            = current_theme_supports( 'customize-selective-refresh-widgets' ) ? 'postMessage' : 'refresh';
    957 		} elseif ( preg_match( $this->setting_id_patterns['widget_instance'], $id, $matches ) ) {
    958 			$id_base                      = $matches['id_base'];
    959 			$args['sanitize_callback']    = function( $value ) use ( $id_base ) {
    960 				return $this->sanitize_widget_instance( $value, $id_base );
    961 			};
    962 			$args['sanitize_js_callback'] = function( $value ) use ( $id_base ) {
    963 				return $this->sanitize_widget_js_instance( $value, $id_base );
    964 			};
    965 			$args['transport']            = $this->is_widget_selective_refreshable( $matches['id_base'] ) ? 'postMessage' : 'refresh';
    966 		}
    967 
    968 		$args = array_merge( $args, $overrides );
    969 
    970 		/**
    971 		 * Filters the common arguments supplied when constructing a Customizer setting.
    972 		 *
    973 		 * @since 3.9.0
    974 		 *
    975 		 * @see WP_Customize_Setting
    976 		 *
    977 		 * @param array  $args Array of Customizer setting arguments.
    978 		 * @param string $id   Widget setting ID.
    979 		 */
    980 		return apply_filters( 'widget_customizer_setting_args', $args, $id );
    981 	}
    982 
    983 	/**
    984 	 * Ensures sidebar widget arrays only ever contain widget IDS.
    985 	 *
    986 	 * Used as the 'sanitize_callback' for each $sidebars_widgets setting.
    987 	 *
    988 	 * @since 3.9.0
    989 	 *
    990 	 * @param string[] $widget_ids Array of widget IDs.
    991 	 * @return string[] Array of sanitized widget IDs.
    992 	 */
    993 	public function sanitize_sidebar_widgets( $widget_ids ) {
    994 		$widget_ids           = array_map( 'strval', (array) $widget_ids );
    995 		$sanitized_widget_ids = array();
    996 		foreach ( $widget_ids as $widget_id ) {
    997 			$sanitized_widget_ids[] = preg_replace( '/[^a-z0-9_\-]/', '', $widget_id );
    998 		}
    999 		return $sanitized_widget_ids;
   1000 	}
   1001 
   1002 	/**
   1003 	 * Builds up an index of all available widgets for use in Backbone models.
   1004 	 *
   1005 	 * @since 3.9.0
   1006 	 *
   1007 	 * @global array $wp_registered_widgets
   1008 	 * @global array $wp_registered_widget_controls
   1009 	 *
   1010 	 * @see wp_list_widgets()
   1011 	 *
   1012 	 * @return array List of available widgets.
   1013 	 */
   1014 	public function get_available_widgets() {
   1015 		static $available_widgets = array();
   1016 		if ( ! empty( $available_widgets ) ) {
   1017 			return $available_widgets;
   1018 		}
   1019 
   1020 		global $wp_registered_widgets, $wp_registered_widget_controls;
   1021 		require_once ABSPATH . 'wp-admin/includes/widgets.php'; // For next_widget_id_number().
   1022 
   1023 		$sort = $wp_registered_widgets;
   1024 		usort( $sort, array( $this, '_sort_name_callback' ) );
   1025 		$done = array();
   1026 
   1027 		foreach ( $sort as $widget ) {
   1028 			if ( in_array( $widget['callback'], $done, true ) ) { // We already showed this multi-widget.
   1029 				continue;
   1030 			}
   1031 
   1032 			$sidebar = is_active_widget( $widget['callback'], $widget['id'], false, false );
   1033 			$done[]  = $widget['callback'];
   1034 
   1035 			if ( ! isset( $widget['params'][0] ) ) {
   1036 				$widget['params'][0] = array();
   1037 			}
   1038 
   1039 			$available_widget = $widget;
   1040 			unset( $available_widget['callback'] ); // Not serializable to JSON.
   1041 
   1042 			$args = array(
   1043 				'widget_id'   => $widget['id'],
   1044 				'widget_name' => $widget['name'],
   1045 				'_display'    => 'template',
   1046 			);
   1047 
   1048 			$is_disabled     = false;
   1049 			$is_multi_widget = ( isset( $wp_registered_widget_controls[ $widget['id'] ]['id_base'] ) && isset( $widget['params'][0]['number'] ) );
   1050 			if ( $is_multi_widget ) {
   1051 				$id_base            = $wp_registered_widget_controls[ $widget['id'] ]['id_base'];
   1052 				$args['_temp_id']   = "$id_base-__i__";
   1053 				$args['_multi_num'] = next_widget_id_number( $id_base );
   1054 				$args['_add']       = 'multi';
   1055 			} else {
   1056 				$args['_add'] = 'single';
   1057 
   1058 				if ( $sidebar && 'wp_inactive_widgets' !== $sidebar ) {
   1059 					$is_disabled = true;
   1060 				}
   1061 				$id_base = $widget['id'];
   1062 			}
   1063 
   1064 			$list_widget_controls_args = wp_list_widget_controls_dynamic_sidebar(
   1065 				array(
   1066 					0 => $args,
   1067 					1 => $widget['params'][0],
   1068 				)
   1069 			);
   1070 			$control_tpl               = $this->get_widget_control( $list_widget_controls_args );
   1071 
   1072 			// The properties here are mapped to the Backbone Widget model.
   1073 			$available_widget = array_merge(
   1074 				$available_widget,
   1075 				array(
   1076 					'temp_id'      => isset( $args['_temp_id'] ) ? $args['_temp_id'] : null,
   1077 					'is_multi'     => $is_multi_widget,
   1078 					'control_tpl'  => $control_tpl,
   1079 					'multi_number' => ( 'multi' === $args['_add'] ) ? $args['_multi_num'] : false,
   1080 					'is_disabled'  => $is_disabled,
   1081 					'id_base'      => $id_base,
   1082 					'transport'    => $this->is_widget_selective_refreshable( $id_base ) ? 'postMessage' : 'refresh',
   1083 					'width'        => $wp_registered_widget_controls[ $widget['id'] ]['width'],
   1084 					'height'       => $wp_registered_widget_controls[ $widget['id'] ]['height'],
   1085 					'is_wide'      => $this->is_wide_widget( $widget['id'] ),
   1086 				)
   1087 			);
   1088 
   1089 			$available_widgets[] = $available_widget;
   1090 		}
   1091 
   1092 		return $available_widgets;
   1093 	}
   1094 
   1095 	/**
   1096 	 * Naturally orders available widgets by name.
   1097 	 *
   1098 	 * @since 3.9.0
   1099 	 *
   1100 	 * @param array $widget_a The first widget to compare.
   1101 	 * @param array $widget_b The second widget to compare.
   1102 	 * @return int Reorder position for the current widget comparison.
   1103 	 */
   1104 	protected function _sort_name_callback( $widget_a, $widget_b ) {
   1105 		return strnatcasecmp( $widget_a['name'], $widget_b['name'] );
   1106 	}
   1107 
   1108 	/**
   1109 	 * Retrieves the widget control markup.
   1110 	 *
   1111 	 * @since 3.9.0
   1112 	 *
   1113 	 * @param array $args Widget control arguments.
   1114 	 * @return string Widget control form HTML markup.
   1115 	 */
   1116 	public function get_widget_control( $args ) {
   1117 		$args[0]['before_form']           = '<div class="form">';
   1118 		$args[0]['after_form']            = '</div><!-- .form -->';
   1119 		$args[0]['before_widget_content'] = '<div class="widget-content">';
   1120 		$args[0]['after_widget_content']  = '</div><!-- .widget-content -->';
   1121 		ob_start();
   1122 		wp_widget_control( ...$args );
   1123 		$control_tpl = ob_get_clean();
   1124 		return $control_tpl;
   1125 	}
   1126 
   1127 	/**
   1128 	 * Retrieves the widget control markup parts.
   1129 	 *
   1130 	 * @since 4.4.0
   1131 	 *
   1132 	 * @param array $args Widget control arguments.
   1133 	 * @return array {
   1134 	 *     @type string $control Markup for widget control wrapping form.
   1135 	 *     @type string $content The contents of the widget form itself.
   1136 	 * }
   1137 	 */
   1138 	public function get_widget_control_parts( $args ) {
   1139 		$args[0]['before_widget_content'] = '<div class="widget-content">';
   1140 		$args[0]['after_widget_content']  = '</div><!-- .widget-content -->';
   1141 		$control_markup                   = $this->get_widget_control( $args );
   1142 
   1143 		$content_start_pos = strpos( $control_markup, $args[0]['before_widget_content'] );
   1144 		$content_end_pos   = strrpos( $control_markup, $args[0]['after_widget_content'] );
   1145 
   1146 		$control  = substr( $control_markup, 0, $content_start_pos + strlen( $args[0]['before_widget_content'] ) );
   1147 		$control .= substr( $control_markup, $content_end_pos );
   1148 		$content  = trim(
   1149 			substr(
   1150 				$control_markup,
   1151 				$content_start_pos + strlen( $args[0]['before_widget_content'] ),
   1152 				$content_end_pos - $content_start_pos - strlen( $args[0]['before_widget_content'] )
   1153 			)
   1154 		);
   1155 
   1156 		return compact( 'control', 'content' );
   1157 	}
   1158 
   1159 	/**
   1160 	 * Adds hooks for the Customizer preview.
   1161 	 *
   1162 	 * @since 3.9.0
   1163 	 */
   1164 	public function customize_preview_init() {
   1165 		add_action( 'wp_enqueue_scripts', array( $this, 'customize_preview_enqueue' ) );
   1166 		add_action( 'wp_print_styles', array( $this, 'print_preview_css' ), 1 );
   1167 		add_action( 'wp_footer', array( $this, 'export_preview_data' ), 20 );
   1168 	}
   1169 
   1170 	/**
   1171 	 * Refreshes the nonce for widget updates.
   1172 	 *
   1173 	 * @since 4.2.0
   1174 	 *
   1175 	 * @param array $nonces Array of nonces.
   1176 	 * @return array Array of nonces.
   1177 	 */
   1178 	public function refresh_nonces( $nonces ) {
   1179 		$nonces['update-widget'] = wp_create_nonce( 'update-widget' );
   1180 		return $nonces;
   1181 	}
   1182 
   1183 	/**
   1184 	 * Tells the script loader to load the scripts and styles of custom blocks
   1185 	 * if the widgets block editor is enabled.
   1186 	 *
   1187 	 * @since 5.8.0
   1188 	 *
   1189 	 * @param bool $is_block_editor_screen Current decision about loading block assets.
   1190 	 * @return bool Filtered decision about loading block assets.
   1191 	 */
   1192 	public function should_load_block_editor_scripts_and_styles( $is_block_editor_screen ) {
   1193 		if ( wp_use_widgets_block_editor() ) {
   1194 			return true;
   1195 		}
   1196 
   1197 		return $is_block_editor_screen;
   1198 	}
   1199 
   1200 	/**
   1201 	 * When previewing, ensures the proper previewing widgets are used.
   1202 	 *
   1203 	 * Because wp_get_sidebars_widgets() gets called early at {@see 'init' } (via
   1204 	 * wp_convert_widget_settings()) and can set global variable `$_wp_sidebars_widgets`
   1205 	 * to the value of `get_option( 'sidebars_widgets' )` before the Customizer preview
   1206 	 * filter is added, it has to be reset after the filter has been added.
   1207 	 *
   1208 	 * @since 3.9.0
   1209 	 *
   1210 	 * @param array $sidebars_widgets List of widgets for the current sidebar.
   1211 	 * @return array
   1212 	 */
   1213 	public function preview_sidebars_widgets( $sidebars_widgets ) {
   1214 		$sidebars_widgets = get_option( 'sidebars_widgets', array() );
   1215 
   1216 		unset( $sidebars_widgets['array_version'] );
   1217 		return $sidebars_widgets;
   1218 	}
   1219 
   1220 	/**
   1221 	 * Enqueues scripts for the Customizer preview.
   1222 	 *
   1223 	 * @since 3.9.0
   1224 	 */
   1225 	public function customize_preview_enqueue() {
   1226 		wp_enqueue_script( 'customize-preview-widgets' );
   1227 	}
   1228 
   1229 	/**
   1230 	 * Inserts default style for highlighted widget at early point so theme
   1231 	 * stylesheet can override.
   1232 	 *
   1233 	 * @since 3.9.0
   1234 	 */
   1235 	public function print_preview_css() {
   1236 		?>
   1237 		<style>
   1238 		.widget-customizer-highlighted-widget {
   1239 			outline: none;
   1240 			-webkit-box-shadow: 0 0 2px rgba(30, 140, 190, 0.8);
   1241 			box-shadow: 0 0 2px rgba(30, 140, 190, 0.8);
   1242 			position: relative;
   1243 			z-index: 1;
   1244 		}
   1245 		</style>
   1246 		<?php
   1247 	}
   1248 
   1249 	/**
   1250 	 * Communicates the sidebars that appeared on the page at the very end of the page,
   1251 	 * and at the very end of the wp_footer,
   1252 	 *
   1253 	 * @since 3.9.0
   1254 	 *
   1255 	 * @global array $wp_registered_sidebars
   1256 	 * @global array $wp_registered_widgets
   1257 	 */
   1258 	public function export_preview_data() {
   1259 		global $wp_registered_sidebars, $wp_registered_widgets;
   1260 
   1261 		$switched_locale = switch_to_locale( get_user_locale() );
   1262 
   1263 		$l10n = array(
   1264 			'widgetTooltip' => __( 'Shift-click to edit this widget.' ),
   1265 		);
   1266 
   1267 		if ( $switched_locale ) {
   1268 			restore_previous_locale();
   1269 		}
   1270 
   1271 		$rendered_sidebars = array_filter( $this->rendered_sidebars );
   1272 		$rendered_widgets  = array_filter( $this->rendered_widgets );
   1273 
   1274 		// Prepare Customizer settings to pass to JavaScript.
   1275 		$settings = array(
   1276 			'renderedSidebars'            => array_fill_keys( array_keys( $rendered_sidebars ), true ),
   1277 			'renderedWidgets'             => array_fill_keys( array_keys( $rendered_widgets ), true ),
   1278 			'registeredSidebars'          => array_values( $wp_registered_sidebars ),
   1279 			'registeredWidgets'           => $wp_registered_widgets,
   1280 			'l10n'                        => $l10n,
   1281 			'selectiveRefreshableWidgets' => $this->get_selective_refreshable_widgets(),
   1282 		);
   1283 
   1284 		foreach ( $settings['registeredWidgets'] as &$registered_widget ) {
   1285 			unset( $registered_widget['callback'] ); // May not be JSON-serializeable.
   1286 		}
   1287 
   1288 		?>
   1289 		<script type="text/javascript">
   1290 			var _wpWidgetCustomizerPreviewSettings = <?php echo wp_json_encode( $settings ); ?>;
   1291 		</script>
   1292 		<?php
   1293 	}
   1294 
   1295 	/**
   1296 	 * Tracks the widgets that were rendered.
   1297 	 *
   1298 	 * @since 3.9.0
   1299 	 *
   1300 	 * @param array $widget Rendered widget to tally.
   1301 	 */
   1302 	public function tally_rendered_widgets( $widget ) {
   1303 		$this->rendered_widgets[ $widget['id'] ] = true;
   1304 	}
   1305 
   1306 	/**
   1307 	 * Determine if a widget is rendered on the page.
   1308 	 *
   1309 	 * @since 4.0.0
   1310 	 *
   1311 	 * @param string $widget_id Widget ID to check.
   1312 	 * @return bool Whether the widget is rendered.
   1313 	 */
   1314 	public function is_widget_rendered( $widget_id ) {
   1315 		return ! empty( $this->rendered_widgets[ $widget_id ] );
   1316 	}
   1317 
   1318 	/**
   1319 	 * Determines if a sidebar is rendered on the page.
   1320 	 *
   1321 	 * @since 4.0.0
   1322 	 *
   1323 	 * @param string $sidebar_id Sidebar ID to check.
   1324 	 * @return bool Whether the sidebar is rendered.
   1325 	 */
   1326 	public function is_sidebar_rendered( $sidebar_id ) {
   1327 		return ! empty( $this->rendered_sidebars[ $sidebar_id ] );
   1328 	}
   1329 
   1330 	/**
   1331 	 * Tallies the sidebars rendered via is_active_sidebar().
   1332 	 *
   1333 	 * Keep track of the times that is_active_sidebar() is called in the template,
   1334 	 * and assume that this means that the sidebar would be rendered on the template
   1335 	 * if there were widgets populating it.
   1336 	 *
   1337 	 * @since 3.9.0
   1338 	 *
   1339 	 * @param bool   $is_active  Whether the sidebar is active.
   1340 	 * @param string $sidebar_id Sidebar ID.
   1341 	 * @return bool Whether the sidebar is active.
   1342 	 */
   1343 	public function tally_sidebars_via_is_active_sidebar_calls( $is_active, $sidebar_id ) {
   1344 		if ( is_registered_sidebar( $sidebar_id ) ) {
   1345 			$this->rendered_sidebars[ $sidebar_id ] = true;
   1346 		}
   1347 
   1348 		/*
   1349 		 * We may need to force this to true, and also force-true the value
   1350 		 * for 'dynamic_sidebar_has_widgets' if we want to ensure that there
   1351 		 * is an area to drop widgets into, if the sidebar is empty.
   1352 		 */
   1353 		return $is_active;
   1354 	}
   1355 
   1356 	/**
   1357 	 * Tallies the sidebars rendered via dynamic_sidebar().
   1358 	 *
   1359 	 * Keep track of the times that dynamic_sidebar() is called in the template,
   1360 	 * and assume this means the sidebar would be rendered on the template if
   1361 	 * there were widgets populating it.
   1362 	 *
   1363 	 * @since 3.9.0
   1364 	 *
   1365 	 * @param bool   $has_widgets Whether the current sidebar has widgets.
   1366 	 * @param string $sidebar_id  Sidebar ID.
   1367 	 * @return bool Whether the current sidebar has widgets.
   1368 	 */
   1369 	public function tally_sidebars_via_dynamic_sidebar_calls( $has_widgets, $sidebar_id ) {
   1370 		if ( is_registered_sidebar( $sidebar_id ) ) {
   1371 			$this->rendered_sidebars[ $sidebar_id ] = true;
   1372 		}
   1373 
   1374 		/*
   1375 		 * We may need to force this to true, and also force-true the value
   1376 		 * for 'is_active_sidebar' if we want to ensure there is an area to
   1377 		 * drop widgets into, if the sidebar is empty.
   1378 		 */
   1379 		return $has_widgets;
   1380 	}
   1381 
   1382 	/**
   1383 	 * Retrieves MAC for a serialized widget instance string.
   1384 	 *
   1385 	 * Allows values posted back from JS to be rejected if any tampering of the
   1386 	 * data has occurred.
   1387 	 *
   1388 	 * @since 3.9.0
   1389 	 *
   1390 	 * @param string $serialized_instance Widget instance.
   1391 	 * @return string MAC for serialized widget instance.
   1392 	 */
   1393 	protected function get_instance_hash_key( $serialized_instance ) {
   1394 		return wp_hash( $serialized_instance );
   1395 	}
   1396 
   1397 	/**
   1398 	 * Sanitizes a widget instance.
   1399 	 *
   1400 	 * Unserialize the JS-instance for storing in the options. It's important that this filter
   1401 	 * only get applied to an instance *once*.
   1402 	 *
   1403 	 * @since 3.9.0
   1404 	 * @since 5.8.0 Added the `$id_base` parameter.
   1405 	 *
   1406 	 * @global WP_Widget_Factory $wp_widget_factory
   1407 	 *
   1408 	 * @param array  $value   Widget instance to sanitize.
   1409 	 * @param string $id_base Optional. Base of the ID of the widget being sanitized. Default null.
   1410 	 * @return array|void Sanitized widget instance.
   1411 	 */
   1412 	public function sanitize_widget_instance( $value, $id_base = null ) {
   1413 		global $wp_widget_factory;
   1414 
   1415 		if ( array() === $value ) {
   1416 			return $value;
   1417 		}
   1418 
   1419 		if ( isset( $value['raw_instance'] ) && $id_base && wp_use_widgets_block_editor() ) {
   1420 			$widget_object = $wp_widget_factory->get_widget_object( $id_base );
   1421 			if ( ! empty( $widget_object->widget_options['show_instance_in_rest'] ) ) {
   1422 				if ( 'block' === $id_base && ! current_user_can( 'unfiltered_html' ) ) {
   1423 					/*
   1424 					 * The content of the 'block' widget is not filtered on the
   1425 					 * fly while editing. Filter the content here to prevent
   1426 					 * vulnerabilities.
   1427 					 */
   1428 					$value['raw_instance']['content'] = wp_kses_post( $value['raw_instance']['content'] );
   1429 				}
   1430 
   1431 				return $value['raw_instance'];
   1432 			}
   1433 		}
   1434 
   1435 		if (
   1436 			empty( $value['is_widget_customizer_js_value'] ) ||
   1437 			empty( $value['instance_hash_key'] ) ||
   1438 			empty( $value['encoded_serialized_instance'] )
   1439 		) {
   1440 			return;
   1441 		}
   1442 
   1443 		$decoded = base64_decode( $value['encoded_serialized_instance'], true );
   1444 		if ( false === $decoded ) {
   1445 			return;
   1446 		}
   1447 
   1448 		if ( ! hash_equals( $this->get_instance_hash_key( $decoded ), $value['instance_hash_key'] ) ) {
   1449 			return;
   1450 		}
   1451 
   1452 		$instance = unserialize( $decoded );
   1453 		if ( false === $instance ) {
   1454 			return;
   1455 		}
   1456 
   1457 		return $instance;
   1458 	}
   1459 
   1460 	/**
   1461 	 * Converts a widget instance into JSON-representable format.
   1462 	 *
   1463 	 * @since 3.9.0
   1464 	 * @since 5.8.0 Added the `$id_base` parameter.
   1465 	 *
   1466 	 * @global WP_Widget_Factory $wp_widget_factory
   1467 	 *
   1468 	 * @param array  $value   Widget instance to convert to JSON.
   1469 	 * @param string $id_base Optional. Base of the ID of the widget being sanitized. Default null.
   1470 	 * @return array JSON-converted widget instance.
   1471 	 */
   1472 	public function sanitize_widget_js_instance( $value, $id_base = null ) {
   1473 		global $wp_widget_factory;
   1474 
   1475 		if ( empty( $value['is_widget_customizer_js_value'] ) ) {
   1476 			$serialized = serialize( $value );
   1477 
   1478 			$js_value = array(
   1479 				'encoded_serialized_instance'   => base64_encode( $serialized ),
   1480 				'title'                         => empty( $value['title'] ) ? '' : $value['title'],
   1481 				'is_widget_customizer_js_value' => true,
   1482 				'instance_hash_key'             => $this->get_instance_hash_key( $serialized ),
   1483 			);
   1484 
   1485 			if ( $id_base && wp_use_widgets_block_editor() ) {
   1486 				$widget_object = $wp_widget_factory->get_widget_object( $id_base );
   1487 				if ( ! empty( $widget_object->widget_options['show_instance_in_rest'] ) ) {
   1488 					$js_value['raw_instance'] = (object) $value;
   1489 				}
   1490 			}
   1491 
   1492 			return $js_value;
   1493 		}
   1494 
   1495 		return $value;
   1496 	}
   1497 
   1498 	/**
   1499 	 * Strips out widget IDs for widgets which are no longer registered.
   1500 	 *
   1501 	 * One example where this might happen is when a plugin orphans a widget
   1502 	 * in a sidebar upon deactivation.
   1503 	 *
   1504 	 * @since 3.9.0
   1505 	 *
   1506 	 * @global array $wp_registered_widgets
   1507 	 *
   1508 	 * @param array $widget_ids List of widget IDs.
   1509 	 * @return array Parsed list of widget IDs.
   1510 	 */
   1511 	public function sanitize_sidebar_widgets_js_instance( $widget_ids ) {
   1512 		global $wp_registered_widgets;
   1513 		$widget_ids = array_values( array_intersect( $widget_ids, array_keys( $wp_registered_widgets ) ) );
   1514 		return $widget_ids;
   1515 	}
   1516 
   1517 	/**
   1518 	 * Finds and invokes the widget update and control callbacks.
   1519 	 *
   1520 	 * Requires that `$_POST` be populated with the instance data.
   1521 	 *
   1522 	 * @since 3.9.0
   1523 	 *
   1524 	 * @global array $wp_registered_widget_updates
   1525 	 * @global array $wp_registered_widget_controls
   1526 	 *
   1527 	 * @param string $widget_id Widget ID.
   1528 	 * @return array|WP_Error Array containing the updated widget information.
   1529 	 *                        A WP_Error object, otherwise.
   1530 	 */
   1531 	public function call_widget_update( $widget_id ) {
   1532 		global $wp_registered_widget_updates, $wp_registered_widget_controls;
   1533 
   1534 		$setting_id = $this->get_setting_id( $widget_id );
   1535 
   1536 		/*
   1537 		 * Make sure that other setting changes have previewed since this widget
   1538 		 * may depend on them (e.g. Menus being present for Navigation Menu widget).
   1539 		 */
   1540 		if ( ! did_action( 'customize_preview_init' ) ) {
   1541 			foreach ( $this->manager->settings() as $setting ) {
   1542 				if ( $setting->id !== $setting_id ) {
   1543 					$setting->preview();
   1544 				}
   1545 			}
   1546 		}
   1547 
   1548 		$this->start_capturing_option_updates();
   1549 		$parsed_id   = $this->parse_widget_id( $widget_id );
   1550 		$option_name = 'widget_' . $parsed_id['id_base'];
   1551 
   1552 		/*
   1553 		 * If a previously-sanitized instance is provided, populate the input vars
   1554 		 * with its values so that the widget update callback will read this instance
   1555 		 */
   1556 		$added_input_vars = array();
   1557 		if ( ! empty( $_POST['sanitized_widget_setting'] ) ) {
   1558 			$sanitized_widget_setting = json_decode( $this->get_post_value( 'sanitized_widget_setting' ), true );
   1559 			if ( false === $sanitized_widget_setting ) {
   1560 				$this->stop_capturing_option_updates();
   1561 				return new WP_Error( 'widget_setting_malformed' );
   1562 			}
   1563 
   1564 			$instance = $this->sanitize_widget_instance( $sanitized_widget_setting, $parsed_id['id_base'] );
   1565 			if ( is_null( $instance ) ) {
   1566 				$this->stop_capturing_option_updates();
   1567 				return new WP_Error( 'widget_setting_unsanitized' );
   1568 			}
   1569 
   1570 			if ( ! is_null( $parsed_id['number'] ) ) {
   1571 				$value                         = array();
   1572 				$value[ $parsed_id['number'] ] = $instance;
   1573 				$key                           = 'widget-' . $parsed_id['id_base'];
   1574 				$_REQUEST[ $key ]              = wp_slash( $value );
   1575 				$_POST[ $key ]                 = $_REQUEST[ $key ];
   1576 				$added_input_vars[]            = $key;
   1577 			} else {
   1578 				foreach ( $instance as $key => $value ) {
   1579 					$_REQUEST[ $key ]   = wp_slash( $value );
   1580 					$_POST[ $key ]      = $_REQUEST[ $key ];
   1581 					$added_input_vars[] = $key;
   1582 				}
   1583 			}
   1584 		}
   1585 
   1586 		// Invoke the widget update callback.
   1587 		foreach ( (array) $wp_registered_widget_updates as $name => $control ) {
   1588 			if ( $name === $parsed_id['id_base'] && is_callable( $control['callback'] ) ) {
   1589 				ob_start();
   1590 				call_user_func_array( $control['callback'], $control['params'] );
   1591 				ob_end_clean();
   1592 				break;
   1593 			}
   1594 		}
   1595 
   1596 		// Clean up any input vars that were manually added.
   1597 		foreach ( $added_input_vars as $key ) {
   1598 			unset( $_POST[ $key ] );
   1599 			unset( $_REQUEST[ $key ] );
   1600 		}
   1601 
   1602 		// Make sure the expected option was updated.
   1603 		if ( 0 !== $this->count_captured_options() ) {
   1604 			if ( $this->count_captured_options() > 1 ) {
   1605 				$this->stop_capturing_option_updates();
   1606 				return new WP_Error( 'widget_setting_too_many_options' );
   1607 			}
   1608 
   1609 			$updated_option_name = key( $this->get_captured_options() );
   1610 			if ( $updated_option_name !== $option_name ) {
   1611 				$this->stop_capturing_option_updates();
   1612 				return new WP_Error( 'widget_setting_unexpected_option' );
   1613 			}
   1614 		}
   1615 
   1616 		// Obtain the widget instance.
   1617 		$option = $this->get_captured_option( $option_name );
   1618 		if ( null !== $parsed_id['number'] ) {
   1619 			$instance = $option[ $parsed_id['number'] ];
   1620 		} else {
   1621 			$instance = $option;
   1622 		}
   1623 
   1624 		/*
   1625 		 * Override the incoming $_POST['customized'] for a newly-created widget's
   1626 		 * setting with the new $instance so that the preview filter currently
   1627 		 * in place from WP_Customize_Setting::preview() will use this value
   1628 		 * instead of the default widget instance value (an empty array).
   1629 		 */
   1630 		$this->manager->set_post_value( $setting_id, $this->sanitize_widget_js_instance( $instance, $parsed_id['id_base'] ) );
   1631 
   1632 		// Obtain the widget control with the updated instance in place.
   1633 		ob_start();
   1634 		$form = $wp_registered_widget_controls[ $widget_id ];
   1635 		if ( $form ) {
   1636 			call_user_func_array( $form['callback'], $form['params'] );
   1637 		}
   1638 		$form = ob_get_clean();
   1639 
   1640 		$this->stop_capturing_option_updates();
   1641 
   1642 		return compact( 'instance', 'form' );
   1643 	}
   1644 
   1645 	/**
   1646 	 * Updates widget settings asynchronously.
   1647 	 *
   1648 	 * Allows the Customizer to update a widget using its form, but return the new
   1649 	 * instance info via Ajax instead of saving it to the options table.
   1650 	 *
   1651 	 * Most code here copied from wp_ajax_save_widget().
   1652 	 *
   1653 	 * @since 3.9.0
   1654 	 *
   1655 	 * @see wp_ajax_save_widget()
   1656 	 */
   1657 	public function wp_ajax_update_widget() {
   1658 
   1659 		if ( ! is_user_logged_in() ) {
   1660 			wp_die( 0 );
   1661 		}
   1662 
   1663 		check_ajax_referer( 'update-widget', 'nonce' );
   1664 
   1665 		if ( ! current_user_can( 'edit_theme_options' ) ) {
   1666 			wp_die( -1 );
   1667 		}
   1668 
   1669 		if ( empty( $_POST['widget-id'] ) ) {
   1670 			wp_send_json_error( 'missing_widget-id' );
   1671 		}
   1672 
   1673 		/** This action is documented in wp-admin/includes/ajax-actions.php */
   1674 		do_action( 'load-widgets.php' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
   1675 
   1676 		/** This action is documented in wp-admin/includes/ajax-actions.php */
   1677 		do_action( 'widgets.php' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
   1678 
   1679 		/** This action is documented in wp-admin/widgets.php */
   1680 		do_action( 'sidebar_admin_setup' );
   1681 
   1682 		$widget_id = $this->get_post_value( 'widget-id' );
   1683 		$parsed_id = $this->parse_widget_id( $widget_id );
   1684 		$id_base   = $parsed_id['id_base'];
   1685 
   1686 		$is_updating_widget_template = (
   1687 			isset( $_POST[ 'widget-' . $id_base ] )
   1688 			&&
   1689 			is_array( $_POST[ 'widget-' . $id_base ] )
   1690 			&&
   1691 			preg_match( '/__i__|%i%/', key( $_POST[ 'widget-' . $id_base ] ) )
   1692 		);
   1693 		if ( $is_updating_widget_template ) {
   1694 			wp_send_json_error( 'template_widget_not_updatable' );
   1695 		}
   1696 
   1697 		$updated_widget = $this->call_widget_update( $widget_id ); // => {instance,form}
   1698 		if ( is_wp_error( $updated_widget ) ) {
   1699 			wp_send_json_error( $updated_widget->get_error_code() );
   1700 		}
   1701 
   1702 		$form     = $updated_widget['form'];
   1703 		$instance = $this->sanitize_widget_js_instance( $updated_widget['instance'], $id_base );
   1704 
   1705 		wp_send_json_success( compact( 'form', 'instance' ) );
   1706 	}
   1707 
   1708 	/*
   1709 	 * Selective Refresh Methods
   1710 	 */
   1711 
   1712 	/**
   1713 	 * Filters arguments for dynamic widget partials.
   1714 	 *
   1715 	 * @since 4.5.0
   1716 	 *
   1717 	 * @param array|false $partial_args Partial arguments.
   1718 	 * @param string      $partial_id   Partial ID.
   1719 	 * @return array (Maybe) modified partial arguments.
   1720 	 */
   1721 	public function customize_dynamic_partial_args( $partial_args, $partial_id ) {
   1722 		if ( ! current_theme_supports( 'customize-selective-refresh-widgets' ) ) {
   1723 			return $partial_args;
   1724 		}
   1725 
   1726 		if ( preg_match( '/^widget\[(?P<widget_id>.+)\]$/', $partial_id, $matches ) ) {
   1727 			if ( false === $partial_args ) {
   1728 				$partial_args = array();
   1729 			}
   1730 			$partial_args = array_merge(
   1731 				$partial_args,
   1732 				array(
   1733 					'type'                => 'widget',
   1734 					'render_callback'     => array( $this, 'render_widget_partial' ),
   1735 					'container_inclusive' => true,
   1736 					'settings'            => array( $this->get_setting_id( $matches['widget_id'] ) ),
   1737 					'capability'          => 'edit_theme_options',
   1738 				)
   1739 			);
   1740 		}
   1741 
   1742 		return $partial_args;
   1743 	}
   1744 
   1745 	/**
   1746 	 * Adds hooks for selective refresh.
   1747 	 *
   1748 	 * @since 4.5.0
   1749 	 */
   1750 	public function selective_refresh_init() {
   1751 		if ( ! current_theme_supports( 'customize-selective-refresh-widgets' ) ) {
   1752 			return;
   1753 		}
   1754 		add_filter( 'dynamic_sidebar_params', array( $this, 'filter_dynamic_sidebar_params' ) );
   1755 		add_filter( 'wp_kses_allowed_html', array( $this, 'filter_wp_kses_allowed_data_attributes' ) );
   1756 		add_action( 'dynamic_sidebar_before', array( $this, 'start_dynamic_sidebar' ) );
   1757 		add_action( 'dynamic_sidebar_after', array( $this, 'end_dynamic_sidebar' ) );
   1758 	}
   1759 
   1760 	/**
   1761 	 * Inject selective refresh data attributes into widget container elements.
   1762 	 *
   1763 	 * @since 4.5.0
   1764 	 *
   1765 	 * @param array $params {
   1766 	 *     Dynamic sidebar params.
   1767 	 *
   1768 	 *     @type array $args        Sidebar args.
   1769 	 *     @type array $widget_args Widget args.
   1770 	 * }
   1771 	 * @see WP_Customize_Nav_Menus::filter_wp_nav_menu_args()
   1772 	 *
   1773 	 * @return array Params.
   1774 	 */
   1775 	public function filter_dynamic_sidebar_params( $params ) {
   1776 		$sidebar_args = array_merge(
   1777 			array(
   1778 				'before_widget' => '',
   1779 				'after_widget'  => '',
   1780 			),
   1781 			$params[0]
   1782 		);
   1783 
   1784 		// Skip widgets not in a registered sidebar or ones which lack a proper wrapper element to attach the data-* attributes to.
   1785 		$matches  = array();
   1786 		$is_valid = (
   1787 			isset( $sidebar_args['id'] )
   1788 			&&
   1789 			is_registered_sidebar( $sidebar_args['id'] )
   1790 			&&
   1791 			( isset( $this->current_dynamic_sidebar_id_stack[0] ) && $this->current_dynamic_sidebar_id_stack[0] === $sidebar_args['id'] )
   1792 			&&
   1793 			preg_match( '#^<(?P<tag_name>\w+)#', $sidebar_args['before_widget'], $matches )
   1794 		);
   1795 		if ( ! $is_valid ) {
   1796 			return $params;
   1797 		}
   1798 		$this->before_widget_tags_seen[ $matches['tag_name'] ] = true;
   1799 
   1800 		$context = array(
   1801 			'sidebar_id' => $sidebar_args['id'],
   1802 		);
   1803 		if ( isset( $this->context_sidebar_instance_number ) ) {
   1804 			$context['sidebar_instance_number'] = $this->context_sidebar_instance_number;
   1805 		} elseif ( isset( $sidebar_args['id'] ) && isset( $this->sidebar_instance_count[ $sidebar_args['id'] ] ) ) {
   1806 			$context['sidebar_instance_number'] = $this->sidebar_instance_count[ $sidebar_args['id'] ];
   1807 		}
   1808 
   1809 		$attributes                    = sprintf( ' data-customize-partial-id="%s"', esc_attr( 'widget[' . $sidebar_args['widget_id'] . ']' ) );
   1810 		$attributes                   .= ' data-customize-partial-type="widget"';
   1811 		$attributes                   .= sprintf( ' data-customize-partial-placement-context="%s"', esc_attr( wp_json_encode( $context ) ) );
   1812 		$attributes                   .= sprintf( ' data-customize-widget-id="%s"', esc_attr( $sidebar_args['widget_id'] ) );
   1813 		$sidebar_args['before_widget'] = preg_replace( '#^(<\w+)#', '$1 ' . $attributes, $sidebar_args['before_widget'] );
   1814 
   1815 		$params[0] = $sidebar_args;
   1816 		return $params;
   1817 	}
   1818 
   1819 	/**
   1820 	 * List of the tag names seen for before_widget strings.
   1821 	 *
   1822 	 * This is used in the {@see 'filter_wp_kses_allowed_html'} filter to ensure that the
   1823 	 * data-* attributes can be allowed.
   1824 	 *
   1825 	 * @since 4.5.0
   1826 	 * @var array
   1827 	 */
   1828 	protected $before_widget_tags_seen = array();
   1829 
   1830 	/**
   1831 	 * Ensures the HTML data-* attributes for selective refresh are allowed by kses.
   1832 	 *
   1833 	 * This is needed in case the `$before_widget` is run through wp_kses() when printed.
   1834 	 *
   1835 	 * @since 4.5.0
   1836 	 *
   1837 	 * @param array $allowed_html Allowed HTML.
   1838 	 * @return array (Maybe) modified allowed HTML.
   1839 	 */
   1840 	public function filter_wp_kses_allowed_data_attributes( $allowed_html ) {
   1841 		foreach ( array_keys( $this->before_widget_tags_seen ) as $tag_name ) {
   1842 			if ( ! isset( $allowed_html[ $tag_name ] ) ) {
   1843 				$allowed_html[ $tag_name ] = array();
   1844 			}
   1845 			$allowed_html[ $tag_name ] = array_merge(
   1846 				$allowed_html[ $tag_name ],
   1847 				array_fill_keys(
   1848 					array(
   1849 						'data-customize-partial-id',
   1850 						'data-customize-partial-type',
   1851 						'data-customize-partial-placement-context',
   1852 						'data-customize-partial-widget-id',
   1853 						'data-customize-partial-options',
   1854 					),
   1855 					true
   1856 				)
   1857 			);
   1858 		}
   1859 		return $allowed_html;
   1860 	}
   1861 
   1862 	/**
   1863 	 * Keep track of the number of times that dynamic_sidebar() was called for a given sidebar index.
   1864 	 *
   1865 	 * This helps facilitate the uncommon scenario where a single sidebar is rendered multiple times on a template.
   1866 	 *
   1867 	 * @since 4.5.0
   1868 	 * @var array
   1869 	 */
   1870 	protected $sidebar_instance_count = array();
   1871 
   1872 	/**
   1873 	 * The current request's sidebar_instance_number context.
   1874 	 *
   1875 	 * @since 4.5.0
   1876 	 * @var int|null
   1877 	 */
   1878 	protected $context_sidebar_instance_number;
   1879 
   1880 	/**
   1881 	 * Current sidebar ID being rendered.
   1882 	 *
   1883 	 * @since 4.5.0
   1884 	 * @var array
   1885 	 */
   1886 	protected $current_dynamic_sidebar_id_stack = array();
   1887 
   1888 	/**
   1889 	 * Begins keeping track of the current sidebar being rendered.
   1890 	 *
   1891 	 * Insert marker before widgets are rendered in a dynamic sidebar.
   1892 	 *
   1893 	 * @since 4.5.0
   1894 	 *
   1895 	 * @param int|string $index Index, name, or ID of the dynamic sidebar.
   1896 	 */
   1897 	public function start_dynamic_sidebar( $index ) {
   1898 		array_unshift( $this->current_dynamic_sidebar_id_stack, $index );
   1899 		if ( ! isset( $this->sidebar_instance_count[ $index ] ) ) {
   1900 			$this->sidebar_instance_count[ $index ] = 0;
   1901 		}
   1902 		$this->sidebar_instance_count[ $index ] += 1;
   1903 		if ( ! $this->manager->selective_refresh->is_render_partials_request() ) {
   1904 			printf( "\n<!--dynamic_sidebar_before:%s:%d-->\n", esc_html( $index ), (int) $this->sidebar_instance_count[ $index ] );
   1905 		}
   1906 	}
   1907 
   1908 	/**
   1909 	 * Finishes keeping track of the current sidebar being rendered.
   1910 	 *
   1911 	 * Inserts a marker after widgets are rendered in a dynamic sidebar.
   1912 	 *
   1913 	 * @since 4.5.0
   1914 	 *
   1915 	 * @param int|string $index Index, name, or ID of the dynamic sidebar.
   1916 	 */
   1917 	public function end_dynamic_sidebar( $index ) {
   1918 		array_shift( $this->current_dynamic_sidebar_id_stack );
   1919 		if ( ! $this->manager->selective_refresh->is_render_partials_request() ) {
   1920 			printf( "\n<!--dynamic_sidebar_after:%s:%d-->\n", esc_html( $index ), (int) $this->sidebar_instance_count[ $index ] );
   1921 		}
   1922 	}
   1923 
   1924 	/**
   1925 	 * Current sidebar being rendered.
   1926 	 *
   1927 	 * @since 4.5.0
   1928 	 * @var string|null
   1929 	 */
   1930 	protected $rendering_widget_id;
   1931 
   1932 	/**
   1933 	 * Current widget being rendered.
   1934 	 *
   1935 	 * @since 4.5.0
   1936 	 * @var string|null
   1937 	 */
   1938 	protected $rendering_sidebar_id;
   1939 
   1940 	/**
   1941 	 * Filters sidebars_widgets to ensure the currently-rendered widget is the only widget in the current sidebar.
   1942 	 *
   1943 	 * @since 4.5.0
   1944 	 *
   1945 	 * @param array $sidebars_widgets Sidebars widgets.
   1946 	 * @return array Filtered sidebars widgets.
   1947 	 */
   1948 	public function filter_sidebars_widgets_for_rendering_widget( $sidebars_widgets ) {
   1949 		$sidebars_widgets[ $this->rendering_sidebar_id ] = array( $this->rendering_widget_id );
   1950 		return $sidebars_widgets;
   1951 	}
   1952 
   1953 	/**
   1954 	 * Renders a specific widget using the supplied sidebar arguments.
   1955 	 *
   1956 	 * @since 4.5.0
   1957 	 *
   1958 	 * @see dynamic_sidebar()
   1959 	 *
   1960 	 * @param WP_Customize_Partial $partial Partial.
   1961 	 * @param array                $context {
   1962 	 *     Sidebar args supplied as container context.
   1963 	 *
   1964 	 *     @type string $sidebar_id              ID for sidebar for widget to render into.
   1965 	 *     @type int    $sidebar_instance_number Disambiguating instance number.
   1966 	 * }
   1967 	 * @return string|false
   1968 	 */
   1969 	public function render_widget_partial( $partial, $context ) {
   1970 		$id_data   = $partial->id_data();
   1971 		$widget_id = array_shift( $id_data['keys'] );
   1972 
   1973 		if ( ! is_array( $context )
   1974 			|| empty( $context['sidebar_id'] )
   1975 			|| ! is_registered_sidebar( $context['sidebar_id'] )
   1976 		) {
   1977 			return false;
   1978 		}
   1979 
   1980 		$this->rendering_sidebar_id = $context['sidebar_id'];
   1981 
   1982 		if ( isset( $context['sidebar_instance_number'] ) ) {
   1983 			$this->context_sidebar_instance_number = (int) $context['sidebar_instance_number'];
   1984 		}
   1985 
   1986 		// Filter sidebars_widgets so that only the queried widget is in the sidebar.
   1987 		$this->rendering_widget_id = $widget_id;
   1988 
   1989 		$filter_callback = array( $this, 'filter_sidebars_widgets_for_rendering_widget' );
   1990 		add_filter( 'sidebars_widgets', $filter_callback, 1000 );
   1991 
   1992 		// Render the widget.
   1993 		ob_start();
   1994 		$this->rendering_sidebar_id = $context['sidebar_id'];
   1995 		dynamic_sidebar( $this->rendering_sidebar_id );
   1996 		$container = ob_get_clean();
   1997 
   1998 		// Reset variables for next partial render.
   1999 		remove_filter( 'sidebars_widgets', $filter_callback, 1000 );
   2000 
   2001 		$this->context_sidebar_instance_number = null;
   2002 		$this->rendering_sidebar_id            = null;
   2003 		$this->rendering_widget_id             = null;
   2004 
   2005 		return $container;
   2006 	}
   2007 
   2008 	//
   2009 	// Option Update Capturing.
   2010 	//
   2011 
   2012 	/**
   2013 	 * List of captured widget option updates.
   2014 	 *
   2015 	 * @since 3.9.0
   2016 	 * @var array $_captured_options Values updated while option capture is happening.
   2017 	 */
   2018 	protected $_captured_options = array();
   2019 
   2020 	/**
   2021 	 * Whether option capture is currently happening.
   2022 	 *
   2023 	 * @since 3.9.0
   2024 	 * @var bool $_is_current Whether option capture is currently happening or not.
   2025 	 */
   2026 	protected $_is_capturing_option_updates = false;
   2027 
   2028 	/**
   2029 	 * Determines whether the captured option update should be ignored.
   2030 	 *
   2031 	 * @since 3.9.0
   2032 	 *
   2033 	 * @param string $option_name Option name.
   2034 	 * @return bool Whether the option capture is ignored.
   2035 	 */
   2036 	protected function is_option_capture_ignored( $option_name ) {
   2037 		return ( 0 === strpos( $option_name, '_transient_' ) );
   2038 	}
   2039 
   2040 	/**
   2041 	 * Retrieves captured widget option updates.
   2042 	 *
   2043 	 * @since 3.9.0
   2044 	 *
   2045 	 * @return array Array of captured options.
   2046 	 */
   2047 	protected function get_captured_options() {
   2048 		return $this->_captured_options;
   2049 	}
   2050 
   2051 	/**
   2052 	 * Retrieves the option that was captured from being saved.
   2053 	 *
   2054 	 * @since 4.2.0
   2055 	 *
   2056 	 * @param string $option_name Option name.
   2057 	 * @param mixed  $default     Optional. Default value to return if the option does not exist. Default false.
   2058 	 * @return mixed Value set for the option.
   2059 	 */
   2060 	protected function get_captured_option( $option_name, $default = false ) {
   2061 		if ( array_key_exists( $option_name, $this->_captured_options ) ) {
   2062 			$value = $this->_captured_options[ $option_name ];
   2063 		} else {
   2064 			$value = $default;
   2065 		}
   2066 		return $value;
   2067 	}
   2068 
   2069 	/**
   2070 	 * Retrieves the number of captured widget option updates.
   2071 	 *
   2072 	 * @since 3.9.0
   2073 	 *
   2074 	 * @return int Number of updated options.
   2075 	 */
   2076 	protected function count_captured_options() {
   2077 		return count( $this->_captured_options );
   2078 	}
   2079 
   2080 	/**
   2081 	 * Begins keeping track of changes to widget options, caching new values.
   2082 	 *
   2083 	 * @since 3.9.0
   2084 	 */
   2085 	protected function start_capturing_option_updates() {
   2086 		if ( $this->_is_capturing_option_updates ) {
   2087 			return;
   2088 		}
   2089 
   2090 		$this->_is_capturing_option_updates = true;
   2091 
   2092 		add_filter( 'pre_update_option', array( $this, 'capture_filter_pre_update_option' ), 10, 3 );
   2093 	}
   2094 
   2095 	/**
   2096 	 * Pre-filters captured option values before updating.
   2097 	 *
   2098 	 * @since 3.9.0
   2099 	 *
   2100 	 * @param mixed  $new_value   The new option value.
   2101 	 * @param string $option_name Name of the option.
   2102 	 * @param mixed  $old_value   The old option value.
   2103 	 * @return mixed Filtered option value.
   2104 	 */
   2105 	public function capture_filter_pre_update_option( $new_value, $option_name, $old_value ) {
   2106 		if ( $this->is_option_capture_ignored( $option_name ) ) {
   2107 			return $new_value;
   2108 		}
   2109 
   2110 		if ( ! isset( $this->_captured_options[ $option_name ] ) ) {
   2111 			add_filter( "pre_option_{$option_name}", array( $this, 'capture_filter_pre_get_option' ) );
   2112 		}
   2113 
   2114 		$this->_captured_options[ $option_name ] = $new_value;
   2115 
   2116 		return $old_value;
   2117 	}
   2118 
   2119 	/**
   2120 	 * Pre-filters captured option values before retrieving.
   2121 	 *
   2122 	 * @since 3.9.0
   2123 	 *
   2124 	 * @param mixed $value Value to return instead of the option value.
   2125 	 * @return mixed Filtered option value.
   2126 	 */
   2127 	public function capture_filter_pre_get_option( $value ) {
   2128 		$option_name = preg_replace( '/^pre_option_/', '', current_filter() );
   2129 
   2130 		if ( isset( $this->_captured_options[ $option_name ] ) ) {
   2131 			$value = $this->_captured_options[ $option_name ];
   2132 
   2133 			/** This filter is documented in wp-includes/option.php */
   2134 			$value = apply_filters( 'option_' . $option_name, $value, $option_name );
   2135 		}
   2136 
   2137 		return $value;
   2138 	}
   2139 
   2140 	/**
   2141 	 * Undoes any changes to the options since options capture began.
   2142 	 *
   2143 	 * @since 3.9.0
   2144 	 */
   2145 	protected function stop_capturing_option_updates() {
   2146 		if ( ! $this->_is_capturing_option_updates ) {
   2147 			return;
   2148 		}
   2149 
   2150 		remove_filter( 'pre_update_option', array( $this, 'capture_filter_pre_update_option' ), 10 );
   2151 
   2152 		foreach ( array_keys( $this->_captured_options ) as $option_name ) {
   2153 			remove_filter( "pre_option_{$option_name}", array( $this, 'capture_filter_pre_get_option' ) );
   2154 		}
   2155 
   2156 		$this->_captured_options            = array();
   2157 		$this->_is_capturing_option_updates = false;
   2158 	}
   2159 
   2160 	/**
   2161 	 * {@internal Missing Summary}
   2162 	 *
   2163 	 * See the {@see 'customize_dynamic_setting_args'} filter.
   2164 	 *
   2165 	 * @since 3.9.0
   2166 	 * @deprecated 4.2.0 Deprecated in favor of the {@see 'customize_dynamic_setting_args'} filter.
   2167 	 */
   2168 	public function setup_widget_addition_previews() {
   2169 		_deprecated_function( __METHOD__, '4.2.0', 'customize_dynamic_setting_args' );
   2170 	}
   2171 
   2172 	/**
   2173 	 * {@internal Missing Summary}
   2174 	 *
   2175 	 * See the {@see 'customize_dynamic_setting_args'} filter.
   2176 	 *
   2177 	 * @since 3.9.0
   2178 	 * @deprecated 4.2.0 Deprecated in favor of the {@see 'customize_dynamic_setting_args'} filter.
   2179 	 */
   2180 	public function prepreview_added_sidebars_widgets() {
   2181 		_deprecated_function( __METHOD__, '4.2.0', 'customize_dynamic_setting_args' );
   2182 	}
   2183 
   2184 	/**
   2185 	 * {@internal Missing Summary}
   2186 	 *
   2187 	 * See the {@see 'customize_dynamic_setting_args'} filter.
   2188 	 *
   2189 	 * @since 3.9.0
   2190 	 * @deprecated 4.2.0 Deprecated in favor of the {@see 'customize_dynamic_setting_args'} filter.
   2191 	 */
   2192 	public function prepreview_added_widget_instance() {
   2193 		_deprecated_function( __METHOD__, '4.2.0', 'customize_dynamic_setting_args' );
   2194 	}
   2195 
   2196 	/**
   2197 	 * {@internal Missing Summary}
   2198 	 *
   2199 	 * See the {@see 'customize_dynamic_setting_args'} filter.
   2200 	 *
   2201 	 * @since 3.9.0
   2202 	 * @deprecated 4.2.0 Deprecated in favor of the {@see 'customize_dynamic_setting_args'} filter.
   2203 	 */
   2204 	public function remove_prepreview_filters() {
   2205 		_deprecated_function( __METHOD__, '4.2.0', 'customize_dynamic_setting_args' );
   2206 	}
   2207 }