Issues (4967)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/wp-includes/class-wp-customize-widgets.php (13 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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
	 * @access public
26
	 * @var WP_Customize_Manager
27
	 */
28
	public $manager;
29
30
	/**
31
	 * All id_bases for widgets defined in core.
32
	 *
33
	 * @since 3.9.0
34
	 * @access protected
35
	 * @var array
36
	 */
37
	protected $core_widget_id_bases = array(
38
		'archives', 'calendar', 'categories', 'links', 'meta',
39
		'nav_menu', 'pages', 'recent-comments', 'recent-posts',
40
		'rss', 'search', 'tag_cloud', 'text',
41
	);
42
43
	/**
44
	 * @since 3.9.0
45
	 * @access protected
46
	 * @var array
47
	 */
48
	protected $rendered_sidebars = array();
49
50
	/**
51
	 * @since 3.9.0
52
	 * @access protected
53
	 * @var array
54
	 */
55
	protected $rendered_widgets = array();
56
57
	/**
58
	 * @since 3.9.0
59
	 * @access protected
60
	 * @var array
61
	 */
62
	protected $old_sidebars_widgets = array();
63
64
	/**
65
	 * Mapping of widget ID base to whether it supports selective refresh.
66
	 *
67
	 * @since 4.5.0
68
	 * @access protected
69
	 * @var array
70
	 */
71
	protected $selective_refreshable_widgets;
72
73
	/**
74
	 * Mapping of setting type to setting ID pattern.
75
	 *
76
	 * @since 4.2.0
77
	 * @access protected
78
	 * @var array
79
	 */
80
	protected $setting_id_patterns = array(
81
		'widget_instance' => '/^widget_(?P<id_base>.+?)(?:\[(?P<widget_number>\d+)\])?$/',
82
		'sidebar_widgets' => '/^sidebars_widgets\[(?P<sidebar_id>.+?)\]$/',
83
	);
84
85
	/**
86
	 * Initial loader.
87
	 *
88
	 * @since 3.9.0
89
	 * @access public
90
	 *
91
	 * @param WP_Customize_Manager $manager Customize manager bootstrap instance.
92
	 */
93
	public function __construct( $manager ) {
94
		$this->manager = $manager;
95
96
		// See https://github.com/xwp/wp-customize-snapshots/blob/962586659688a5b1fd9ae93618b7ce2d4e7a421c/php/class-customize-snapshot-manager.php#L420-L449
97
		add_filter( 'customize_dynamic_setting_args',          array( $this, 'filter_customize_dynamic_setting_args' ), 10, 2 );
98
		add_action( 'widgets_init',                            array( $this, 'register_settings' ), 95 );
99
		add_action( 'customize_register',                      array( $this, 'schedule_customize_register' ), 1 );
100
101
		// Skip remaining hooks when the user can't manage widgets anyway.
102
		if ( ! current_user_can( 'edit_theme_options' ) ) {
103
			return;
104
		}
105
106
		add_action( 'wp_loaded',                               array( $this, 'override_sidebars_widgets_for_theme_switch' ) );
107
		add_action( 'customize_controls_init',                 array( $this, 'customize_controls_init' ) );
108
		add_action( 'customize_controls_enqueue_scripts',      array( $this, 'enqueue_scripts' ) );
109
		add_action( 'customize_controls_print_styles',         array( $this, 'print_styles' ) );
110
		add_action( 'customize_controls_print_scripts',        array( $this, 'print_scripts' ) );
111
		add_action( 'customize_controls_print_footer_scripts', array( $this, 'print_footer_scripts' ) );
112
		add_action( 'customize_controls_print_footer_scripts', array( $this, 'output_widget_control_templates' ) );
113
		add_action( 'customize_preview_init',                  array( $this, 'customize_preview_init' ) );
114
		add_filter( 'customize_refresh_nonces',                array( $this, 'refresh_nonces' ) );
115
116
		add_action( 'dynamic_sidebar',                         array( $this, 'tally_rendered_widgets' ) );
117
		add_filter( 'is_active_sidebar',                       array( $this, 'tally_sidebars_via_is_active_sidebar_calls' ), 10, 2 );
118
		add_filter( 'dynamic_sidebar_has_widgets',             array( $this, 'tally_sidebars_via_dynamic_sidebar_calls' ), 10, 2 );
119
120
		// Selective Refresh.
121
		add_filter( 'customize_dynamic_partial_args',          array( $this, 'customize_dynamic_partial_args' ), 10, 2 );
122
		add_action( 'customize_preview_init',                  array( $this, 'selective_refresh_init' ) );
123
	}
124
125
	/**
126
	 * List whether each registered widget can be use selective refresh.
127
	 *
128
	 * If the theme does not support the customize-selective-refresh-widgets feature,
129
	 * then this will always return an empty array.
130
	 *
131
	 * @since 4.5.0
132
	 * @access public
133
	 *
134
	 * @return array Mapping of id_base to support. If theme doesn't support
135
	 *               selective refresh, an empty array is returned.
136
	 */
137
	public function get_selective_refreshable_widgets() {
138
		global $wp_widget_factory;
139
		if ( ! current_theme_supports( 'customize-selective-refresh-widgets' ) ) {
140
			return array();
141
		}
142
		if ( ! isset( $this->selective_refreshable_widgets ) ) {
143
			$this->selective_refreshable_widgets = array();
144
			foreach ( $wp_widget_factory->widgets as $wp_widget ) {
145
				$this->selective_refreshable_widgets[ $wp_widget->id_base ] = ! empty( $wp_widget->widget_options['customize_selective_refresh'] );
146
			}
147
		}
148
		return $this->selective_refreshable_widgets;
149
	}
150
151
	/**
152
	 * Determines if a widget supports selective refresh.
153
	 *
154
	 * @since 4.5.0
155
	 * @access public
156
	 *
157
	 * @param string $id_base Widget ID Base.
158
	 * @return bool Whether the widget can be selective refreshed.
159
	 */
160
	public function is_widget_selective_refreshable( $id_base ) {
161
		$selective_refreshable_widgets = $this->get_selective_refreshable_widgets();
162
		return ! empty( $selective_refreshable_widgets[ $id_base ] );
163
	}
164
165
	/**
166
	 * Retrieves the widget setting type given a setting ID.
167
	 *
168
	 * @since 4.2.0
169
	 * @access protected
170
	 *
171
	 * @staticvar array $cache
172
	 *
173
	 * @param string $setting_id Setting ID.
174
	 * @return string|void Setting type.
175
	 */
176
	protected function get_setting_type( $setting_id ) {
177
		static $cache = array();
178
		if ( isset( $cache[ $setting_id ] ) ) {
179
			return $cache[ $setting_id ];
180
		}
181
		foreach ( $this->setting_id_patterns as $type => $pattern ) {
182
			if ( preg_match( $pattern, $setting_id ) ) {
183
				$cache[ $setting_id ] = $type;
184
				return $type;
185
			}
186
		}
187
	}
188
189
	/**
190
	 * Inspects the incoming customized data for any widget settings, and dynamically adds
191
	 * them up-front so widgets will be initialized properly.
192
	 *
193
	 * @since 4.2.0
194
	 * @access public
195
	 */
196
	public function register_settings() {
197
		$widget_setting_ids = array();
198
		$incoming_setting_ids = array_keys( $this->manager->unsanitized_post_values() );
199
		foreach ( $incoming_setting_ids as $setting_id ) {
200
			if ( ! is_null( $this->get_setting_type( $setting_id ) ) ) {
201
				$widget_setting_ids[] = $setting_id;
202
			}
203
		}
204
		if ( $this->manager->doing_ajax( 'update-widget' ) && isset( $_REQUEST['widget-id'] ) ) {
205
			$widget_setting_ids[] = $this->get_setting_id( wp_unslash( $_REQUEST['widget-id'] ) );
0 ignored issues
show
It seems like wp_unslash($_REQUEST['widget-id']) targeting wp_unslash() can also be of type array; however, WP_Customize_Widgets::get_setting_id() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
206
		}
207
208
		$settings = $this->manager->add_dynamic_settings( array_unique( $widget_setting_ids ) );
209
210
		/*
211
		 * Preview settings right away so that widgets and sidebars will get registered properly.
212
		 * But don't do this if a customize_save because this will cause WP to think there is nothing
213
		 * changed that needs to be saved.
214
		 */
215
		if ( ! $this->manager->doing_ajax( 'customize_save' ) ) {
216
			foreach ( $settings as $setting ) {
217
				$setting->preview();
218
			}
219
		}
220
	}
221
222
	/**
223
	 * Determines the arguments for a dynamically-created setting.
224
	 *
225
	 * @since 4.2.0
226
	 * @access public
227
	 *
228
	 * @param false|array $args       The arguments to the WP_Customize_Setting constructor.
229
	 * @param string      $setting_id ID for dynamic setting, usually coming from `$_POST['customized']`.
230
	 * @return false|array Setting arguments, false otherwise.
231
	 */
232
	public function filter_customize_dynamic_setting_args( $args, $setting_id ) {
233
		if ( $this->get_setting_type( $setting_id ) ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->get_setting_type($setting_id) of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
234
			$args = $this->get_setting_args( $setting_id );
235
		}
236
		return $args;
237
	}
238
239
	/**
240
	 * Retrieves an unslashed post value or return a default.
241
	 *
242
	 * @since 3.9.0
243
	 * @access protected
244
	 *
245
	 * @param string $name    Post value.
246
	 * @param mixed  $default Default post value.
247
	 * @return mixed Unslashed post value or default value.
248
	 */
249
	protected function get_post_value( $name, $default = null ) {
250
		if ( ! isset( $_POST[ $name ] ) ) {
251
			return $default;
252
		}
253
254
		return wp_unslash( $_POST[ $name ] );
255
	}
256
257
	/**
258
	 * Override sidebars_widgets for theme switch.
259
	 *
260
	 * When switching a theme via the Customizer, supply any previously-configured
261
	 * sidebars_widgets from the target theme as the initial sidebars_widgets
262
	 * setting. Also store the old theme's existing settings so that they can
263
	 * be passed along for storing in the sidebars_widgets theme_mod when the
264
	 * theme gets switched.
265
	 *
266
	 * @since 3.9.0
267
	 * @access public
268
	 *
269
	 * @global array $sidebars_widgets
270
	 * @global array $_wp_sidebars_widgets
271
	 */
272
	public function override_sidebars_widgets_for_theme_switch() {
273
		global $sidebars_widgets;
274
275
		if ( $this->manager->doing_ajax() || $this->manager->is_theme_active() ) {
276
			return;
277
		}
278
279
		$this->old_sidebars_widgets = wp_get_sidebars_widgets();
280
		add_filter( 'customize_value_old_sidebars_widgets_data', array( $this, 'filter_customize_value_old_sidebars_widgets_data' ) );
281
		$this->manager->set_post_value( 'old_sidebars_widgets_data', $this->old_sidebars_widgets ); // Override any value cached in changeset.
282
283
		// retrieve_widgets() looks at the global $sidebars_widgets
284
		$sidebars_widgets = $this->old_sidebars_widgets;
285
		$sidebars_widgets = retrieve_widgets( 'customize' );
286
		add_filter( 'option_sidebars_widgets', array( $this, 'filter_option_sidebars_widgets_for_theme_switch' ), 1 );
287
		// reset global cache var used by wp_get_sidebars_widgets()
288
		unset( $GLOBALS['_wp_sidebars_widgets'] );
289
	}
290
291
	/**
292
	 * Filters old_sidebars_widgets_data Customizer setting.
293
	 *
294
	 * When switching themes, filter the Customizer setting old_sidebars_widgets_data
295
	 * to supply initial $sidebars_widgets before they were overridden by retrieve_widgets().
296
	 * The value for old_sidebars_widgets_data gets set in the old theme's sidebars_widgets
297
	 * theme_mod.
298
	 *
299
	 * @since 3.9.0
300
	 * @access public
301
	 *
302
	 * @see WP_Customize_Widgets::handle_theme_switch()
303
	 *
304
	 * @param array $old_sidebars_widgets
305
	 * @return array
306
	 */
307
	public function filter_customize_value_old_sidebars_widgets_data( $old_sidebars_widgets ) {
0 ignored issues
show
The parameter $old_sidebars_widgets is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
308
		return $this->old_sidebars_widgets;
309
	}
310
311
	/**
312
	 * Filters sidebars_widgets option for theme switch.
313
	 *
314
	 * When switching themes, the retrieve_widgets() function is run when the Customizer initializes,
315
	 * and then the new sidebars_widgets here get supplied as the default value for the sidebars_widgets
316
	 * option.
317
	 *
318
	 * @since 3.9.0
319
	 * @access public
320
	 *
321
	 * @see WP_Customize_Widgets::handle_theme_switch()
322
	 * @global array $sidebars_widgets
323
	 *
324
	 * @param array $sidebars_widgets
325
	 * @return array
0 ignored issues
show
Consider making the return type a bit more specific; maybe use array<string,integer>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
326
	 */
327
	public function filter_option_sidebars_widgets_for_theme_switch( $sidebars_widgets ) {
0 ignored issues
show
The parameter $sidebars_widgets is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
328
		$sidebars_widgets = $GLOBALS['sidebars_widgets'];
329
		$sidebars_widgets['array_version'] = 3;
330
		return $sidebars_widgets;
331
	}
332
333
	/**
334
	 * Ensures all widgets get loaded into the Customizer.
335
	 *
336
	 * Note: these actions are also fired in wp_ajax_update_widget().
337
	 *
338
	 * @since 3.9.0
339
	 * @access public
340
	 */
341
	public function customize_controls_init() {
342
		/** This action is documented in wp-admin/includes/ajax-actions.php */
343
		do_action( 'load-widgets.php' );
344
345
		/** This action is documented in wp-admin/includes/ajax-actions.php */
346
		do_action( 'widgets.php' );
347
348
		/** This action is documented in wp-admin/widgets.php */
349
		do_action( 'sidebar_admin_setup' );
350
	}
351
352
	/**
353
	 * Ensures widgets are available for all types of previews.
354
	 *
355
	 * When in preview, hook to {@see 'customize_register'} for settings after WordPress is loaded
356
	 * so that all filters have been initialized (e.g. Widget Visibility).
357
	 *
358
	 * @since 3.9.0
359
	 * @access public
360
	 */
361
	public function schedule_customize_register() {
362
		if ( is_admin() ) {
363
			$this->customize_register();
364
		} else {
365
			add_action( 'wp', array( $this, 'customize_register' ) );
366
		}
367
	}
368
369
	/**
370
	 * Registers Customizer settings and controls for all sidebars and widgets.
371
	 *
372
	 * @since 3.9.0
373
	 * @access public
374
	 *
375
	 * @global array $wp_registered_widgets
376
	 * @global array $wp_registered_widget_controls
377
	 * @global array $wp_registered_sidebars
378
	 */
379
	public function customize_register() {
380
		global $wp_registered_widgets, $wp_registered_widget_controls, $wp_registered_sidebars;
381
382
		add_filter( 'sidebars_widgets', array( $this, 'preview_sidebars_widgets' ), 1 );
383
384
		$sidebars_widgets = array_merge(
385
			array( 'wp_inactive_widgets' => array() ),
386
			array_fill_keys( array_keys( $wp_registered_sidebars ), array() ),
387
			wp_get_sidebars_widgets()
388
		);
389
390
		$new_setting_ids = array();
391
392
		/*
393
		 * Register a setting for all widgets, including those which are active,
394
		 * inactive, and orphaned since a widget may get suppressed from a sidebar
395
		 * via a plugin (like Widget Visibility).
396
		 */
397
		foreach ( array_keys( $wp_registered_widgets ) as $widget_id ) {
398
			$setting_id   = $this->get_setting_id( $widget_id );
399
			$setting_args = $this->get_setting_args( $setting_id );
400
			if ( ! $this->manager->get_setting( $setting_id ) ) {
401
				$this->manager->add_setting( $setting_id, $setting_args );
402
			}
403
			$new_setting_ids[] = $setting_id;
404
		}
405
406
		/*
407
		 * Add a setting which will be supplied for the theme's sidebars_widgets
408
		 * theme_mod when the theme is switched.
409
		 */
410
		if ( ! $this->manager->is_theme_active() ) {
411
			$setting_id = 'old_sidebars_widgets_data';
412
			$setting_args = $this->get_setting_args( $setting_id, array(
413
				'type' => 'global_variable',
414
				'dirty' => true,
415
			) );
416
			$this->manager->add_setting( $setting_id, $setting_args );
417
		}
418
419
		$this->manager->add_panel( 'widgets', array(
420
			'type'            => 'widgets',
421
			'title'           => __( 'Widgets' ),
422
			'description'     => __( 'Widgets are independent sections of content that can be placed into widgetized areas provided by your theme (commonly called sidebars).' ),
423
			'priority'        => 110,
424
			'active_callback' => array( $this, 'is_panel_active' ),
425
			'auto_expand_sole_section' => true,
426
		) );
427
428
		foreach ( $sidebars_widgets as $sidebar_id => $sidebar_widget_ids ) {
429
			if ( empty( $sidebar_widget_ids ) ) {
430
				$sidebar_widget_ids = array();
431
			}
432
433
			$is_registered_sidebar = is_registered_sidebar( $sidebar_id );
434
			$is_inactive_widgets   = ( 'wp_inactive_widgets' === $sidebar_id );
435
			$is_active_sidebar     = ( $is_registered_sidebar && ! $is_inactive_widgets );
436
437
			// Add setting for managing the sidebar's widgets.
438
			if ( $is_registered_sidebar || $is_inactive_widgets ) {
439
				$setting_id   = sprintf( 'sidebars_widgets[%s]', $sidebar_id );
440
				$setting_args = $this->get_setting_args( $setting_id );
441
				if ( ! $this->manager->get_setting( $setting_id ) ) {
442
					if ( ! $this->manager->is_theme_active() ) {
443
						$setting_args['dirty'] = true;
444
					}
445
					$this->manager->add_setting( $setting_id, $setting_args );
446
				}
447
				$new_setting_ids[] = $setting_id;
448
449
				// Add section to contain controls.
450
				$section_id = sprintf( 'sidebar-widgets-%s', $sidebar_id );
451
				if ( $is_active_sidebar ) {
452
453
					$section_args = array(
454
						'title' => $wp_registered_sidebars[ $sidebar_id ]['name'],
455
						'description' => $wp_registered_sidebars[ $sidebar_id ]['description'],
456
						'priority' => array_search( $sidebar_id, array_keys( $wp_registered_sidebars ) ),
457
						'panel' => 'widgets',
458
						'sidebar_id' => $sidebar_id,
459
					);
460
461
					/**
462
					 * Filters Customizer widget section arguments for a given sidebar.
463
					 *
464
					 * @since 3.9.0
465
					 *
466
					 * @param array      $section_args Array of Customizer widget section arguments.
467
					 * @param string     $section_id   Customizer section ID.
468
					 * @param int|string $sidebar_id   Sidebar ID.
469
					 */
470
					$section_args = apply_filters( 'customizer_widgets_section_args', $section_args, $section_id, $sidebar_id );
471
472
					$section = new WP_Customize_Sidebar_Section( $this->manager, $section_id, $section_args );
473
					$this->manager->add_section( $section );
474
475
					$control = new WP_Widget_Area_Customize_Control( $this->manager, $setting_id, array(
476
						'section'    => $section_id,
477
						'sidebar_id' => $sidebar_id,
478
						'priority'   => count( $sidebar_widget_ids ), // place 'Add Widget' and 'Reorder' buttons at end.
479
					) );
480
					$new_setting_ids[] = $setting_id;
481
482
					$this->manager->add_control( $control );
483
				}
484
			}
485
486
			// Add a control for each active widget (located in a sidebar).
487
			foreach ( $sidebar_widget_ids as $i => $widget_id ) {
488
489
				// Skip widgets that may have gone away due to a plugin being deactivated.
490
				if ( ! $is_active_sidebar || ! isset( $wp_registered_widgets[$widget_id] ) ) {
491
					continue;
492
				}
493
494
				$registered_widget = $wp_registered_widgets[$widget_id];
495
				$setting_id        = $this->get_setting_id( $widget_id );
496
				$id_base           = $wp_registered_widget_controls[$widget_id]['id_base'];
497
498
				$control = new WP_Widget_Form_Customize_Control( $this->manager, $setting_id, array(
499
					'label'          => $registered_widget['name'],
500
					'section'        => $section_id,
0 ignored issues
show
The variable $section_id does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
501
					'sidebar_id'     => $sidebar_id,
502
					'widget_id'      => $widget_id,
503
					'widget_id_base' => $id_base,
504
					'priority'       => $i,
505
					'width'          => $wp_registered_widget_controls[$widget_id]['width'],
506
					'height'         => $wp_registered_widget_controls[$widget_id]['height'],
507
					'is_wide'        => $this->is_wide_widget( $widget_id ),
508
				) );
509
				$this->manager->add_control( $control );
510
			}
511
		}
512
513
		if ( ! $this->manager->doing_ajax( 'customize_save' ) ) {
514
			foreach ( $new_setting_ids as $new_setting_id ) {
515
				$this->manager->get_setting( $new_setting_id )->preview();
516
			}
517
		}
518
	}
519
520
	/**
521
	 * Determines whether the widgets panel is active, based on whether there are sidebars registered.
522
	 *
523
	 * @since 4.4.0
524
	 * @access public
525
	 *
526
	 * @see WP_Customize_Panel::$active_callback
527
	 *
528
	 * @global array $wp_registered_sidebars
529
	 * @return bool Active.
530
	 */
531
	public function is_panel_active() {
532
		global $wp_registered_sidebars;
533
		return ! empty( $wp_registered_sidebars );
534
	}
535
536
	/**
537
	 * Converts a widget_id into its corresponding Customizer setting ID (option name).
538
	 *
539
	 * @since 3.9.0
540
	 * @access public
541
	 *
542
	 * @param string $widget_id Widget ID.
543
	 * @return string Maybe-parsed widget ID.
544
	 */
545
	public function get_setting_id( $widget_id ) {
546
		$parsed_widget_id = $this->parse_widget_id( $widget_id );
547
		$setting_id       = sprintf( 'widget_%s', $parsed_widget_id['id_base'] );
548
549
		if ( ! is_null( $parsed_widget_id['number'] ) ) {
550
			$setting_id .= sprintf( '[%d]', $parsed_widget_id['number'] );
551
		}
552
		return $setting_id;
553
	}
554
555
	/**
556
	 * Determines whether the widget is considered "wide".
557
	 *
558
	 * Core widgets which may have controls wider than 250, but can still be shown
559
	 * in the narrow Customizer panel. The RSS and Text widgets in Core, for example,
560
	 * have widths of 400 and yet they still render fine in the Customizer panel.
561
	 *
562
	 * This method will return all Core widgets as being not wide, but this can be
563
	 * overridden with the {@see 'is_wide_widget_in_customizer'} filter.
564
	 *
565
	 * @since 3.9.0
566
	 * @access public
567
	 *
568
	 * @global $wp_registered_widget_controls
569
	 *
570
	 * @param string $widget_id Widget ID.
571
	 * @return bool Whether or not the widget is a "wide" widget.
572
	 */
573
	public function is_wide_widget( $widget_id ) {
574
		global $wp_registered_widget_controls;
575
576
		$parsed_widget_id = $this->parse_widget_id( $widget_id );
577
		$width            = $wp_registered_widget_controls[$widget_id]['width'];
578
		$is_core          = in_array( $parsed_widget_id['id_base'], $this->core_widget_id_bases );
579
		$is_wide          = ( $width > 250 && ! $is_core );
580
581
		/**
582
		 * Filters whether the given widget is considered "wide".
583
		 *
584
		 * @since 3.9.0
585
		 *
586
		 * @param bool   $is_wide   Whether the widget is wide, Default false.
587
		 * @param string $widget_id Widget ID.
588
		 */
589
		return apply_filters( 'is_wide_widget_in_customizer', $is_wide, $widget_id );
590
	}
591
592
	/**
593
	 * Converts a widget ID into its id_base and number components.
594
	 *
595
	 * @since 3.9.0
596
	 * @access public
597
	 *
598
	 * @param string $widget_id Widget ID.
599
	 * @return array Array containing a widget's id_base and number components.
600
	 */
601
	public function parse_widget_id( $widget_id ) {
602
		$parsed = array(
603
			'number' => null,
604
			'id_base' => null,
605
		);
606
607
		if ( preg_match( '/^(.+)-(\d+)$/', $widget_id, $matches ) ) {
608
			$parsed['id_base'] = $matches[1];
609
			$parsed['number']  = intval( $matches[2] );
610
		} else {
611
			// likely an old single widget
612
			$parsed['id_base'] = $widget_id;
613
		}
614
		return $parsed;
615
	}
616
617
	/**
618
	 * Converts a widget setting ID (option path) to its id_base and number components.
619
	 *
620
	 * @since 3.9.0
621
	 * @access public
622
	 *
623
	 * @param string $setting_id Widget setting ID.
624
	 * @return WP_Error|array Array containing a widget's id_base and number components,
625
	 *                        or a WP_Error object.
626
	 */
627
	public function parse_widget_setting_id( $setting_id ) {
628
		if ( ! preg_match( '/^(widget_(.+?))(?:\[(\d+)\])?$/', $setting_id, $matches ) ) {
629
			return new WP_Error( 'widget_setting_invalid_id' );
630
		}
631
632
		$id_base = $matches[2];
633
		$number  = isset( $matches[3] ) ? intval( $matches[3] ) : null;
634
635
		return compact( 'id_base', 'number' );
636
	}
637
638
	/**
639
	 * Calls admin_print_styles-widgets.php and admin_print_styles hooks to
640
	 * allow custom styles from plugins.
641
	 *
642
	 * @since 3.9.0
643
	 * @access public
644
	 */
645
	public function print_styles() {
646
		/** This action is documented in wp-admin/admin-header.php */
647
		do_action( 'admin_print_styles-widgets.php' );
648
649
		/** This action is documented in wp-admin/admin-header.php */
650
		do_action( 'admin_print_styles' );
651
	}
652
653
	/**
654
	 * Calls admin_print_scripts-widgets.php and admin_print_scripts hooks to
655
	 * allow custom scripts from plugins.
656
	 *
657
	 * @since 3.9.0
658
	 * @access public
659
	 */
660
	public function print_scripts() {
661
		/** This action is documented in wp-admin/admin-header.php */
662
		do_action( 'admin_print_scripts-widgets.php' );
663
664
		/** This action is documented in wp-admin/admin-header.php */
665
		do_action( 'admin_print_scripts' );
666
	}
667
668
	/**
669
	 * Enqueues scripts and styles for Customizer panel and export data to JavaScript.
670
	 *
671
	 * @since 3.9.0
672
	 * @access public
673
	 *
674
	 * @global WP_Scripts $wp_scripts
675
	 * @global array $wp_registered_sidebars
676
	 * @global array $wp_registered_widgets
677
	 */
678
	public function enqueue_scripts() {
679
		global $wp_scripts, $wp_registered_sidebars, $wp_registered_widgets;
680
681
		wp_enqueue_style( 'customize-widgets' );
682
		wp_enqueue_script( 'customize-widgets' );
683
684
		/** This action is documented in wp-admin/admin-header.php */
685
		do_action( 'admin_enqueue_scripts', 'widgets.php' );
686
687
		/*
688
		 * Export available widgets with control_tpl removed from model
689
		 * since plugins need templates to be in the DOM.
690
		 */
691
		$available_widgets = array();
692
693
		foreach ( $this->get_available_widgets() as $available_widget ) {
694
			unset( $available_widget['control_tpl'] );
695
			$available_widgets[] = $available_widget;
696
		}
697
698
		$widget_reorder_nav_tpl = sprintf(
699
			'<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>',
700
			__( 'Move to another area&hellip;' ),
701
			__( 'Move down' ),
702
			__( 'Move up' )
703
		);
704
705
		$move_widget_area_tpl = str_replace(
706
			array( '{description}', '{btn}' ),
707
			array(
708
				__( 'Select an area to move this widget into:' ),
709
				_x( 'Move', 'Move widget' ),
710
			),
711
			'<div class="move-widget-area">
712
				<p class="description">{description}</p>
713
				<ul class="widget-area-select">
714
					<% _.each( sidebars, function ( sidebar ){ %>
715
						<li class="" data-id="<%- sidebar.id %>" title="<%- sidebar.description %>" tabindex="0"><%- sidebar.name %></li>
716
					<% }); %>
717
				</ul>
718
				<div class="move-widget-actions">
719
					<button class="move-widget-btn button" type="button">{btn}</button>
720
				</div>
721
			</div>'
722
		);
723
724
		/*
725
		 * Gather all strings in PHP that may be needed by JS on the client.
726
		 * Once JS i18n is implemented (in #20491), this can be removed.
727
		 */
728
		$some_non_rendered_areas_messages = array();
729
		$some_non_rendered_areas_messages[1] = html_entity_decode(
730
			/* translators: placeholder is the number of other widget areas registered but not rendered */
731
			__( 'Your theme has 1 other widget area, but this particular page doesn&#8217;t display it.' ),
732
			ENT_QUOTES,
733
			get_bloginfo( 'charset' )
734
		);
735
		$registered_sidebar_count = count( $wp_registered_sidebars );
736
		for ( $non_rendered_count = 2; $non_rendered_count < $registered_sidebar_count; $non_rendered_count++ ) {
737
			$some_non_rendered_areas_messages[ $non_rendered_count ] = html_entity_decode( sprintf(
738
				/* translators: placeholder is the number of other widget areas registered but not rendered */
739
				_n(
740
					'Your theme has %s other widget area, but this particular page doesn&#8217;t display it.',
741
					'Your theme has %s other widget areas, but this particular page doesn&#8217;t display them.',
742
					$non_rendered_count
743
				),
744
				number_format_i18n( $non_rendered_count )
745
			), ENT_QUOTES, get_bloginfo( 'charset' ) );
746
		}
747
748
		if ( 1 === $registered_sidebar_count ) {
749
			$no_areas_shown_message = html_entity_decode( sprintf(
750
				/* translators: placeholder is the total number of widget areas registered */
751
				__( 'Your theme has 1 widget area, but this particular page doesn&#8217;t display it.' )
752
			), ENT_QUOTES, get_bloginfo( 'charset' ) );
753
		} else {
754
			$no_areas_shown_message = html_entity_decode( sprintf(
755
				/* translators: placeholder is the total number of widget areas registered */
756
				_n(
757
					'Your theme has %s widget area, but this particular page doesn&#8217;t display it.',
758
					'Your theme has %s widget areas, but this particular page doesn&#8217;t display them.',
759
					$registered_sidebar_count
760
				),
761
				number_format_i18n( $registered_sidebar_count )
762
			), ENT_QUOTES, get_bloginfo( 'charset' ) );
763
		}
764
765
		$settings = array(
766
			'registeredSidebars'   => array_values( $wp_registered_sidebars ),
767
			'registeredWidgets'    => $wp_registered_widgets,
768
			'availableWidgets'     => $available_widgets, // @todo Merge this with registered_widgets
769
			'l10n' => array(
770
				'saveBtnLabel'     => __( 'Apply' ),
771
				'saveBtnTooltip'   => __( 'Save and preview changes before publishing them.' ),
772
				'removeBtnLabel'   => __( 'Remove' ),
773
				'removeBtnTooltip' => __( 'Trash widget by moving it to the inactive widgets sidebar.' ),
774
				'error'            => __( 'An error has occurred. Please reload the page and try again.' ),
775
				'widgetMovedUp'    => __( 'Widget moved up' ),
776
				'widgetMovedDown'  => __( 'Widget moved down' ),
777
				'navigatePreview'  => __( 'You can navigate to other pages on your site while using the Customizer to view and edit the widgets displayed on those pages.' ),
778
				'someAreasShown'   => $some_non_rendered_areas_messages,
779
				'noAreasShown'     => $no_areas_shown_message,
780
				'reorderModeOn'    => __( 'Reorder mode enabled' ),
781
				'reorderModeOff'   => __( 'Reorder mode closed' ),
782
				'reorderLabelOn'   => esc_attr__( 'Reorder widgets' ),
783
				/* translators: placeholder is the count for the number of widgets found */
784
				'widgetsFound'     => __( 'Number of widgets found: %d' ),
785
				'noWidgetsFound'   => __( 'No widgets found.' ),
786
			),
787
			'tpl' => array(
788
				'widgetReorderNav' => $widget_reorder_nav_tpl,
789
				'moveWidgetArea'   => $move_widget_area_tpl,
790
			),
791
			'selectiveRefreshableWidgets' => $this->get_selective_refreshable_widgets(),
792
		);
793
794
		foreach ( $settings['registeredWidgets'] as &$registered_widget ) {
795
			unset( $registered_widget['callback'] ); // may not be JSON-serializeable
796
		}
797
798
		$wp_scripts->add_data(
799
			'customize-widgets',
800
			'data',
801
			sprintf( 'var _wpCustomizeWidgetsSettings = %s;', wp_json_encode( $settings ) )
802
		);
803
	}
804
805
	/**
806
	 * Renders the widget form control templates into the DOM.
807
	 *
808
	 * @since 3.9.0
809
	 * @access public
810
	 */
811
	public function output_widget_control_templates() {
812
		?>
813
		<div id="widgets-left"><!-- compatibility with JS which looks for widget templates here -->
814
		<div id="available-widgets">
815
			<div class="customize-section-title">
816
				<button class="customize-section-back" tabindex="-1">
817
					<span class="screen-reader-text"><?php _e( 'Back' ); ?></span>
818
				</button>
819
				<h3>
820
					<span class="customize-action"><?php
821
						/* translators: &#9656; is the unicode right-pointing triangle, and %s is the section title in the Customizer */
822
						echo sprintf( __( 'Customizing &#9656; %s' ), esc_html( $this->manager->get_panel( 'widgets' )->title ) );
823
					?></span>
824
					<?php _e( 'Add a Widget' ); ?>
825
				</h3>
826
			</div>
827
			<div id="available-widgets-filter">
828
				<label class="screen-reader-text" for="widgets-search"><?php _e( 'Search Widgets' ); ?></label>
829
				<input type="text" id="widgets-search" placeholder="<?php esc_attr_e( 'Search widgets&hellip;' ) ?>" aria-describedby="widgets-search-desc" />
830
				<div class="search-icon" aria-hidden="true"></div>
831
				<button type="button" class="clear-results"><span class="screen-reader-text"><?php _e( 'Clear Results' ); ?></span></button>
832
				<p class="screen-reader-text" id="widgets-search-desc"><?php _e( 'The search results will be updated as you type.' ); ?></p>
833
			</div>
834
			<div id="available-widgets-list">
835
			<?php foreach ( $this->get_available_widgets() as $available_widget ): ?>
836
				<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">
837
					<?php echo $available_widget['control_tpl']; ?>
838
				</div>
839
			<?php endforeach; ?>
840
			<p class="no-widgets-found-message"><?php _e( 'No widgets found.' ); ?></p>
841
			</div><!-- #available-widgets-list -->
842
		</div><!-- #available-widgets -->
843
		</div><!-- #widgets-left -->
844
		<?php
845
	}
846
847
	/**
848
	 * Calls admin_print_footer_scripts and admin_print_scripts hooks to
849
	 * allow custom scripts from plugins.
850
	 *
851
	 * @since 3.9.0
852
	 * @access public
853
	 */
854
	public function print_footer_scripts() {
855
		/** This action is documented in wp-admin/admin-footer.php */
856
		do_action( 'admin_print_footer_scripts-widgets.php' );
857
858
		/** This action is documented in wp-admin/admin-footer.php */
859
		do_action( 'admin_print_footer_scripts' );
860
861
		/** This action is documented in wp-admin/admin-footer.php */
862
		do_action( 'admin_footer-widgets.php' );
863
	}
864
865
	/**
866
	 * Retrieves common arguments to supply when constructing a Customizer setting.
867
	 *
868
	 * @since 3.9.0
869
	 * @access public
870
	 *
871
	 * @param string $id        Widget setting ID.
872
	 * @param array  $overrides Array of setting overrides.
873
	 * @return array Possibly modified setting arguments.
874
	 */
875
	public function get_setting_args( $id, $overrides = array() ) {
876
		$args = array(
877
			'type'       => 'option',
878
			'capability' => 'edit_theme_options',
879
			'default'    => array(),
880
		);
881
882
		if ( preg_match( $this->setting_id_patterns['sidebar_widgets'], $id, $matches ) ) {
883
			$args['sanitize_callback'] = array( $this, 'sanitize_sidebar_widgets' );
884
			$args['sanitize_js_callback'] = array( $this, 'sanitize_sidebar_widgets_js_instance' );
885
			$args['transport'] = current_theme_supports( 'customize-selective-refresh-widgets' ) ? 'postMessage' : 'refresh';
886
		} elseif ( preg_match( $this->setting_id_patterns['widget_instance'], $id, $matches ) ) {
887
			$args['sanitize_callback'] = array( $this, 'sanitize_widget_instance' );
888
			$args['sanitize_js_callback'] = array( $this, 'sanitize_widget_js_instance' );
889
			$args['transport'] = $this->is_widget_selective_refreshable( $matches['id_base'] ) ? 'postMessage' : 'refresh';
890
		}
891
892
		$args = array_merge( $args, $overrides );
893
894
		/**
895
		 * Filters the common arguments supplied when constructing a Customizer setting.
896
		 *
897
		 * @since 3.9.0
898
		 *
899
		 * @see WP_Customize_Setting
900
		 *
901
		 * @param array  $args Array of Customizer setting arguments.
902
		 * @param string $id   Widget setting ID.
903
		 */
904
		return apply_filters( 'widget_customizer_setting_args', $args, $id );
905
	}
906
907
	/**
908
	 * Ensures sidebar widget arrays only ever contain widget IDS.
909
	 *
910
	 * Used as the 'sanitize_callback' for each $sidebars_widgets setting.
911
	 *
912
	 * @since 3.9.0
913
	 * @access public
914
	 *
915
	 * @param array $widget_ids Array of widget IDs.
916
	 * @return array Array of sanitized widget IDs.
917
	 */
918
	public function sanitize_sidebar_widgets( $widget_ids ) {
919
		$widget_ids = array_map( 'strval', (array) $widget_ids );
920
		$sanitized_widget_ids = array();
921
		foreach ( $widget_ids as $widget_id ) {
922
			$sanitized_widget_ids[] = preg_replace( '/[^a-z0-9_\-]/', '', $widget_id );
923
		}
924
		return $sanitized_widget_ids;
925
	}
926
927
	/**
928
	 * Builds up an index of all available widgets for use in Backbone models.
929
	 *
930
	 * @since 3.9.0
931
	 * @access public
932
	 *
933
	 * @global array $wp_registered_widgets
934
	 * @global array $wp_registered_widget_controls
935
	 * @staticvar array $available_widgets
936
	 *
937
	 * @see wp_list_widgets()
938
	 *
939
	 * @return array List of available widgets.
940
	 */
941
	public function get_available_widgets() {
942
		static $available_widgets = array();
943
		if ( ! empty( $available_widgets ) ) {
944
			return $available_widgets;
945
		}
946
947
		global $wp_registered_widgets, $wp_registered_widget_controls;
948
		require_once ABSPATH . '/wp-admin/includes/widgets.php'; // for next_widget_id_number()
949
950
		$sort = $wp_registered_widgets;
951
		usort( $sort, array( $this, '_sort_name_callback' ) );
952
		$done = array();
953
954
		foreach ( $sort as $widget ) {
955
			if ( in_array( $widget['callback'], $done, true ) ) { // We already showed this multi-widget
956
				continue;
957
			}
958
959
			$sidebar = is_active_widget( $widget['callback'], $widget['id'], false, false );
960
			$done[]  = $widget['callback'];
961
962
			if ( ! isset( $widget['params'][0] ) ) {
963
				$widget['params'][0] = array();
964
			}
965
966
			$available_widget = $widget;
967
			unset( $available_widget['callback'] ); // not serializable to JSON
968
969
			$args = array(
970
				'widget_id'   => $widget['id'],
971
				'widget_name' => $widget['name'],
972
				'_display'    => 'template',
973
			);
974
975
			$is_disabled     = false;
976
			$is_multi_widget = ( isset( $wp_registered_widget_controls[$widget['id']]['id_base'] ) && isset( $widget['params'][0]['number'] ) );
977
			if ( $is_multi_widget ) {
978
				$id_base            = $wp_registered_widget_controls[$widget['id']]['id_base'];
979
				$args['_temp_id']   = "$id_base-__i__";
980
				$args['_multi_num'] = next_widget_id_number( $id_base );
981
				$args['_add']       = 'multi';
982
			} else {
983
				$args['_add'] = 'single';
984
985
				if ( $sidebar && 'wp_inactive_widgets' !== $sidebar ) {
986
					$is_disabled = true;
987
				}
988
				$id_base = $widget['id'];
989
			}
990
991
			$list_widget_controls_args = wp_list_widget_controls_dynamic_sidebar( array( 0 => $args, 1 => $widget['params'][0] ) );
992
			$control_tpl = $this->get_widget_control( $list_widget_controls_args );
993
994
			// The properties here are mapped to the Backbone Widget model.
995
			$available_widget = array_merge( $available_widget, array(
996
				'temp_id'      => isset( $args['_temp_id'] ) ? $args['_temp_id'] : null,
997
				'is_multi'     => $is_multi_widget,
998
				'control_tpl'  => $control_tpl,
999
				'multi_number' => ( $args['_add'] === 'multi' ) ? $args['_multi_num'] : false,
1000
				'is_disabled'  => $is_disabled,
1001
				'id_base'      => $id_base,
1002
				'transport'    => $this->is_widget_selective_refreshable( $id_base ) ? 'postMessage' : 'refresh',
1003
				'width'        => $wp_registered_widget_controls[$widget['id']]['width'],
1004
				'height'       => $wp_registered_widget_controls[$widget['id']]['height'],
1005
				'is_wide'      => $this->is_wide_widget( $widget['id'] ),
1006
			) );
1007
1008
			$available_widgets[] = $available_widget;
1009
		}
1010
1011
		return $available_widgets;
1012
	}
1013
1014
	/**
1015
	 * Naturally orders available widgets by name.
1016
	 *
1017
	 * @since 3.9.0
1018
	 * @access protected
1019
	 *
1020
	 * @param array $widget_a The first widget to compare.
1021
	 * @param array $widget_b The second widget to compare.
1022
	 * @return int Reorder position for the current widget comparison.
1023
	 */
1024
	protected function _sort_name_callback( $widget_a, $widget_b ) {
1025
		return strnatcasecmp( $widget_a['name'], $widget_b['name'] );
1026
	}
1027
1028
	/**
1029
	 * Retrieves the widget control markup.
1030
	 *
1031
	 * @since 3.9.0
1032
	 * @access public
1033
	 *
1034
	 * @param array $args Widget control arguments.
1035
	 * @return string Widget control form HTML markup.
1036
	 */
1037
	public function get_widget_control( $args ) {
1038
		$args[0]['before_form'] = '<div class="form">';
1039
		$args[0]['after_form'] = '</div><!-- .form -->';
1040
		$args[0]['before_widget_content'] = '<div class="widget-content">';
1041
		$args[0]['after_widget_content'] = '</div><!-- .widget-content -->';
1042
		ob_start();
1043
		call_user_func_array( 'wp_widget_control', $args );
1044
		$control_tpl = ob_get_clean();
1045
		return $control_tpl;
1046
	}
1047
1048
	/**
1049
	 * Retrieves the widget control markup parts.
1050
	 *
1051
	 * @since 4.4.0
1052
	 * @access public
1053
	 *
1054
	 * @param array $args Widget control arguments.
1055
	 * @return array {
1056
	 *     @type string $control Markup for widget control wrapping form.
1057
	 *     @type string $content The contents of the widget form itself.
1058
	 * }
1059
	 */
1060
	public function get_widget_control_parts( $args ) {
1061
		$args[0]['before_widget_content'] = '<div class="widget-content">';
1062
		$args[0]['after_widget_content'] = '</div><!-- .widget-content -->';
1063
		$control_markup = $this->get_widget_control( $args );
1064
1065
		$content_start_pos = strpos( $control_markup, $args[0]['before_widget_content'] );
1066
		$content_end_pos = strrpos( $control_markup, $args[0]['after_widget_content'] );
1067
1068
		$control = substr( $control_markup, 0, $content_start_pos + strlen( $args[0]['before_widget_content'] ) );
1069
		$control .= substr( $control_markup, $content_end_pos );
1070
		$content = trim( substr(
1071
			$control_markup,
1072
			$content_start_pos + strlen( $args[0]['before_widget_content'] ),
1073
			$content_end_pos - $content_start_pos - strlen( $args[0]['before_widget_content'] )
1074
		) );
1075
1076
		return compact( 'control', 'content' );
1077
	}
1078
1079
	/**
1080
	 * Adds hooks for the Customizer preview.
1081
	 *
1082
	 * @since 3.9.0
1083
	 * @access public
1084
	 */
1085
	public function customize_preview_init() {
1086
		add_action( 'wp_enqueue_scripts', array( $this, 'customize_preview_enqueue' ) );
1087
		add_action( 'wp_print_styles',    array( $this, 'print_preview_css' ), 1 );
1088
		add_action( 'wp_footer',          array( $this, 'export_preview_data' ), 20 );
1089
	}
1090
1091
	/**
1092
	 * Refreshes the nonce for widget updates.
1093
	 *
1094
	 * @since 4.2.0
1095
	 * @access public
1096
	 *
1097
	 * @param  array $nonces Array of nonces.
1098
	 * @return array $nonces Array of nonces.
0 ignored issues
show
Consider making the return type a bit more specific; maybe use array<string,string>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
1099
	 */
1100
	public function refresh_nonces( $nonces ) {
1101
		$nonces['update-widget'] = wp_create_nonce( 'update-widget' );
1102
		return $nonces;
1103
	}
1104
1105
	/**
1106
	 * When previewing, ensures the proper previewing widgets are used.
1107
	 *
1108
	 * Because wp_get_sidebars_widgets() gets called early at {@see 'init' } (via
1109
	 * wp_convert_widget_settings()) and can set global variable `$_wp_sidebars_widgets`
1110
	 * to the value of `get_option( 'sidebars_widgets' )` before the Customizer preview
1111
	 * filter is added, it has to be reset after the filter has been added.
1112
	 *
1113
	 * @since 3.9.0
1114
	 * @access public
1115
	 *
1116
	 * @param array $sidebars_widgets List of widgets for the current sidebar.
1117
	 * @return array
1118
	 */
1119
	public function preview_sidebars_widgets( $sidebars_widgets ) {
0 ignored issues
show
The parameter $sidebars_widgets is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1120
		$sidebars_widgets = get_option( 'sidebars_widgets', array() );
1121
1122
		unset( $sidebars_widgets['array_version'] );
1123
		return $sidebars_widgets;
1124
	}
1125
1126
	/**
1127
	 * Enqueues scripts for the Customizer preview.
1128
	 *
1129
	 * @since 3.9.0
1130
	 * @access public
1131
	 */
1132
	public function customize_preview_enqueue() {
1133
		wp_enqueue_script( 'customize-preview-widgets' );
1134
	}
1135
1136
	/**
1137
	 * Inserts default style for highlighted widget at early point so theme
1138
	 * stylesheet can override.
1139
	 *
1140
	 * @since 3.9.0
1141
	 * @access public
1142
	 */
1143
	public function print_preview_css() {
1144
		?>
1145
		<style>
1146
		.widget-customizer-highlighted-widget {
1147
			outline: none;
1148
			-webkit-box-shadow: 0 0 2px rgba(30,140,190,0.8);
1149
			box-shadow: 0 0 2px rgba(30,140,190,0.8);
1150
			position: relative;
1151
			z-index: 1;
1152
		}
1153
		</style>
1154
		<?php
1155
	}
1156
1157
	/**
1158
	 * Communicates the sidebars that appeared on the page at the very end of the page,
1159
	 * and at the very end of the wp_footer,
1160
	 *
1161
	 * @since 3.9.0
1162
	 * @access public
1163
     *
1164
	 * @global array $wp_registered_sidebars
1165
	 * @global array $wp_registered_widgets
1166
	 */
1167
	public function export_preview_data() {
1168
		global $wp_registered_sidebars, $wp_registered_widgets;
1169
1170
		$switched_locale = switch_to_locale( get_user_locale() );
1171
		$l10n = array(
1172
			'widgetTooltip'  => __( 'Shift-click to edit this widget.' ),
1173
		);
1174
		if ( $switched_locale ) {
1175
			restore_previous_locale();
1176
		}
1177
1178
		// Prepare Customizer settings to pass to JavaScript.
1179
		$settings = array(
1180
			'renderedSidebars'   => array_fill_keys( array_unique( $this->rendered_sidebars ), true ),
1181
			'renderedWidgets'    => array_fill_keys( array_keys( $this->rendered_widgets ), true ),
1182
			'registeredSidebars' => array_values( $wp_registered_sidebars ),
1183
			'registeredWidgets'  => $wp_registered_widgets,
1184
			'l10n'               => $l10n,
1185
			'selectiveRefreshableWidgets' => $this->get_selective_refreshable_widgets(),
1186
		);
1187
		foreach ( $settings['registeredWidgets'] as &$registered_widget ) {
1188
			unset( $registered_widget['callback'] ); // may not be JSON-serializeable
1189
		}
1190
1191
		?>
1192
		<script type="text/javascript">
1193
			var _wpWidgetCustomizerPreviewSettings = <?php echo wp_json_encode( $settings ); ?>;
1194
		</script>
1195
		<?php
1196
	}
1197
1198
	/**
1199
	 * Tracks the widgets that were rendered.
1200
	 *
1201
	 * @since 3.9.0
1202
	 * @access public
1203
	 *
1204
	 * @param array $widget Rendered widget to tally.
1205
	 */
1206
	public function tally_rendered_widgets( $widget ) {
1207
		$this->rendered_widgets[ $widget['id'] ] = true;
1208
	}
1209
1210
	/**
1211
	 * Determine if a widget is rendered on the page.
1212
	 *
1213
	 * @since 4.0.0
1214
	 * @access public
1215
	 *
1216
	 * @param string $widget_id Widget ID to check.
1217
	 * @return bool Whether the widget is rendered.
1218
	 */
1219
	public function is_widget_rendered( $widget_id ) {
1220
		return in_array( $widget_id, $this->rendered_widgets );
1221
	}
1222
1223
	/**
1224
	 * Determines if a sidebar is rendered on the page.
1225
	 *
1226
	 * @since 4.0.0
1227
	 * @access public
1228
	 *
1229
	 * @param string $sidebar_id Sidebar ID to check.
1230
	 * @return bool Whether the sidebar is rendered.
1231
	 */
1232
	public function is_sidebar_rendered( $sidebar_id ) {
1233
		return in_array( $sidebar_id, $this->rendered_sidebars );
1234
	}
1235
1236
	/**
1237
	 * Tallies the sidebars rendered via is_active_sidebar().
1238
	 *
1239
	 * Keep track of the times that is_active_sidebar() is called in the template,
1240
	 * and assume that this means that the sidebar would be rendered on the template
1241
	 * if there were widgets populating it.
1242
	 *
1243
	 * @since 3.9.0
1244
	 * @access public
1245
	 *
1246
	 * @param bool   $is_active  Whether the sidebar is active.
1247
	 * @param string $sidebar_id Sidebar ID.
1248
	 * @return bool Whether the sidebar is active.
1249
	 */
1250
	public function tally_sidebars_via_is_active_sidebar_calls( $is_active, $sidebar_id ) {
1251
		if ( is_registered_sidebar( $sidebar_id ) ) {
1252
			$this->rendered_sidebars[] = $sidebar_id;
1253
		}
1254
		/*
1255
		 * We may need to force this to true, and also force-true the value
1256
		 * for 'dynamic_sidebar_has_widgets' if we want to ensure that there
1257
		 * is an area to drop widgets into, if the sidebar is empty.
1258
		 */
1259
		return $is_active;
1260
	}
1261
1262
	/**
1263
	 * Tallies the sidebars rendered via dynamic_sidebar().
1264
	 *
1265
	 * Keep track of the times that dynamic_sidebar() is called in the template,
1266
	 * and assume this means the sidebar would be rendered on the template if
1267
	 * there were widgets populating it.
1268
	 *
1269
	 * @since 3.9.0
1270
	 * @access public
1271
	 *
1272
	 * @param bool   $has_widgets Whether the current sidebar has widgets.
1273
	 * @param string $sidebar_id  Sidebar ID.
1274
	 * @return bool Whether the current sidebar has widgets.
1275
	 */
1276
	public function tally_sidebars_via_dynamic_sidebar_calls( $has_widgets, $sidebar_id ) {
1277
		if ( is_registered_sidebar( $sidebar_id ) ) {
1278
			$this->rendered_sidebars[] = $sidebar_id;
1279
		}
1280
1281
		/*
1282
		 * We may need to force this to true, and also force-true the value
1283
		 * for 'is_active_sidebar' if we want to ensure there is an area to
1284
		 * drop widgets into, if the sidebar is empty.
1285
		 */
1286
		return $has_widgets;
1287
	}
1288
1289
	/**
1290
	 * Retrieves MAC for a serialized widget instance string.
1291
	 *
1292
	 * Allows values posted back from JS to be rejected if any tampering of the
1293
	 * data has occurred.
1294
	 *
1295
	 * @since 3.9.0
1296
	 * @access protected
1297
	 *
1298
	 * @param string $serialized_instance Widget instance.
1299
	 * @return string MAC for serialized widget instance.
0 ignored issues
show
Should the return type not be string|false?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
1300
	 */
1301
	protected function get_instance_hash_key( $serialized_instance ) {
1302
		return wp_hash( $serialized_instance );
1303
	}
1304
1305
	/**
1306
	 * Sanitizes a widget instance.
1307
	 *
1308
	 * Unserialize the JS-instance for storing in the options. It's important that this filter
1309
	 * only get applied to an instance *once*.
1310
	 *
1311
	 * @since 3.9.0
1312
	 * @access public
1313
	 *
1314
	 * @param array $value Widget instance to sanitize.
1315
	 * @return array|void Sanitized widget instance.
1316
	 */
1317
	public function sanitize_widget_instance( $value ) {
1318
		if ( $value === array() ) {
1319
			return $value;
1320
		}
1321
1322
		if ( empty( $value['is_widget_customizer_js_value'] )
1323
			|| empty( $value['instance_hash_key'] )
1324
			|| empty( $value['encoded_serialized_instance'] ) )
1325
		{
1326
			return;
1327
		}
1328
1329
		$decoded = base64_decode( $value['encoded_serialized_instance'], true );
1330
		if ( false === $decoded ) {
1331
			return;
1332
		}
1333
1334
		if ( ! hash_equals( $this->get_instance_hash_key( $decoded ), $value['instance_hash_key'] ) ) {
0 ignored issues
show
It seems like $this->get_instance_hash_key($decoded) targeting WP_Customize_Widgets::get_instance_hash_key() can also be of type false; however, hash_equals() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
1335
			return;
1336
		}
1337
1338
		$instance = unserialize( $decoded );
1339
		if ( false === $instance ) {
1340
			return;
1341
		}
1342
1343
		return $instance;
1344
	}
1345
1346
	/**
1347
	 * Converts a widget instance into JSON-representable format.
1348
	 *
1349
	 * @since 3.9.0
1350
	 * @access public
1351
	 *
1352
	 * @param array $value Widget instance to convert to JSON.
1353
	 * @return array JSON-converted widget instance.
1354
	 */
1355
	public function sanitize_widget_js_instance( $value ) {
1356
		if ( empty( $value['is_widget_customizer_js_value'] ) ) {
1357
			$serialized = serialize( $value );
1358
1359
			$value = array(
1360
				'encoded_serialized_instance'   => base64_encode( $serialized ),
1361
				'title'                         => empty( $value['title'] ) ? '' : $value['title'],
1362
				'is_widget_customizer_js_value' => true,
1363
				'instance_hash_key'             => $this->get_instance_hash_key( $serialized ),
1364
			);
1365
		}
1366
		return $value;
1367
	}
1368
1369
	/**
1370
	 * Strips out widget IDs for widgets which are no longer registered.
1371
	 *
1372
	 * One example where this might happen is when a plugin orphans a widget
1373
	 * in a sidebar upon deactivation.
1374
	 *
1375
	 * @since 3.9.0
1376
	 * @access public
1377
	 *
1378
	 * @global array $wp_registered_widgets
1379
	 *
1380
	 * @param array $widget_ids List of widget IDs.
1381
	 * @return array Parsed list of widget IDs.
1382
	 */
1383
	public function sanitize_sidebar_widgets_js_instance( $widget_ids ) {
1384
		global $wp_registered_widgets;
1385
		$widget_ids = array_values( array_intersect( $widget_ids, array_keys( $wp_registered_widgets ) ) );
1386
		return $widget_ids;
1387
	}
1388
1389
	/**
1390
	 * Finds and invokes the widget update and control callbacks.
1391
	 *
1392
	 * Requires that `$_POST` be populated with the instance data.
1393
	 *
1394
	 * @since 3.9.0
1395
	 * @access public
1396
	 *
1397
	 * @global array $wp_registered_widget_updates
1398
	 * @global array $wp_registered_widget_controls
1399
	 *
1400
	 * @param  string $widget_id Widget ID.
1401
	 * @return WP_Error|array Array containing the updated widget information.
1402
	 *                        A WP_Error object, otherwise.
1403
	 */
1404
	public function call_widget_update( $widget_id ) {
1405
		global $wp_registered_widget_updates, $wp_registered_widget_controls;
1406
1407
		$setting_id = $this->get_setting_id( $widget_id );
1408
1409
		/*
1410
		 * Make sure that other setting changes have previewed since this widget
1411
		 * may depend on them (e.g. Menus being present for Custom Menu widget).
1412
		 */
1413
		if ( ! did_action( 'customize_preview_init' ) ) {
1414
			foreach ( $this->manager->settings() as $setting ) {
1415
				if ( $setting->id !== $setting_id ) {
1416
					$setting->preview();
1417
				}
1418
			}
1419
		}
1420
1421
		$this->start_capturing_option_updates();
1422
		$parsed_id   = $this->parse_widget_id( $widget_id );
1423
		$option_name = 'widget_' . $parsed_id['id_base'];
1424
1425
		/*
1426
		 * If a previously-sanitized instance is provided, populate the input vars
1427
		 * with its values so that the widget update callback will read this instance
1428
		 */
1429
		$added_input_vars = array();
1430
		if ( ! empty( $_POST['sanitized_widget_setting'] ) ) {
1431
			$sanitized_widget_setting = json_decode( $this->get_post_value( 'sanitized_widget_setting' ), true );
1432
			if ( false === $sanitized_widget_setting ) {
1433
				$this->stop_capturing_option_updates();
1434
				return new WP_Error( 'widget_setting_malformed' );
1435
			}
1436
1437
			$instance = $this->sanitize_widget_instance( $sanitized_widget_setting );
1438
			if ( is_null( $instance ) ) {
1439
				$this->stop_capturing_option_updates();
1440
				return new WP_Error( 'widget_setting_unsanitized' );
1441
			}
1442
1443
			if ( ! is_null( $parsed_id['number'] ) ) {
1444
				$value = array();
1445
				$value[$parsed_id['number']] = $instance;
1446
				$key = 'widget-' . $parsed_id['id_base'];
1447
				$_REQUEST[$key] = $_POST[$key] = wp_slash( $value );
1448
				$added_input_vars[] = $key;
1449
			} else {
1450
				foreach ( $instance as $key => $value ) {
1451
					$_REQUEST[$key] = $_POST[$key] = wp_slash( $value );
1452
					$added_input_vars[] = $key;
1453
				}
1454
			}
1455
		}
1456
1457
		// Invoke the widget update callback.
1458 View Code Duplication
		foreach ( (array) $wp_registered_widget_updates as $name => $control ) {
1459
			if ( $name === $parsed_id['id_base'] && is_callable( $control['callback'] ) ) {
1460
				ob_start();
1461
				call_user_func_array( $control['callback'], $control['params'] );
1462
				ob_end_clean();
1463
				break;
1464
			}
1465
		}
1466
1467
		// Clean up any input vars that were manually added
1468
		foreach ( $added_input_vars as $key ) {
1469
			unset( $_POST[ $key ] );
1470
			unset( $_REQUEST[ $key ] );
1471
		}
1472
1473
		// Make sure the expected option was updated.
1474
		if ( 0 !== $this->count_captured_options() ) {
1475
			if ( $this->count_captured_options() > 1 ) {
1476
				$this->stop_capturing_option_updates();
1477
				return new WP_Error( 'widget_setting_too_many_options' );
1478
			}
1479
1480
			$updated_option_name = key( $this->get_captured_options() );
1481
			if ( $updated_option_name !== $option_name ) {
1482
				$this->stop_capturing_option_updates();
1483
				return new WP_Error( 'widget_setting_unexpected_option' );
1484
			}
1485
		}
1486
1487
		// Obtain the widget instance.
1488
		$option = $this->get_captured_option( $option_name );
1489
		if ( null !== $parsed_id['number'] ) {
1490
			$instance = $option[ $parsed_id['number'] ];
1491
		} else {
1492
			$instance = $option;
1493
		}
1494
1495
		/*
1496
		 * Override the incoming $_POST['customized'] for a newly-created widget's
1497
		 * setting with the new $instance so that the preview filter currently
1498
		 * in place from WP_Customize_Setting::preview() will use this value
1499
		 * instead of the default widget instance value (an empty array).
1500
		 */
1501
		$this->manager->set_post_value( $setting_id, $this->sanitize_widget_js_instance( $instance ) );
1502
1503
		// Obtain the widget control with the updated instance in place.
1504
		ob_start();
1505
		$form = $wp_registered_widget_controls[ $widget_id ];
1506
		if ( $form ) {
1507
			call_user_func_array( $form['callback'], $form['params'] );
1508
		}
1509
		$form = ob_get_clean();
1510
1511
		$this->stop_capturing_option_updates();
1512
1513
		return compact( 'instance', 'form' );
1514
	}
1515
1516
	/**
1517
	 * Updates widget settings asynchronously.
1518
	 *
1519
	 * Allows the Customizer to update a widget using its form, but return the new
1520
	 * instance info via Ajax instead of saving it to the options table.
1521
	 *
1522
	 * Most code here copied from wp_ajax_save_widget().
1523
	 *
1524
	 * @since 3.9.0
1525
	 * @access public
1526
	 *
1527
	 * @see wp_ajax_save_widget()
1528
	 */
1529
	public function wp_ajax_update_widget() {
1530
1531
		if ( ! is_user_logged_in() ) {
1532
			wp_die( 0 );
1533
		}
1534
1535
		check_ajax_referer( 'update-widget', 'nonce' );
1536
1537
		if ( ! current_user_can( 'edit_theme_options' ) ) {
1538
			wp_die( -1 );
1539
		}
1540
1541
		if ( empty( $_POST['widget-id'] ) ) {
1542
			wp_send_json_error( 'missing_widget-id' );
1543
		}
1544
1545
		/** This action is documented in wp-admin/includes/ajax-actions.php */
1546
		do_action( 'load-widgets.php' );
1547
1548
		/** This action is documented in wp-admin/includes/ajax-actions.php */
1549
		do_action( 'widgets.php' );
1550
1551
		/** This action is documented in wp-admin/widgets.php */
1552
		do_action( 'sidebar_admin_setup' );
1553
1554
		$widget_id = $this->get_post_value( 'widget-id' );
1555
		$parsed_id = $this->parse_widget_id( $widget_id );
1556
		$id_base = $parsed_id['id_base'];
1557
1558
		$is_updating_widget_template = (
1559
			isset( $_POST[ 'widget-' . $id_base ] )
1560
			&&
1561
			is_array( $_POST[ 'widget-' . $id_base ] )
1562
			&&
1563
			preg_match( '/__i__|%i%/', key( $_POST[ 'widget-' . $id_base ] ) )
1564
		);
1565
		if ( $is_updating_widget_template ) {
1566
			wp_send_json_error( 'template_widget_not_updatable' );
1567
		}
1568
1569
		$updated_widget = $this->call_widget_update( $widget_id ); // => {instance,form}
1570
		if ( is_wp_error( $updated_widget ) ) {
1571
			wp_send_json_error( $updated_widget->get_error_code() );
1572
		}
1573
1574
		$form = $updated_widget['form'];
1575
		$instance = $this->sanitize_widget_js_instance( $updated_widget['instance'] );
1576
1577
		wp_send_json_success( compact( 'form', 'instance' ) );
1578
	}
1579
1580
	/*
1581
	 * Selective Refresh Methods
1582
	 */
1583
1584
	/**
1585
	 * Filters arguments for dynamic widget partials.
1586
	 *
1587
	 * @since 4.5.0
1588
	 * @access public
1589
	 *
1590
	 * @param array|false $partial_args Partial arguments.
1591
	 * @param string      $partial_id   Partial ID.
1592
	 * @return array (Maybe) modified partial arguments.
0 ignored issues
show
Should the return type not be array|false? Also, consider making the array more specific, something like array<String>, or String[].

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

If the return type contains the type array, this check recommends the use of a more specific type like String[] or array<String>.

Loading history...
1593
	 */
1594
	public function customize_dynamic_partial_args( $partial_args, $partial_id ) {
1595
		if ( ! current_theme_supports( 'customize-selective-refresh-widgets' ) ) {
1596
			return $partial_args;
1597
		}
1598
1599 View Code Duplication
		if ( preg_match( '/^widget\[(?P<widget_id>.+)\]$/', $partial_id, $matches ) ) {
1600
			if ( false === $partial_args ) {
1601
				$partial_args = array();
1602
			}
1603
			$partial_args = array_merge(
1604
				$partial_args,
1605
				array(
1606
					'type'                => 'widget',
1607
					'render_callback'     => array( $this, 'render_widget_partial' ),
1608
					'container_inclusive' => true,
1609
					'settings'            => array( $this->get_setting_id( $matches['widget_id'] ) ),
1610
					'capability'          => 'edit_theme_options',
1611
				)
1612
			);
1613
		}
1614
1615
		return $partial_args;
1616
	}
1617
1618
	/**
1619
	 * Adds hooks for selective refresh.
1620
	 *
1621
	 * @since 4.5.0
1622
	 * @access public
1623
	 */
1624
	public function selective_refresh_init() {
1625
		if ( ! current_theme_supports( 'customize-selective-refresh-widgets' ) ) {
1626
			return;
1627
		}
1628
		add_filter( 'dynamic_sidebar_params', array( $this, 'filter_dynamic_sidebar_params' ) );
1629
		add_filter( 'wp_kses_allowed_html', array( $this, 'filter_wp_kses_allowed_data_attributes' ) );
1630
		add_action( 'dynamic_sidebar_before', array( $this, 'start_dynamic_sidebar' ) );
1631
		add_action( 'dynamic_sidebar_after', array( $this, 'end_dynamic_sidebar' ) );
1632
	}
1633
1634
	/**
1635
	 * Inject selective refresh data attributes into widget container elements.
1636
	 *
1637
	 * @param array $params {
1638
	 *     Dynamic sidebar params.
1639
	 *
1640
	 *     @type array $args        Sidebar args.
1641
	 *     @type array $widget_args Widget args.
1642
	 * }
1643
	 * @see WP_Customize_Nav_Menus_Partial_Refresh::filter_wp_nav_menu_args()
1644
	 *
1645
	 * @return array Params.
1646
	 */
1647
	public function filter_dynamic_sidebar_params( $params ) {
1648
		$sidebar_args = array_merge(
1649
			array(
1650
				'before_widget' => '',
1651
				'after_widget' => '',
1652
			),
1653
			$params[0]
1654
		);
1655
1656
		// Skip widgets not in a registered sidebar or ones which lack a proper wrapper element to attach the data-* attributes to.
1657
		$matches = array();
1658
		$is_valid = (
1659
			isset( $sidebar_args['id'] )
1660
			&&
1661
			is_registered_sidebar( $sidebar_args['id'] )
1662
			&&
1663
			( isset( $this->current_dynamic_sidebar_id_stack[0] ) && $this->current_dynamic_sidebar_id_stack[0] === $sidebar_args['id'] )
1664
			&&
1665
			preg_match( '#^<(?P<tag_name>\w+)#', $sidebar_args['before_widget'], $matches )
1666
		);
1667
		if ( ! $is_valid ) {
1668
			return $params;
1669
		}
1670
		$this->before_widget_tags_seen[ $matches['tag_name'] ] = true;
1671
1672
		$context = array(
1673
			'sidebar_id' => $sidebar_args['id'],
1674
		);
1675
		if ( isset( $this->context_sidebar_instance_number ) ) {
1676
			$context['sidebar_instance_number'] = $this->context_sidebar_instance_number;
1677
		} else if ( isset( $sidebar_args['id'] ) && isset( $this->sidebar_instance_count[ $sidebar_args['id'] ] ) ) {
1678
			$context['sidebar_instance_number'] = $this->sidebar_instance_count[ $sidebar_args['id'] ];
1679
		}
1680
1681
		$attributes = sprintf( ' data-customize-partial-id="%s"', esc_attr( 'widget[' . $sidebar_args['widget_id'] . ']' ) );
1682
		$attributes .= ' data-customize-partial-type="widget"';
1683
		$attributes .= sprintf( ' data-customize-partial-placement-context="%s"', esc_attr( wp_json_encode( $context ) ) );
0 ignored issues
show
It seems like wp_json_encode($context) targeting wp_json_encode() can also be of type false; however, esc_attr() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
1684
		$attributes .= sprintf( ' data-customize-widget-id="%s"', esc_attr( $sidebar_args['widget_id'] ) );
1685
		$sidebar_args['before_widget'] = preg_replace( '#^(<\w+)#', '$1 ' . $attributes, $sidebar_args['before_widget'] );
1686
1687
		$params[0] = $sidebar_args;
1688
		return $params;
1689
	}
1690
1691
	/**
1692
	 * List of the tag names seen for before_widget strings.
1693
	 *
1694
	 * This is used in the {@see 'filter_wp_kses_allowed_html'} filter to ensure that the
1695
	 * data-* attributes can be whitelisted.
1696
	 *
1697
	 * @since 4.5.0
1698
	 * @access protected
1699
	 * @var array
1700
	 */
1701
	protected $before_widget_tags_seen = array();
1702
1703
	/**
1704
	 * Ensures the HTML data-* attributes for selective refresh are allowed by kses.
1705
	 *
1706
	 * This is needed in case the `$before_widget` is run through wp_kses() when printed.
1707
	 *
1708
	 * @since 4.5.0
1709
	 * @access public
1710
	 *
1711
	 * @param array $allowed_html Allowed HTML.
1712
	 * @return array (Maybe) modified allowed HTML.
1713
	 */
1714
	public function filter_wp_kses_allowed_data_attributes( $allowed_html ) {
1715
		foreach ( array_keys( $this->before_widget_tags_seen ) as $tag_name ) {
1716
			if ( ! isset( $allowed_html[ $tag_name ] ) ) {
1717
				$allowed_html[ $tag_name ] = array();
1718
			}
1719
			$allowed_html[ $tag_name ] = array_merge(
1720
				$allowed_html[ $tag_name ],
1721
				array_fill_keys( array(
1722
					'data-customize-partial-id',
1723
					'data-customize-partial-type',
1724
					'data-customize-partial-placement-context',
1725
					'data-customize-partial-widget-id',
1726
					'data-customize-partial-options',
1727
				), true )
1728
			);
1729
		}
1730
		return $allowed_html;
1731
	}
1732
1733
	/**
1734
	 * Keep track of the number of times that dynamic_sidebar() was called for a given sidebar index.
1735
	 *
1736
	 * This helps facilitate the uncommon scenario where a single sidebar is rendered multiple times on a template.
1737
	 *
1738
	 * @since 4.5.0
1739
	 * @access protected
1740
	 * @var array
1741
	 */
1742
	protected $sidebar_instance_count = array();
1743
1744
	/**
1745
	 * The current request's sidebar_instance_number context.
1746
	 *
1747
	 * @since 4.5.0
1748
	 * @access protected
1749
	 * @var int
1750
	 */
1751
	protected $context_sidebar_instance_number;
1752
1753
	/**
1754
	 * Current sidebar ID being rendered.
1755
	 *
1756
	 * @since 4.5.0
1757
	 * @access protected
1758
	 * @var array
1759
	 */
1760
	protected $current_dynamic_sidebar_id_stack = array();
1761
1762
	/**
1763
	 * Begins keeping track of the current sidebar being rendered.
1764
	 *
1765
	 * Insert marker before widgets are rendered in a dynamic sidebar.
1766
	 *
1767
	 * @since 4.5.0
1768
	 * @access public
1769
	 *
1770
	 * @param int|string $index Index, name, or ID of the dynamic sidebar.
1771
	 */
1772
	public function start_dynamic_sidebar( $index ) {
1773
		array_unshift( $this->current_dynamic_sidebar_id_stack, $index );
1774
		if ( ! isset( $this->sidebar_instance_count[ $index ] ) ) {
1775
			$this->sidebar_instance_count[ $index ] = 0;
1776
		}
1777
		$this->sidebar_instance_count[ $index ] += 1;
1778 View Code Duplication
		if ( ! $this->manager->selective_refresh->is_render_partials_request() ) {
1779
			printf( "\n<!--dynamic_sidebar_before:%s:%d-->\n", esc_html( $index ), intval( $this->sidebar_instance_count[ $index ] ) );
1780
		}
1781
	}
1782
1783
	/**
1784
	 * Finishes keeping track of the current sidebar being rendered.
1785
	 *
1786
	 * Inserts a marker after widgets are rendered in a dynamic sidebar.
1787
	 *
1788
	 * @since 4.5.0
1789
	 * @access public
1790
	 *
1791
	 * @param int|string $index Index, name, or ID of the dynamic sidebar.
1792
	 */
1793
	public function end_dynamic_sidebar( $index ) {
1794
		array_shift( $this->current_dynamic_sidebar_id_stack );
1795 View Code Duplication
		if ( ! $this->manager->selective_refresh->is_render_partials_request() ) {
1796
			printf( "\n<!--dynamic_sidebar_after:%s:%d-->\n", esc_html( $index ), intval( $this->sidebar_instance_count[ $index ] ) );
1797
		}
1798
	}
1799
1800
	/**
1801
	 * Current sidebar being rendered.
1802
	 *
1803
	 * @since 4.5.0
1804
	 * @access protected
1805
	 * @var string
1806
	 */
1807
	protected $rendering_widget_id;
1808
1809
	/**
1810
	 * Current widget being rendered.
1811
	 *
1812
	 * @since 4.5.0
1813
	 * @access protected
1814
	 * @var string
1815
	 */
1816
	protected $rendering_sidebar_id;
1817
1818
	/**
1819
	 * Filters sidebars_widgets to ensure the currently-rendered widget is the only widget in the current sidebar.
1820
	 *
1821
	 * @since 4.5.0
1822
	 * @access protected
1823
	 *
1824
	 * @param array $sidebars_widgets Sidebars widgets.
1825
	 * @return array Filtered sidebars widgets.
0 ignored issues
show
Consider making the return type a bit more specific; maybe use array<string,string[]>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
1826
	 */
1827
	public function filter_sidebars_widgets_for_rendering_widget( $sidebars_widgets ) {
1828
		$sidebars_widgets[ $this->rendering_sidebar_id ] = array( $this->rendering_widget_id );
1829
		return $sidebars_widgets;
1830
	}
1831
1832
	/**
1833
	 * Renders a specific widget using the supplied sidebar arguments.
1834
	 *
1835
	 * @since 4.5.0
1836
	 * @access public
1837
	 *
1838
	 * @see dynamic_sidebar()
1839
	 *
1840
	 * @param WP_Customize_Partial $partial Partial.
1841
	 * @param array                $context {
1842
	 *     Sidebar args supplied as container context.
1843
	 *
1844
	 *     @type string $sidebar_id              ID for sidebar for widget to render into.
1845
	 *     @type int    $sidebar_instance_number Disambiguating instance number.
1846
	 * }
1847
	 * @return string|false
1848
	 */
1849
	public function render_widget_partial( $partial, $context ) {
1850
		$id_data   = $partial->id_data();
1851
		$widget_id = array_shift( $id_data['keys'] );
1852
1853
		if ( ! is_array( $context )
1854
			|| empty( $context['sidebar_id'] )
1855
			|| ! is_registered_sidebar( $context['sidebar_id'] )
1856
		) {
1857
			return false;
1858
		}
1859
1860
		$this->rendering_sidebar_id = $context['sidebar_id'];
1861
1862
		if ( isset( $context['sidebar_instance_number'] ) ) {
1863
			$this->context_sidebar_instance_number = intval( $context['sidebar_instance_number'] );
1864
		}
1865
1866
		// Filter sidebars_widgets so that only the queried widget is in the sidebar.
1867
		$this->rendering_widget_id = $widget_id;
1868
1869
		$filter_callback = array( $this, 'filter_sidebars_widgets_for_rendering_widget' );
1870
		add_filter( 'sidebars_widgets', $filter_callback, 1000 );
1871
1872
		// Render the widget.
1873
		ob_start();
1874
		dynamic_sidebar( $this->rendering_sidebar_id = $context['sidebar_id'] );
1875
		$container = ob_get_clean();
1876
1877
		// Reset variables for next partial render.
1878
		remove_filter( 'sidebars_widgets', $filter_callback, 1000 );
1879
1880
		$this->context_sidebar_instance_number = null;
1881
		$this->rendering_sidebar_id = null;
1882
		$this->rendering_widget_id = null;
1883
1884
		return $container;
1885
	}
1886
1887
	//
1888
	// Option Update Capturing
1889
	//
1890
1891
	/**
1892
	 * List of captured widget option updates.
1893
	 *
1894
	 * @since 3.9.0
1895
	 * @access protected
1896
	 * @var array $_captured_options Values updated while option capture is happening.
1897
	 */
1898
	protected $_captured_options = array();
1899
1900
	/**
1901
	 * Whether option capture is currently happening.
1902
	 *
1903
	 * @since 3.9.0
1904
	 * @access protected
1905
	 * @var bool $_is_current Whether option capture is currently happening or not.
1906
	 */
1907
	protected $_is_capturing_option_updates = false;
1908
1909
	/**
1910
	 * Determines whether the captured option update should be ignored.
1911
	 *
1912
	 * @since 3.9.0
1913
	 * @access protected
1914
	 *
1915
	 * @param string $option_name Option name.
1916
	 * @return bool Whether the option capture is ignored.
1917
	 */
1918
	protected function is_option_capture_ignored( $option_name ) {
1919
		return ( 0 === strpos( $option_name, '_transient_' ) );
1920
	}
1921
1922
	/**
1923
	 * Retrieves captured widget option updates.
1924
	 *
1925
	 * @since 3.9.0
1926
	 * @access protected
1927
	 *
1928
	 * @return array Array of captured options.
1929
	 */
1930
	protected function get_captured_options() {
1931
		return $this->_captured_options;
1932
	}
1933
1934
	/**
1935
	 * Retrieves the option that was captured from being saved.
1936
	 *
1937
	 * @since 4.2.0
1938
	 * @access protected
1939
	 *
1940
	 * @param string $option_name Option name.
1941
	 * @param mixed  $default     Optional. Default value to return if the option does not exist. Default false.
1942
	 * @return mixed Value set for the option.
1943
	 */
1944
	protected function get_captured_option( $option_name, $default = false ) {
1945
		if ( array_key_exists( $option_name, $this->_captured_options ) ) {
1946
			$value = $this->_captured_options[ $option_name ];
1947
		} else {
1948
			$value = $default;
1949
		}
1950
		return $value;
1951
	}
1952
1953
	/**
1954
	 * Retrieves the number of captured widget option updates.
1955
	 *
1956
	 * @since 3.9.0
1957
	 * @access protected
1958
	 *
1959
	 * @return int Number of updated options.
1960
	 */
1961
	protected function count_captured_options() {
1962
		return count( $this->_captured_options );
1963
	}
1964
1965
	/**
1966
	 * Begins keeping track of changes to widget options, caching new values.
1967
	 *
1968
	 * @since 3.9.0
1969
	 * @access protected
1970
	 */
1971
	protected function start_capturing_option_updates() {
1972
		if ( $this->_is_capturing_option_updates ) {
1973
			return;
1974
		}
1975
1976
		$this->_is_capturing_option_updates = true;
1977
1978
		add_filter( 'pre_update_option', array( $this, 'capture_filter_pre_update_option' ), 10, 3 );
1979
	}
1980
1981
	/**
1982
	 * Pre-filters captured option values before updating.
1983
	 *
1984
	 * @since 3.9.0
1985
	 * @access public
1986
	 *
1987
	 * @param mixed  $new_value   The new option value.
1988
	 * @param string $option_name Name of the option.
1989
	 * @param mixed  $old_value   The old option value.
1990
	 * @return mixed Filtered option value.
1991
	 */
1992
	public function capture_filter_pre_update_option( $new_value, $option_name, $old_value ) {
1993
		if ( $this->is_option_capture_ignored( $option_name ) ) {
1994
			return;
1995
		}
1996
1997
		if ( ! isset( $this->_captured_options[ $option_name ] ) ) {
1998
			add_filter( "pre_option_{$option_name}", array( $this, 'capture_filter_pre_get_option' ) );
1999
		}
2000
2001
		$this->_captured_options[ $option_name ] = $new_value;
2002
2003
		return $old_value;
2004
	}
2005
2006
	/**
2007
	 * Pre-filters captured option values before retrieving.
2008
	 *
2009
	 * @since 3.9.0
2010
	 * @access public
2011
	 *
2012
	 * @param mixed $value Value to return instead of the option value.
2013
	 * @return mixed Filtered option value.
2014
	 */
2015
	public function capture_filter_pre_get_option( $value ) {
2016
		$option_name = preg_replace( '/^pre_option_/', '', current_filter() );
2017
2018
		if ( isset( $this->_captured_options[ $option_name ] ) ) {
2019
			$value = $this->_captured_options[ $option_name ];
2020
2021
			/** This filter is documented in wp-includes/option.php */
2022
			$value = apply_filters( 'option_' . $option_name, $value );
2023
		}
2024
2025
		return $value;
2026
	}
2027
2028
	/**
2029
	 * Undoes any changes to the options since options capture began.
2030
	 *
2031
	 * @since 3.9.0
2032
	 * @access protected
2033
	 */
2034
	protected function stop_capturing_option_updates() {
2035
		if ( ! $this->_is_capturing_option_updates ) {
2036
			return;
2037
		}
2038
2039
		remove_filter( 'pre_update_option', array( $this, 'capture_filter_pre_update_option' ), 10 );
2040
2041
		foreach ( array_keys( $this->_captured_options ) as $option_name ) {
2042
			remove_filter( "pre_option_{$option_name}", array( $this, 'capture_filter_pre_get_option' ) );
2043
		}
2044
2045
		$this->_captured_options = array();
2046
		$this->_is_capturing_option_updates = false;
2047
	}
2048
2049
	/**
2050
	 * {@internal Missing Summary}
2051
	 *
2052
	 * See the {@see 'customize_dynamic_setting_args'} filter.
2053
	 *
2054
	 * @since 3.9.0
2055
	 * @deprecated 4.2.0 Deprecated in favor of the {@see 'customize_dynamic_setting_args'} filter.
2056
	 */
2057
	public function setup_widget_addition_previews() {
2058
		_deprecated_function( __METHOD__, '4.2.0' );
2059
	}
2060
2061
	/**
2062
	 * {@internal Missing Summary}
2063
	 *
2064
	 * See the {@see 'customize_dynamic_setting_args'} filter.
2065
	 *
2066
	 * @since 3.9.0
2067
	 * @deprecated 4.2.0 Deprecated in favor of the {@see 'customize_dynamic_setting_args'} filter.
2068
	 */
2069
	public function prepreview_added_sidebars_widgets() {
2070
		_deprecated_function( __METHOD__, '4.2.0' );
2071
	}
2072
2073
	/**
2074
	 * {@internal Missing Summary}
2075
	 *
2076
	 * See the {@see 'customize_dynamic_setting_args'} filter.
2077
	 *
2078
	 * @since 3.9.0
2079
	 * @deprecated 4.2.0 Deprecated in favor of the {@see 'customize_dynamic_setting_args'} filter.
2080
	 */
2081
	public function prepreview_added_widget_instance() {
2082
		_deprecated_function( __METHOD__, '4.2.0' );
2083
	}
2084
2085
	/**
2086
	 * {@internal Missing Summary}
2087
	 *
2088
	 * See the {@see 'customize_dynamic_setting_args'} filter.
2089
	 *
2090
	 * @since 3.9.0
2091
	 * @deprecated 4.2.0 Deprecated in favor of the {@see 'customize_dynamic_setting_args'} filter.
2092
	 */
2093
	public function remove_prepreview_filters() {
2094
		_deprecated_function( __METHOD__, '4.2.0' );
2095
	}
2096
}
2097