Issues (2010)

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.

wp-includes/class-wp-customize-widgets.php (6 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
		// Skip useless hooks when the user can't manage widgets anyway.
97
		if ( ! current_user_can( 'edit_theme_options' ) ) {
98
			return;
99
		}
100
101
		add_filter( 'customize_dynamic_setting_args',          array( $this, 'filter_customize_dynamic_setting_args' ), 10, 2 );
102
		add_action( 'widgets_init',                            array( $this, 'register_settings' ), 95 );
103
		add_action( 'wp_loaded',                               array( $this, 'override_sidebars_widgets_for_theme_switch' ) );
104
		add_action( 'customize_controls_init',                 array( $this, 'customize_controls_init' ) );
105
		add_action( 'customize_register',                      array( $this, 'schedule_customize_register' ), 1 );
106
		add_action( 'customize_controls_enqueue_scripts',      array( $this, 'enqueue_scripts' ) );
107
		add_action( 'customize_controls_print_styles',         array( $this, 'print_styles' ) );
108
		add_action( 'customize_controls_print_scripts',        array( $this, 'print_scripts' ) );
109
		add_action( 'customize_controls_print_footer_scripts', array( $this, 'print_footer_scripts' ) );
110
		add_action( 'customize_controls_print_footer_scripts', array( $this, 'output_widget_control_templates' ) );
111
		add_action( 'customize_preview_init',                  array( $this, 'customize_preview_init' ) );
112
		add_filter( 'customize_refresh_nonces',                array( $this, 'refresh_nonces' ) );
113
114
		add_action( 'dynamic_sidebar',                         array( $this, 'tally_rendered_widgets' ) );
115
		add_filter( 'is_active_sidebar',                       array( $this, 'tally_sidebars_via_is_active_sidebar_calls' ), 10, 2 );
116
		add_filter( 'dynamic_sidebar_has_widgets',             array( $this, 'tally_sidebars_via_dynamic_sidebar_calls' ), 10, 2 );
117
118
		// Selective Refresh.
119
		add_filter( 'customize_dynamic_partial_args',          array( $this, 'customize_dynamic_partial_args' ), 10, 2 );
120
		add_action( 'customize_preview_init',                  array( $this, 'selective_refresh_init' ) );
121
	}
122
123
	/**
124
	 * List whether each registered widget can be use selective refresh.
125
	 *
126
	 * If the theme does not support the customize-selective-refresh-widgets feature,
127
	 * then this will always return an empty array.
128
	 *
129
	 * @since 4.5.0
130
	 * @access public
131
	 *
132
	 * @return array Mapping of id_base to support. If theme doesn't support
133
	 *               selective refresh, an empty array is returned.
134
	 */
135
	public function get_selective_refreshable_widgets() {
136
		global $wp_widget_factory;
137
		if ( ! current_theme_supports( 'customize-selective-refresh-widgets' ) ) {
138
			return array();
139
		}
140
		if ( ! isset( $this->selective_refreshable_widgets ) ) {
141
			$this->selective_refreshable_widgets = array();
142
			foreach ( $wp_widget_factory->widgets as $wp_widget ) {
143
				$this->selective_refreshable_widgets[ $wp_widget->id_base ] = ! empty( $wp_widget->widget_options['customize_selective_refresh'] );
144
			}
145
		}
146
		return $this->selective_refreshable_widgets;
147
	}
148
149
	/**
150
	 * Determines if a widget supports selective refresh.
151
	 *
152
	 * @since 4.5.0
153
	 * @access public
154
	 *
155
	 * @param string $id_base Widget ID Base.
156
	 * @return bool Whether the widget can be selective refreshed.
157
	 */
158
	public function is_widget_selective_refreshable( $id_base ) {
159
		$selective_refreshable_widgets = $this->get_selective_refreshable_widgets();
160
		return ! empty( $selective_refreshable_widgets[ $id_base ] );
161
	}
162
163
	/**
164
	 * Retrieves the widget setting type given a setting ID.
165
	 *
166
	 * @since 4.2.0
167
	 * @access protected
168
	 *
169
	 * @staticvar array $cache
170
	 *
171
	 * @param string $setting_id Setting ID.
172
	 * @return string|void Setting type.
173
	 */
174
	protected function get_setting_type( $setting_id ) {
175
		static $cache = array();
176
		if ( isset( $cache[ $setting_id ] ) ) {
177
			return $cache[ $setting_id ];
178
		}
179
		foreach ( $this->setting_id_patterns as $type => $pattern ) {
180
			if ( preg_match( $pattern, $setting_id ) ) {
181
				$cache[ $setting_id ] = $type;
182
				return $type;
183
			}
184
		}
185
	}
186
187
	/**
188
	 * Inspects the incoming customized data for any widget settings, and dynamically adds
189
	 * them up-front so widgets will be initialized properly.
190
	 *
191
	 * @since 4.2.0
192
	 * @access public
193
	 */
194
	public function register_settings() {
195
		$widget_setting_ids = array();
196
		$incoming_setting_ids = array_keys( $this->manager->unsanitized_post_values() );
197
		foreach ( $incoming_setting_ids as $setting_id ) {
198
			if ( ! is_null( $this->get_setting_type( $setting_id ) ) ) {
199
				$widget_setting_ids[] = $setting_id;
200
			}
201
		}
202
		if ( $this->manager->doing_ajax( 'update-widget' ) && isset( $_REQUEST['widget-id'] ) ) {
203
			$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...
204
		}
205
206
		$settings = $this->manager->add_dynamic_settings( array_unique( $widget_setting_ids ) );
207
208
		/*
209
		 * Preview settings right away so that widgets and sidebars will get registered properly.
210
		 * But don't do this if a customize_save because this will cause WP to think there is nothing
211
		 * changed that needs to be saved.
212
		 */
213
		if ( ! $this->manager->doing_ajax( 'customize_save' ) ) {
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
	 * @access public
225
	 *
226
	 * @param false|array $args       The arguments to the WP_Customize_Setting constructor.
227
	 * @param string      $setting_id ID for dynamic setting, usually coming from `$_POST['customized']`.
228
	 * @return false|array Setting arguments, false otherwise.
229
	 */
230
	public function filter_customize_dynamic_setting_args( $args, $setting_id ) {
231
		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...
232
			$args = $this->get_setting_args( $setting_id );
233
		}
234
		return $args;
235
	}
236
237
	/**
238
	 * Retrieves an unslashed post value or return a default.
239
	 *
240
	 * @since 3.9.0
241
	 * @access protected
242
	 *
243
	 * @param string $name    Post value.
244
	 * @param mixed  $default Default post value.
245
	 * @return mixed Unslashed post value or default value.
246
	 */
247
	protected function get_post_value( $name, $default = null ) {
248
		if ( ! isset( $_POST[ $name ] ) ) {
249
			return $default;
250
		}
251
252
		return wp_unslash( $_POST[ $name ] );
253
	}
254
255
	/**
256
	 * Override sidebars_widgets for theme switch.
257
	 *
258
	 * When switching a theme via the Customizer, supply any previously-configured
259
	 * sidebars_widgets from the target theme as the initial sidebars_widgets
260
	 * setting. Also store the old theme's existing settings so that they can
261
	 * be passed along for storing in the sidebars_widgets theme_mod when the
262
	 * theme gets switched.
263
	 *
264
	 * @since 3.9.0
265
	 * @access public
266
	 *
267
	 * @global array $sidebars_widgets
268
	 * @global array $_wp_sidebars_widgets
269
	 */
270
	public function override_sidebars_widgets_for_theme_switch() {
271
		global $sidebars_widgets;
272
273
		if ( $this->manager->doing_ajax() || $this->manager->is_theme_active() ) {
274
			return;
275
		}
276
277
		$this->old_sidebars_widgets = wp_get_sidebars_widgets();
278
		add_filter( 'customize_value_old_sidebars_widgets_data', array( $this, 'filter_customize_value_old_sidebars_widgets_data' ) );
279
280
		// retrieve_widgets() looks at the global $sidebars_widgets
281
		$sidebars_widgets = $this->old_sidebars_widgets;
282
		$sidebars_widgets = retrieve_widgets( 'customize' );
283
		add_filter( 'option_sidebars_widgets', array( $this, 'filter_option_sidebars_widgets_for_theme_switch' ), 1 );
284
		// reset global cache var used by wp_get_sidebars_widgets()
285
		unset( $GLOBALS['_wp_sidebars_widgets'] );
286
	}
287
288
	/**
289
	 * Filters old_sidebars_widgets_data Customizer setting.
290
	 *
291
	 * When switching themes, filter the Customizer setting old_sidebars_widgets_data
292
	 * to supply initial $sidebars_widgets before they were overridden by retrieve_widgets().
293
	 * The value for old_sidebars_widgets_data gets set in the old theme's sidebars_widgets
294
	 * theme_mod.
295
	 *
296
	 * @since 3.9.0
297
	 * @access public
298
	 *
299
	 * @see WP_Customize_Widgets::handle_theme_switch()
300
	 *
301
	 * @param array $old_sidebars_widgets
302
	 * @return array
303
	 */
304
	public function filter_customize_value_old_sidebars_widgets_data( $old_sidebars_widgets ) {
305
		return $this->old_sidebars_widgets;
306
	}
307
308
	/**
309
	 * Filters sidebars_widgets option for theme switch.
310
	 *
311
	 * When switching themes, the retrieve_widgets() function is run when the Customizer initializes,
312
	 * and then the new sidebars_widgets here get supplied as the default value for the sidebars_widgets
313
	 * option.
314
	 *
315
	 * @since 3.9.0
316
	 * @access public
317
	 *
318
	 * @see WP_Customize_Widgets::handle_theme_switch()
319
	 * @global array $sidebars_widgets
320
	 *
321
	 * @param array $sidebars_widgets
322
	 * @return array
323
	 */
324
	public function filter_option_sidebars_widgets_for_theme_switch( $sidebars_widgets ) {
325
		$sidebars_widgets = $GLOBALS['sidebars_widgets'];
326
		$sidebars_widgets['array_version'] = 3;
327
		return $sidebars_widgets;
328
	}
329
330
	/**
331
	 * Ensures all widgets get loaded into the Customizer.
332
	 *
333
	 * Note: these actions are also fired in wp_ajax_update_widget().
334
	 *
335
	 * @since 3.9.0
336
	 * @access public
337
	 */
338
	public function customize_controls_init() {
339
		/** This action is documented in wp-admin/includes/ajax-actions.php */
340
		do_action( 'load-widgets.php' );
341
342
		/** This action is documented in wp-admin/includes/ajax-actions.php */
343
		do_action( 'widgets.php' );
344
345
		/** This action is documented in wp-admin/widgets.php */
346
		do_action( 'sidebar_admin_setup' );
347
	}
348
349
	/**
350
	 * Ensures widgets are available for all types of previews.
351
	 *
352
	 * When in preview, hook to {@see 'customize_register'} for settings after WordPress is loaded
353
	 * so that all filters have been initialized (e.g. Widget Visibility).
354
	 *
355
	 * @since 3.9.0
356
	 * @access public
357
	 */
358
	public function schedule_customize_register() {
359
		if ( is_admin() ) {
360
			$this->customize_register();
361
		} else {
362
			add_action( 'wp', array( $this, 'customize_register' ) );
363
		}
364
	}
365
366
	/**
367
	 * Registers Customizer settings and controls for all sidebars and widgets.
368
	 *
369
	 * @since 3.9.0
370
	 * @access public
371
	 *
372
	 * @global array $wp_registered_widgets
373
	 * @global array $wp_registered_widget_controls
374
	 * @global array $wp_registered_sidebars
375
	 */
376
	public function customize_register() {
377
		global $wp_registered_widgets, $wp_registered_widget_controls, $wp_registered_sidebars;
378
379
		add_filter( 'sidebars_widgets', array( $this, 'preview_sidebars_widgets' ), 1 );
380
381
		$sidebars_widgets = array_merge(
382
			array( 'wp_inactive_widgets' => array() ),
383
			array_fill_keys( array_keys( $wp_registered_sidebars ), array() ),
384
			wp_get_sidebars_widgets()
385
		);
386
387
		$new_setting_ids = array();
388
389
		/*
390
		 * Register a setting for all widgets, including those which are active,
391
		 * inactive, and orphaned since a widget may get suppressed from a sidebar
392
		 * via a plugin (like Widget Visibility).
393
		 */
394
		foreach ( array_keys( $wp_registered_widgets ) as $widget_id ) {
395
			$setting_id   = $this->get_setting_id( $widget_id );
396
			$setting_args = $this->get_setting_args( $setting_id );
397
			if ( ! $this->manager->get_setting( $setting_id ) ) {
398
				$this->manager->add_setting( $setting_id, $setting_args );
399
			}
400
			$new_setting_ids[] = $setting_id;
401
		}
402
403
		/*
404
		 * Add a setting which will be supplied for the theme's sidebars_widgets
405
		 * theme_mod when the theme is switched.
406
		 */
407
		if ( ! $this->manager->is_theme_active() ) {
408
			$setting_id = 'old_sidebars_widgets_data';
409
			$setting_args = $this->get_setting_args( $setting_id, array(
410
				'type' => 'global_variable',
411
				'dirty' => true,
412
			) );
413
			$this->manager->add_setting( $setting_id, $setting_args );
414
		}
415
416
		$this->manager->add_panel( 'widgets', 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
		) );
423
424
		foreach ( $sidebars_widgets as $sidebar_id => $sidebar_widget_ids ) {
425
			if ( empty( $sidebar_widget_ids ) ) {
426
				$sidebar_widget_ids = array();
427
			}
428
429
			$is_registered_sidebar = is_registered_sidebar( $sidebar_id );
430
			$is_inactive_widgets   = ( 'wp_inactive_widgets' === $sidebar_id );
431
			$is_active_sidebar     = ( $is_registered_sidebar && ! $is_inactive_widgets );
432
433
			// Add setting for managing the sidebar's widgets.
434
			if ( $is_registered_sidebar || $is_inactive_widgets ) {
435
				$setting_id   = sprintf( 'sidebars_widgets[%s]', $sidebar_id );
436
				$setting_args = $this->get_setting_args( $setting_id );
437
				if ( ! $this->manager->get_setting( $setting_id ) ) {
438
					if ( ! $this->manager->is_theme_active() ) {
439
						$setting_args['dirty'] = true;
440
					}
441
					$this->manager->add_setting( $setting_id, $setting_args );
442
				}
443
				$new_setting_ids[] = $setting_id;
444
445
				// Add section to contain controls.
446
				$section_id = sprintf( 'sidebar-widgets-%s', $sidebar_id );
447
				if ( $is_active_sidebar ) {
448
449
					$section_args = array(
450
						'title' => $wp_registered_sidebars[ $sidebar_id ]['name'],
451
						'description' => $wp_registered_sidebars[ $sidebar_id ]['description'],
452
						'priority' => array_search( $sidebar_id, array_keys( $wp_registered_sidebars ) ),
453
						'panel' => 'widgets',
454
						'sidebar_id' => $sidebar_id,
455
					);
456
457
					/**
458
					 * Filters Customizer widget section arguments for a given sidebar.
459
					 *
460
					 * @since 3.9.0
461
					 *
462
					 * @param array      $section_args Array of Customizer widget section arguments.
463
					 * @param string     $section_id   Customizer section ID.
464
					 * @param int|string $sidebar_id   Sidebar ID.
465
					 */
466
					$section_args = apply_filters( 'customizer_widgets_section_args', $section_args, $section_id, $sidebar_id );
467
468
					$section = new WP_Customize_Sidebar_Section( $this->manager, $section_id, $section_args );
469
					$this->manager->add_section( $section );
470
471
					$control = new WP_Widget_Area_Customize_Control( $this->manager, $setting_id, array(
472
						'section'    => $section_id,
473
						'sidebar_id' => $sidebar_id,
474
						'priority'   => count( $sidebar_widget_ids ), // place 'Add Widget' and 'Reorder' buttons at end.
475
					) );
476
					$new_setting_ids[] = $setting_id;
477
478
					$this->manager->add_control( $control );
479
				}
480
			}
481
482
			// Add a control for each active widget (located in a sidebar).
483
			foreach ( $sidebar_widget_ids as $i => $widget_id ) {
484
485
				// Skip widgets that may have gone away due to a plugin being deactivated.
486
				if ( ! $is_active_sidebar || ! isset( $wp_registered_widgets[$widget_id] ) ) {
487
					continue;
488
				}
489
490
				$registered_widget = $wp_registered_widgets[$widget_id];
491
				$setting_id        = $this->get_setting_id( $widget_id );
492
				$id_base           = $wp_registered_widget_controls[$widget_id]['id_base'];
493
494
				$control = new WP_Widget_Form_Customize_Control( $this->manager, $setting_id, array(
495
					'label'          => $registered_widget['name'],
496
					'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...
497
					'sidebar_id'     => $sidebar_id,
498
					'widget_id'      => $widget_id,
499
					'widget_id_base' => $id_base,
500
					'priority'       => $i,
501
					'width'          => $wp_registered_widget_controls[$widget_id]['width'],
502
					'height'         => $wp_registered_widget_controls[$widget_id]['height'],
503
					'is_wide'        => $this->is_wide_widget( $widget_id ),
504
				) );
505
				$this->manager->add_control( $control );
506
			}
507
		}
508
509
		if ( ! $this->manager->doing_ajax( 'customize_save' ) ) {
510
			foreach ( $new_setting_ids as $new_setting_id ) {
511
				$this->manager->get_setting( $new_setting_id )->preview();
512
			}
513
		}
514
	}
515
516
	/**
517
	 * Determines whether the widgets panel is active, based on whether there are sidebars registered.
518
	 *
519
	 * @since 4.4.0
520
	 * @access public
521
	 *
522
	 * @see WP_Customize_Panel::$active_callback
523
	 *
524
	 * @global array $wp_registered_sidebars
525
	 * @return bool Active.
526
	 */
527
	public function is_panel_active() {
528
		global $wp_registered_sidebars;
529
		return ! empty( $wp_registered_sidebars );
530
	}
531
532
	/**
533
	 * Converts a widget_id into its corresponding Customizer setting ID (option name).
534
	 *
535
	 * @since 3.9.0
536
	 * @access public
537
	 *
538
	 * @param string $widget_id Widget ID.
539
	 * @return string Maybe-parsed widget ID.
540
	 */
541
	public function get_setting_id( $widget_id ) {
542
		$parsed_widget_id = $this->parse_widget_id( $widget_id );
543
		$setting_id       = sprintf( 'widget_%s', $parsed_widget_id['id_base'] );
544
545
		if ( ! is_null( $parsed_widget_id['number'] ) ) {
546
			$setting_id .= sprintf( '[%d]', $parsed_widget_id['number'] );
547
		}
548
		return $setting_id;
549
	}
550
551
	/**
552
	 * Determines whether the widget is considered "wide".
553
	 *
554
	 * Core widgets which may have controls wider than 250, but can still be shown
555
	 * in the narrow Customizer panel. The RSS and Text widgets in Core, for example,
556
	 * have widths of 400 and yet they still render fine in the Customizer panel.
557
	 *
558
	 * This method will return all Core widgets as being not wide, but this can be
559
	 * overridden with the {@see 'is_wide_widget_in_customizer'} filter.
560
	 *
561
	 * @since 3.9.0
562
	 * @access public
563
	 *
564
	 * @global $wp_registered_widget_controls
565
	 *
566
	 * @param string $widget_id Widget ID.
567
	 * @return bool Whether or not the widget is a "wide" widget.
568
	 */
569
	public function is_wide_widget( $widget_id ) {
570
		global $wp_registered_widget_controls;
571
572
		$parsed_widget_id = $this->parse_widget_id( $widget_id );
573
		$width            = $wp_registered_widget_controls[$widget_id]['width'];
574
		$is_core          = in_array( $parsed_widget_id['id_base'], $this->core_widget_id_bases );
575
		$is_wide          = ( $width > 250 && ! $is_core );
576
577
		/**
578
		 * Filters whether the given widget is considered "wide".
579
		 *
580
		 * @since 3.9.0
581
		 *
582
		 * @param bool   $is_wide   Whether the widget is wide, Default false.
583
		 * @param string $widget_id Widget ID.
584
		 */
585
		return apply_filters( 'is_wide_widget_in_customizer', $is_wide, $widget_id );
586
	}
587
588
	/**
589
	 * Converts a widget ID into its id_base and number components.
590
	 *
591
	 * @since 3.9.0
592
	 * @access public
593
	 *
594
	 * @param string $widget_id Widget ID.
595
	 * @return array Array containing a widget's id_base and number components.
596
	 */
597
	public function parse_widget_id( $widget_id ) {
598
		$parsed = array(
599
			'number' => null,
600
			'id_base' => null,
601
		);
602
603
		if ( preg_match( '/^(.+)-(\d+)$/', $widget_id, $matches ) ) {
604
			$parsed['id_base'] = $matches[1];
605
			$parsed['number']  = intval( $matches[2] );
606
		} else {
607
			// likely an old single widget
608
			$parsed['id_base'] = $widget_id;
609
		}
610
		return $parsed;
611
	}
612
613
	/**
614
	 * Converts a widget setting ID (option path) to its id_base and number components.
615
	 *
616
	 * @since 3.9.0
617
	 * @access public
618
	 *
619
	 * @param string $setting_id Widget setting ID.
620
	 * @return WP_Error|array Array containing a widget's id_base and number components,
621
	 *                        or a WP_Error object.
622
	 */
623
	public function parse_widget_setting_id( $setting_id ) {
624
		if ( ! preg_match( '/^(widget_(.+?))(?:\[(\d+)\])?$/', $setting_id, $matches ) ) {
625
			return new WP_Error( 'widget_setting_invalid_id' );
626
		}
627
628
		$id_base = $matches[2];
629
		$number  = isset( $matches[3] ) ? intval( $matches[3] ) : null;
630
631
		return compact( 'id_base', 'number' );
632
	}
633
634
	/**
635
	 * Calls admin_print_styles-widgets.php and admin_print_styles hooks to
636
	 * allow custom styles from plugins.
637
	 *
638
	 * @since 3.9.0
639
	 * @access public
640
	 */
641
	public function print_styles() {
642
		/** This action is documented in wp-admin/admin-header.php */
643
		do_action( 'admin_print_styles-widgets.php' );
644
645
		/** This action is documented in wp-admin/admin-header.php */
646
		do_action( 'admin_print_styles' );
647
	}
648
649
	/**
650
	 * Calls admin_print_scripts-widgets.php and admin_print_scripts hooks to
651
	 * allow custom scripts from plugins.
652
	 *
653
	 * @since 3.9.0
654
	 * @access public
655
	 */
656
	public function print_scripts() {
657
		/** This action is documented in wp-admin/admin-header.php */
658
		do_action( 'admin_print_scripts-widgets.php' );
659
660
		/** This action is documented in wp-admin/admin-header.php */
661
		do_action( 'admin_print_scripts' );
662
	}
663
664
	/**
665
	 * Enqueues scripts and styles for Customizer panel and export data to JavaScript.
666
	 *
667
	 * @since 3.9.0
668
	 * @access public
669
	 *
670
	 * @global WP_Scripts $wp_scripts
671
	 * @global array $wp_registered_sidebars
672
	 * @global array $wp_registered_widgets
673
	 */
674
	public function enqueue_scripts() {
675
		global $wp_scripts, $wp_registered_sidebars, $wp_registered_widgets;
676
677
		wp_enqueue_style( 'customize-widgets' );
678
		wp_enqueue_script( 'customize-widgets' );
679
680
		/** This action is documented in wp-admin/admin-header.php */
681
		do_action( 'admin_enqueue_scripts', 'widgets.php' );
682
683
		/*
684
		 * Export available widgets with control_tpl removed from model
685
		 * since plugins need templates to be in the DOM.
686
		 */
687
		$available_widgets = array();
688
689
		foreach ( $this->get_available_widgets() as $available_widget ) {
690
			unset( $available_widget['control_tpl'] );
691
			$available_widgets[] = $available_widget;
692
		}
693
694
		$widget_reorder_nav_tpl = sprintf(
695
			'<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>',
696
			__( 'Move to another area&hellip;' ),
697
			__( 'Move down' ),
698
			__( 'Move up' )
699
		);
700
701
		$move_widget_area_tpl = str_replace(
702
			array( '{description}', '{btn}' ),
703
			array(
704
				__( 'Select an area to move this widget into:' ),
705
				_x( 'Move', 'Move widget' ),
706
			),
707
			'<div class="move-widget-area">
708
				<p class="description">{description}</p>
709
				<ul class="widget-area-select">
710
					<% _.each( sidebars, function ( sidebar ){ %>
711
						<li class="" data-id="<%- sidebar.id %>" title="<%- sidebar.description %>" tabindex="0"><%- sidebar.name %></li>
712
					<% }); %>
713
				</ul>
714
				<div class="move-widget-actions">
715
					<button class="move-widget-btn button-secondary" type="button">{btn}</button>
716
				</div>
717
			</div>'
718
		);
719
720
		$settings = array(
721
			'registeredSidebars'   => array_values( $wp_registered_sidebars ),
722
			'registeredWidgets'    => $wp_registered_widgets,
723
			'availableWidgets'     => $available_widgets, // @todo Merge this with registered_widgets
724
			'l10n' => array(
725
				'saveBtnLabel'     => __( 'Apply' ),
726
				'saveBtnTooltip'   => __( 'Save and preview changes before publishing them.' ),
727
				'removeBtnLabel'   => __( 'Remove' ),
728
				'removeBtnTooltip' => __( 'Trash widget by moving it to the inactive widgets sidebar.' ),
729
				'error'            => __( 'An error has occurred. Please reload the page and try again.' ),
730
				'widgetMovedUp'    => __( 'Widget moved up' ),
731
				'widgetMovedDown'  => __( 'Widget moved down' ),
732
				'noAreasRendered'  => __( 'There are no widget areas currently rendered in the preview. Navigate in the preview to a template that makes use of a widget area in order to access its widgets here.' ),
733
				'reorderModeOn'    => __( 'Reorder mode enabled' ),
734
				'reorderModeOff'   => __( 'Reorder mode closed' ),
735
				'reorderLabelOn'   => esc_attr__( 'Reorder widgets' ),
736
				'reorderLabelOff'  => esc_attr__( 'Close reorder mode' ),
737
			),
738
			'tpl' => array(
739
				'widgetReorderNav' => $widget_reorder_nav_tpl,
740
				'moveWidgetArea'   => $move_widget_area_tpl,
741
			),
742
			'selectiveRefreshableWidgets' => $this->get_selective_refreshable_widgets(),
743
		);
744
745
		foreach ( $settings['registeredWidgets'] as &$registered_widget ) {
746
			unset( $registered_widget['callback'] ); // may not be JSON-serializeable
747
		}
748
749
		$wp_scripts->add_data(
750
			'customize-widgets',
751
			'data',
752
			sprintf( 'var _wpCustomizeWidgetsSettings = %s;', wp_json_encode( $settings ) )
753
		);
754
	}
755
756
	/**
757
	 * Renders the widget form control templates into the DOM.
758
	 *
759
	 * @since 3.9.0
760
	 * @access public
761
	 */
762
	public function output_widget_control_templates() {
763
		?>
764
		<div id="widgets-left"><!-- compatibility with JS which looks for widget templates here -->
765
		<div id="available-widgets">
766
			<div class="customize-section-title">
767
				<button class="customize-section-back" tabindex="-1">
768
					<span class="screen-reader-text"><?php _e( 'Back' ); ?></span>
769
				</button>
770
				<h3>
771
					<span class="customize-action"><?php
772
						/* translators: &#9656; is the unicode right-pointing triangle, and %s is the section title in the Customizer */
773
						echo sprintf( __( 'Customizing &#9656; %s' ), esc_html( $this->manager->get_panel( 'widgets' )->title ) );
774
					?></span>
775
					<?php _e( 'Add a Widget' ); ?>
776
				</h3>
777
			</div>
778
			<div id="available-widgets-filter">
779
				<label class="screen-reader-text" for="widgets-search"><?php _e( 'Search Widgets' ); ?></label>
780
				<input type="search" id="widgets-search" placeholder="<?php esc_attr_e( 'Search widgets&hellip;' ) ?>" />
781
			</div>
782
			<div id="available-widgets-list">
783
			<?php foreach ( $this->get_available_widgets() as $available_widget ): ?>
784
				<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">
785
					<?php echo $available_widget['control_tpl']; ?>
786
				</div>
787
			<?php endforeach; ?>
788
			</div><!-- #available-widgets-list -->
789
		</div><!-- #available-widgets -->
790
		</div><!-- #widgets-left -->
791
		<?php
792
	}
793
794
	/**
795
	 * Calls admin_print_footer_scripts and admin_print_scripts hooks to
796
	 * allow custom scripts from plugins.
797
	 *
798
	 * @since 3.9.0
799
	 * @access public
800
	 */
801
	public function print_footer_scripts() {
802
		/** This action is documented in wp-admin/admin-footer.php */
803
		do_action( 'admin_print_footer_scripts-widgets.php' );
804
805
		/** This action is documented in wp-admin/admin-footer.php */
806
		do_action( 'admin_print_footer_scripts' );
807
808
		/** This action is documented in wp-admin/admin-footer.php */
809
		do_action( 'admin_footer-widgets.php' );
810
	}
811
812
	/**
813
	 * Retrieves common arguments to supply when constructing a Customizer setting.
814
	 *
815
	 * @since 3.9.0
816
	 * @access public
817
	 *
818
	 * @param string $id        Widget setting ID.
819
	 * @param array  $overrides Array of setting overrides.
820
	 * @return array Possibly modified setting arguments.
821
	 */
822
	public function get_setting_args( $id, $overrides = array() ) {
823
		$args = array(
824
			'type'       => 'option',
825
			'capability' => 'edit_theme_options',
826
			'default'    => array(),
827
		);
828
829
		if ( preg_match( $this->setting_id_patterns['sidebar_widgets'], $id, $matches ) ) {
830
			$args['sanitize_callback'] = array( $this, 'sanitize_sidebar_widgets' );
831
			$args['sanitize_js_callback'] = array( $this, 'sanitize_sidebar_widgets_js_instance' );
832
			$args['transport'] = current_theme_supports( 'customize-selective-refresh-widgets' ) ? 'postMessage' : 'refresh';
833
		} elseif ( preg_match( $this->setting_id_patterns['widget_instance'], $id, $matches ) ) {
834
			$args['sanitize_callback'] = array( $this, 'sanitize_widget_instance' );
835
			$args['sanitize_js_callback'] = array( $this, 'sanitize_widget_js_instance' );
836
			$args['transport'] = $this->is_widget_selective_refreshable( $matches['id_base'] ) ? 'postMessage' : 'refresh';
837
		}
838
839
		$args = array_merge( $args, $overrides );
840
841
		/**
842
		 * Filters the common arguments supplied when constructing a Customizer setting.
843
		 *
844
		 * @since 3.9.0
845
		 *
846
		 * @see WP_Customize_Setting
847
		 *
848
		 * @param array  $args Array of Customizer setting arguments.
849
		 * @param string $id   Widget setting ID.
850
		 */
851
		return apply_filters( 'widget_customizer_setting_args', $args, $id );
852
	}
853
854
	/**
855
	 * Ensures sidebar widget arrays only ever contain widget IDS.
856
	 *
857
	 * Used as the 'sanitize_callback' for each $sidebars_widgets setting.
858
	 *
859
	 * @since 3.9.0
860
	 * @access public
861
	 *
862
	 * @param array $widget_ids Array of widget IDs.
863
	 * @return array Array of sanitized widget IDs.
864
	 */
865
	public function sanitize_sidebar_widgets( $widget_ids ) {
866
		$widget_ids = array_map( 'strval', (array) $widget_ids );
867
		$sanitized_widget_ids = array();
868
		foreach ( $widget_ids as $widget_id ) {
869
			$sanitized_widget_ids[] = preg_replace( '/[^a-z0-9_\-]/', '', $widget_id );
870
		}
871
		return $sanitized_widget_ids;
872
	}
873
874
	/**
875
	 * Builds up an index of all available widgets for use in Backbone models.
876
	 *
877
	 * @since 3.9.0
878
	 * @access public
879
	 *
880
	 * @global array $wp_registered_widgets
881
	 * @global array $wp_registered_widget_controls
882
	 * @staticvar array $available_widgets
883
	 *
884
	 * @see wp_list_widgets()
885
	 *
886
	 * @return array List of available widgets.
887
	 */
888
	public function get_available_widgets() {
889
		static $available_widgets = array();
890
		if ( ! empty( $available_widgets ) ) {
891
			return $available_widgets;
892
		}
893
894
		global $wp_registered_widgets, $wp_registered_widget_controls;
895
		require_once ABSPATH . '/wp-admin/includes/widgets.php'; // for next_widget_id_number()
896
897
		$sort = $wp_registered_widgets;
898
		usort( $sort, array( $this, '_sort_name_callback' ) );
899
		$done = array();
900
901
		foreach ( $sort as $widget ) {
902
			if ( in_array( $widget['callback'], $done, true ) ) { // We already showed this multi-widget
903
				continue;
904
			}
905
906
			$sidebar = is_active_widget( $widget['callback'], $widget['id'], false, false );
907
			$done[]  = $widget['callback'];
908
909
			if ( ! isset( $widget['params'][0] ) ) {
910
				$widget['params'][0] = array();
911
			}
912
913
			$available_widget = $widget;
914
			unset( $available_widget['callback'] ); // not serializable to JSON
915
916
			$args = array(
917
				'widget_id'   => $widget['id'],
918
				'widget_name' => $widget['name'],
919
				'_display'    => 'template',
920
			);
921
922
			$is_disabled     = false;
923
			$is_multi_widget = ( isset( $wp_registered_widget_controls[$widget['id']]['id_base'] ) && isset( $widget['params'][0]['number'] ) );
924
			if ( $is_multi_widget ) {
925
				$id_base            = $wp_registered_widget_controls[$widget['id']]['id_base'];
926
				$args['_temp_id']   = "$id_base-__i__";
927
				$args['_multi_num'] = next_widget_id_number( $id_base );
928
				$args['_add']       = 'multi';
929
			} else {
930
				$args['_add'] = 'single';
931
932
				if ( $sidebar && 'wp_inactive_widgets' !== $sidebar ) {
933
					$is_disabled = true;
934
				}
935
				$id_base = $widget['id'];
936
			}
937
938
			$list_widget_controls_args = wp_list_widget_controls_dynamic_sidebar( array( 0 => $args, 1 => $widget['params'][0] ) );
939
			$control_tpl = $this->get_widget_control( $list_widget_controls_args );
940
941
			// The properties here are mapped to the Backbone Widget model.
942
			$available_widget = array_merge( $available_widget, array(
943
				'temp_id'      => isset( $args['_temp_id'] ) ? $args['_temp_id'] : null,
944
				'is_multi'     => $is_multi_widget,
945
				'control_tpl'  => $control_tpl,
946
				'multi_number' => ( $args['_add'] === 'multi' ) ? $args['_multi_num'] : false,
947
				'is_disabled'  => $is_disabled,
948
				'id_base'      => $id_base,
949
				'transport'    => $this->is_widget_selective_refreshable( $id_base ) ? 'postMessage' : 'refresh',
950
				'width'        => $wp_registered_widget_controls[$widget['id']]['width'],
951
				'height'       => $wp_registered_widget_controls[$widget['id']]['height'],
952
				'is_wide'      => $this->is_wide_widget( $widget['id'] ),
953
			) );
954
955
			$available_widgets[] = $available_widget;
956
		}
957
958
		return $available_widgets;
959
	}
960
961
	/**
962
	 * Naturally orders available widgets by name.
963
	 *
964
	 * @since 3.9.0
965
	 * @access protected
966
	 *
967
	 * @param array $widget_a The first widget to compare.
968
	 * @param array $widget_b The second widget to compare.
969
	 * @return int Reorder position for the current widget comparison.
970
	 */
971
	protected function _sort_name_callback( $widget_a, $widget_b ) {
972
		return strnatcasecmp( $widget_a['name'], $widget_b['name'] );
973
	}
974
975
	/**
976
	 * Retrieves the widget control markup.
977
	 *
978
	 * @since 3.9.0
979
	 * @access public
980
	 *
981
	 * @param array $args Widget control arguments.
982
	 * @return string Widget control form HTML markup.
983
	 */
984
	public function get_widget_control( $args ) {
985
		$args[0]['before_form'] = '<div class="form">';
986
		$args[0]['after_form'] = '</div><!-- .form -->';
987
		$args[0]['before_widget_content'] = '<div class="widget-content">';
988
		$args[0]['after_widget_content'] = '</div><!-- .widget-content -->';
989
		ob_start();
990
		call_user_func_array( 'wp_widget_control', $args );
991
		$control_tpl = ob_get_clean();
992
		return $control_tpl;
993
	}
994
995
	/**
996
	 * Retrieves the widget control markup parts.
997
	 *
998
	 * @since 4.4.0
999
	 * @access public
1000
	 *
1001
	 * @param array $args Widget control arguments.
1002
	 * @return array {
1003
	 *     @type string $control Markup for widget control wrapping form.
1004
	 *     @type string $content The contents of the widget form itself.
1005
	 * }
1006
	 */
1007
	public function get_widget_control_parts( $args ) {
1008
		$args[0]['before_widget_content'] = '<div class="widget-content">';
1009
		$args[0]['after_widget_content'] = '</div><!-- .widget-content -->';
1010
		$control_markup = $this->get_widget_control( $args );
1011
1012
		$content_start_pos = strpos( $control_markup, $args[0]['before_widget_content'] );
1013
		$content_end_pos = strrpos( $control_markup, $args[0]['after_widget_content'] );
1014
1015
		$control = substr( $control_markup, 0, $content_start_pos + strlen( $args[0]['before_widget_content'] ) );
1016
		$control .= substr( $control_markup, $content_end_pos );
1017
		$content = trim( substr(
1018
			$control_markup,
1019
			$content_start_pos + strlen( $args[0]['before_widget_content'] ),
1020
			$content_end_pos - $content_start_pos - strlen( $args[0]['before_widget_content'] )
1021
		) );
1022
1023
		return compact( 'control', 'content' );
1024
	}
1025
1026
	/**
1027
	 * Adds hooks for the Customizer preview.
1028
	 *
1029
	 * @since 3.9.0
1030
	 * @access public
1031
	 */
1032
	public function customize_preview_init() {
1033
		add_action( 'wp_enqueue_scripts', array( $this, 'customize_preview_enqueue' ) );
1034
		add_action( 'wp_print_styles',    array( $this, 'print_preview_css' ), 1 );
1035
		add_action( 'wp_footer',          array( $this, 'export_preview_data' ), 20 );
1036
	}
1037
1038
	/**
1039
	 * Refreshes the nonce for widget updates.
1040
	 *
1041
	 * @since 4.2.0
1042
	 * @access public
1043
	 *
1044
	 * @param  array $nonces Array of nonces.
1045
	 * @return array $nonces Array of nonces.
1046
	 */
1047
	public function refresh_nonces( $nonces ) {
1048
		$nonces['update-widget'] = wp_create_nonce( 'update-widget' );
1049
		return $nonces;
1050
	}
1051
1052
	/**
1053
	 * When previewing, ensures the proper previewing widgets are used.
1054
	 *
1055
	 * Because wp_get_sidebars_widgets() gets called early at {@see 'init' } (via
1056
	 * wp_convert_widget_settings()) and can set global variable `$_wp_sidebars_widgets`
1057
	 * to the value of `get_option( 'sidebars_widgets' )` before the Customizer preview
1058
	 * filter is added, it has to be reset after the filter has been added.
1059
	 *
1060
	 * @since 3.9.0
1061
	 * @access public
1062
	 *
1063
	 * @param array $sidebars_widgets List of widgets for the current sidebar.
1064
	 * @return array
1065
	 */
1066
	public function preview_sidebars_widgets( $sidebars_widgets ) {
1067
		$sidebars_widgets = get_option( 'sidebars_widgets', array() );
1068
1069
		unset( $sidebars_widgets['array_version'] );
1070
		return $sidebars_widgets;
1071
	}
1072
1073
	/**
1074
	 * Enqueues scripts for the Customizer preview.
1075
	 *
1076
	 * @since 3.9.0
1077
	 * @access public
1078
	 */
1079
	public function customize_preview_enqueue() {
1080
		wp_enqueue_script( 'customize-preview-widgets' );
1081
		wp_enqueue_style( 'customize-preview' );
1082
	}
1083
1084
	/**
1085
	 * Inserts default style for highlighted widget at early point so theme
1086
	 * stylesheet can override.
1087
	 *
1088
	 * @since 3.9.0
1089
	 * @access public
1090
	 */
1091
	public function print_preview_css() {
1092
		?>
1093
		<style>
1094
		.widget-customizer-highlighted-widget {
1095
			outline: none;
1096
			-webkit-box-shadow: 0 0 2px rgba(30,140,190,0.8);
1097
			box-shadow: 0 0 2px rgba(30,140,190,0.8);
1098
			position: relative;
1099
			z-index: 1;
1100
		}
1101
		</style>
1102
		<?php
1103
	}
1104
1105
	/**
1106
	 * Communicates the sidebars that appeared on the page at the very end of the page,
1107
	 * and at the very end of the wp_footer,
1108
	 *
1109
	 * @since 3.9.0
1110
	 * @access public
1111
     *
1112
	 * @global array $wp_registered_sidebars
1113
	 * @global array $wp_registered_widgets
1114
	 */
1115
	public function export_preview_data() {
1116
		global $wp_registered_sidebars, $wp_registered_widgets;
1117
1118
		// Prepare Customizer settings to pass to JavaScript.
1119
		$settings = array(
1120
			'renderedSidebars'   => array_fill_keys( array_unique( $this->rendered_sidebars ), true ),
1121
			'renderedWidgets'    => array_fill_keys( array_keys( $this->rendered_widgets ), true ),
1122
			'registeredSidebars' => array_values( $wp_registered_sidebars ),
1123
			'registeredWidgets'  => $wp_registered_widgets,
1124
			'l10n'               => array(
1125
				'widgetTooltip'  => __( 'Shift-click to edit this widget.' ),
1126
			),
1127
			'selectiveRefreshableWidgets' => $this->get_selective_refreshable_widgets(),
1128
		);
1129
		foreach ( $settings['registeredWidgets'] as &$registered_widget ) {
1130
			unset( $registered_widget['callback'] ); // may not be JSON-serializeable
1131
		}
1132
1133
		?>
1134
		<script type="text/javascript">
1135
			var _wpWidgetCustomizerPreviewSettings = <?php echo wp_json_encode( $settings ); ?>;
1136
		</script>
1137
		<?php
1138
	}
1139
1140
	/**
1141
	 * Tracks the widgets that were rendered.
1142
	 *
1143
	 * @since 3.9.0
1144
	 * @access public
1145
	 *
1146
	 * @param array $widget Rendered widget to tally.
1147
	 */
1148
	public function tally_rendered_widgets( $widget ) {
1149
		$this->rendered_widgets[ $widget['id'] ] = true;
1150
	}
1151
1152
	/**
1153
	 * Determine if a widget is rendered on the page.
1154
	 *
1155
	 * @since 4.0.0
1156
	 * @access public
1157
	 *
1158
	 * @param string $widget_id Widget ID to check.
1159
	 * @return bool Whether the widget is rendered.
1160
	 */
1161
	public function is_widget_rendered( $widget_id ) {
1162
		return in_array( $widget_id, $this->rendered_widgets );
1163
	}
1164
1165
	/**
1166
	 * Determines if a sidebar is rendered on the page.
1167
	 *
1168
	 * @since 4.0.0
1169
	 * @access public
1170
	 *
1171
	 * @param string $sidebar_id Sidebar ID to check.
1172
	 * @return bool Whether the sidebar is rendered.
1173
	 */
1174
	public function is_sidebar_rendered( $sidebar_id ) {
1175
		return in_array( $sidebar_id, $this->rendered_sidebars );
1176
	}
1177
1178
	/**
1179
	 * Tallies the sidebars rendered via is_active_sidebar().
1180
	 *
1181
	 * Keep track of the times that is_active_sidebar() is called in the template,
1182
	 * and assume that this means that the sidebar would be rendered on the template
1183
	 * if there were widgets populating it.
1184
	 *
1185
	 * @since 3.9.0
1186
	 * @access public
1187
	 *
1188
	 * @param bool   $is_active  Whether the sidebar is active.
1189
	 * @param string $sidebar_id Sidebar ID.
1190
	 * @return bool Whether the sidebar is active.
1191
	 */
1192
	public function tally_sidebars_via_is_active_sidebar_calls( $is_active, $sidebar_id ) {
1193
		if ( is_registered_sidebar( $sidebar_id ) ) {
1194
			$this->rendered_sidebars[] = $sidebar_id;
1195
		}
1196
		/*
1197
		 * We may need to force this to true, and also force-true the value
1198
		 * for 'dynamic_sidebar_has_widgets' if we want to ensure that there
1199
		 * is an area to drop widgets into, if the sidebar is empty.
1200
		 */
1201
		return $is_active;
1202
	}
1203
1204
	/**
1205
	 * Tallies the sidebars rendered via dynamic_sidebar().
1206
	 *
1207
	 * Keep track of the times that dynamic_sidebar() is called in the template,
1208
	 * and assume this means the sidebar would be rendered on the template if
1209
	 * there were widgets populating it.
1210
	 *
1211
	 * @since 3.9.0
1212
	 * @access public
1213
	 *
1214
	 * @param bool   $has_widgets Whether the current sidebar has widgets.
1215
	 * @param string $sidebar_id  Sidebar ID.
1216
	 * @return bool Whether the current sidebar has widgets.
1217
	 */
1218
	public function tally_sidebars_via_dynamic_sidebar_calls( $has_widgets, $sidebar_id ) {
1219
		if ( is_registered_sidebar( $sidebar_id ) ) {
1220
			$this->rendered_sidebars[] = $sidebar_id;
1221
		}
1222
1223
		/*
1224
		 * We may need to force this to true, and also force-true the value
1225
		 * for 'is_active_sidebar' if we want to ensure there is an area to
1226
		 * drop widgets into, if the sidebar is empty.
1227
		 */
1228
		return $has_widgets;
1229
	}
1230
1231
	/**
1232
	 * Retrieves MAC for a serialized widget instance string.
1233
	 *
1234
	 * Allows values posted back from JS to be rejected if any tampering of the
1235
	 * data has occurred.
1236
	 *
1237
	 * @since 3.9.0
1238
	 * @access protected
1239
	 *
1240
	 * @param string $serialized_instance Widget instance.
1241
	 * @return string MAC for serialized widget instance.
1242
	 */
1243
	protected function get_instance_hash_key( $serialized_instance ) {
1244
		return wp_hash( $serialized_instance );
1245
	}
1246
1247
	/**
1248
	 * Sanitizes a widget instance.
1249
	 *
1250
	 * Unserialize the JS-instance for storing in the options. It's important that this filter
1251
	 * only get applied to an instance *once*.
1252
	 *
1253
	 * @since 3.9.0
1254
	 * @access public
1255
	 *
1256
	 * @param array $value Widget instance to sanitize.
1257
	 * @return array|void Sanitized widget instance.
1258
	 */
1259
	public function sanitize_widget_instance( $value ) {
1260
		if ( $value === array() ) {
1261
			return $value;
1262
		}
1263
1264
		if ( empty( $value['is_widget_customizer_js_value'] )
1265
			|| empty( $value['instance_hash_key'] )
1266
			|| empty( $value['encoded_serialized_instance'] ) )
1267
		{
1268
			return;
1269
		}
1270
1271
		$decoded = base64_decode( $value['encoded_serialized_instance'], true );
1272
		if ( false === $decoded ) {
1273
			return;
1274
		}
1275
1276
		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...
1277
			return;
1278
		}
1279
1280
		$instance = unserialize( $decoded );
1281
		if ( false === $instance ) {
1282
			return;
1283
		}
1284
1285
		return $instance;
1286
	}
1287
1288
	/**
1289
	 * Converts a widget instance into JSON-representable format.
1290
	 *
1291
	 * @since 3.9.0
1292
	 * @access public
1293
	 *
1294
	 * @param array $value Widget instance to convert to JSON.
1295
	 * @return array JSON-converted widget instance.
1296
	 */
1297
	public function sanitize_widget_js_instance( $value ) {
1298
		if ( empty( $value['is_widget_customizer_js_value'] ) ) {
1299
			$serialized = serialize( $value );
1300
1301
			$value = array(
1302
				'encoded_serialized_instance'   => base64_encode( $serialized ),
1303
				'title'                         => empty( $value['title'] ) ? '' : $value['title'],
1304
				'is_widget_customizer_js_value' => true,
1305
				'instance_hash_key'             => $this->get_instance_hash_key( $serialized ),
1306
			);
1307
		}
1308
		return $value;
1309
	}
1310
1311
	/**
1312
	 * Strips out widget IDs for widgets which are no longer registered.
1313
	 *
1314
	 * One example where this might happen is when a plugin orphans a widget
1315
	 * in a sidebar upon deactivation.
1316
	 *
1317
	 * @since 3.9.0
1318
	 * @access public
1319
	 *
1320
	 * @global array $wp_registered_widgets
1321
	 *
1322
	 * @param array $widget_ids List of widget IDs.
1323
	 * @return array Parsed list of widget IDs.
1324
	 */
1325
	public function sanitize_sidebar_widgets_js_instance( $widget_ids ) {
1326
		global $wp_registered_widgets;
1327
		$widget_ids = array_values( array_intersect( $widget_ids, array_keys( $wp_registered_widgets ) ) );
1328
		return $widget_ids;
1329
	}
1330
1331
	/**
1332
	 * Finds and invokes the widget update and control callbacks.
1333
	 *
1334
	 * Requires that `$_POST` be populated with the instance data.
1335
	 *
1336
	 * @since 3.9.0
1337
	 * @access public
1338
	 *
1339
	 * @global array $wp_registered_widget_updates
1340
	 * @global array $wp_registered_widget_controls
1341
	 *
1342
	 * @param  string $widget_id Widget ID.
1343
	 * @return WP_Error|array Array containing the updated widget information.
1344
	 *                        A WP_Error object, otherwise.
1345
	 */
1346
	public function call_widget_update( $widget_id ) {
1347
		global $wp_registered_widget_updates, $wp_registered_widget_controls;
1348
1349
		$setting_id = $this->get_setting_id( $widget_id );
1350
1351
		/*
1352
		 * Make sure that other setting changes have previewed since this widget
1353
		 * may depend on them (e.g. Menus being present for Custom Menu widget).
1354
		 */
1355
		if ( ! did_action( 'customize_preview_init' ) ) {
1356
			foreach ( $this->manager->settings() as $setting ) {
1357
				if ( $setting->id !== $setting_id ) {
1358
					$setting->preview();
1359
				}
1360
			}
1361
		}
1362
1363
		$this->start_capturing_option_updates();
1364
		$parsed_id   = $this->parse_widget_id( $widget_id );
1365
		$option_name = 'widget_' . $parsed_id['id_base'];
1366
1367
		/*
1368
		 * If a previously-sanitized instance is provided, populate the input vars
1369
		 * with its values so that the widget update callback will read this instance
1370
		 */
1371
		$added_input_vars = array();
1372
		if ( ! empty( $_POST['sanitized_widget_setting'] ) ) {
1373
			$sanitized_widget_setting = json_decode( $this->get_post_value( 'sanitized_widget_setting' ), true );
1374
			if ( false === $sanitized_widget_setting ) {
1375
				$this->stop_capturing_option_updates();
1376
				return new WP_Error( 'widget_setting_malformed' );
1377
			}
1378
1379
			$instance = $this->sanitize_widget_instance( $sanitized_widget_setting );
0 ignored issues
show
It seems like $sanitized_widget_setting defined by json_decode($this->get_p...widget_setting'), true) on line 1373 can also be of type object; however, WP_Customize_Widgets::sanitize_widget_instance() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1380
			if ( is_null( $instance ) ) {
1381
				$this->stop_capturing_option_updates();
1382
				return new WP_Error( 'widget_setting_unsanitized' );
1383
			}
1384
1385
			if ( ! is_null( $parsed_id['number'] ) ) {
1386
				$value = array();
1387
				$value[$parsed_id['number']] = $instance;
1388
				$key = 'widget-' . $parsed_id['id_base'];
1389
				$_REQUEST[$key] = $_POST[$key] = wp_slash( $value );
1390
				$added_input_vars[] = $key;
1391
			} else {
1392
				foreach ( $instance as $key => $value ) {
1393
					$_REQUEST[$key] = $_POST[$key] = wp_slash( $value );
1394
					$added_input_vars[] = $key;
1395
				}
1396
			}
1397
		}
1398
1399
		// Invoke the widget update callback.
1400 View Code Duplication
		foreach ( (array) $wp_registered_widget_updates as $name => $control ) {
1401
			if ( $name === $parsed_id['id_base'] && is_callable( $control['callback'] ) ) {
1402
				ob_start();
1403
				call_user_func_array( $control['callback'], $control['params'] );
1404
				ob_end_clean();
1405
				break;
1406
			}
1407
		}
1408
1409
		// Clean up any input vars that were manually added
1410
		foreach ( $added_input_vars as $key ) {
1411
			unset( $_POST[ $key ] );
1412
			unset( $_REQUEST[ $key ] );
1413
		}
1414
1415
		// Make sure the expected option was updated.
1416
		if ( 0 !== $this->count_captured_options() ) {
1417
			if ( $this->count_captured_options() > 1 ) {
1418
				$this->stop_capturing_option_updates();
1419
				return new WP_Error( 'widget_setting_too_many_options' );
1420
			}
1421
1422
			$updated_option_name = key( $this->get_captured_options() );
1423
			if ( $updated_option_name !== $option_name ) {
1424
				$this->stop_capturing_option_updates();
1425
				return new WP_Error( 'widget_setting_unexpected_option' );
1426
			}
1427
		}
1428
1429
		// Obtain the widget instance.
1430
		$option = $this->get_captured_option( $option_name );
1431
		if ( null !== $parsed_id['number'] ) {
1432
			$instance = $option[ $parsed_id['number'] ];
1433
		} else {
1434
			$instance = $option;
1435
		}
1436
1437
		/*
1438
		 * Override the incoming $_POST['customized'] for a newly-created widget's
1439
		 * setting with the new $instance so that the preview filter currently
1440
		 * in place from WP_Customize_Setting::preview() will use this value
1441
		 * instead of the default widget instance value (an empty array).
1442
		 */
1443
		$this->manager->set_post_value( $setting_id, $this->sanitize_widget_js_instance( $instance ) );
1444
1445
		// Obtain the widget control with the updated instance in place.
1446
		ob_start();
1447
		$form = $wp_registered_widget_controls[ $widget_id ];
1448
		if ( $form ) {
1449
			call_user_func_array( $form['callback'], $form['params'] );
1450
		}
1451
		$form = ob_get_clean();
1452
1453
		$this->stop_capturing_option_updates();
1454
1455
		return compact( 'instance', 'form' );
1456
	}
1457
1458
	/**
1459
	 * Updates widget settings asynchronously.
1460
	 *
1461
	 * Allows the Customizer to update a widget using its form, but return the new
1462
	 * instance info via Ajax instead of saving it to the options table.
1463
	 *
1464
	 * Most code here copied from wp_ajax_save_widget().
1465
	 *
1466
	 * @since 3.9.0
1467
	 * @access public
1468
	 *
1469
	 * @see wp_ajax_save_widget()
1470
	 */
1471
	public function wp_ajax_update_widget() {
1472
1473
		if ( ! is_user_logged_in() ) {
1474
			wp_die( 0 );
1475
		}
1476
1477
		check_ajax_referer( 'update-widget', 'nonce' );
1478
1479
		if ( ! current_user_can( 'edit_theme_options' ) ) {
1480
			wp_die( -1 );
1481
		}
1482
1483
		if ( empty( $_POST['widget-id'] ) ) {
1484
			wp_send_json_error( 'missing_widget-id' );
1485
		}
1486
1487
		/** This action is documented in wp-admin/includes/ajax-actions.php */
1488
		do_action( 'load-widgets.php' );
1489
1490
		/** This action is documented in wp-admin/includes/ajax-actions.php */
1491
		do_action( 'widgets.php' );
1492
1493
		/** This action is documented in wp-admin/widgets.php */
1494
		do_action( 'sidebar_admin_setup' );
1495
1496
		$widget_id = $this->get_post_value( 'widget-id' );
1497
		$parsed_id = $this->parse_widget_id( $widget_id );
1498
		$id_base = $parsed_id['id_base'];
1499
1500
		$is_updating_widget_template = (
1501
			isset( $_POST[ 'widget-' . $id_base ] )
1502
			&&
1503
			is_array( $_POST[ 'widget-' . $id_base ] )
1504
			&&
1505
			preg_match( '/__i__|%i%/', key( $_POST[ 'widget-' . $id_base ] ) )
1506
		);
1507
		if ( $is_updating_widget_template ) {
1508
			wp_send_json_error( 'template_widget_not_updatable' );
1509
		}
1510
1511
		$updated_widget = $this->call_widget_update( $widget_id ); // => {instance,form}
1512
		if ( is_wp_error( $updated_widget ) ) {
1513
			wp_send_json_error( $updated_widget->get_error_code() );
1514
		}
1515
1516
		$form = $updated_widget['form'];
1517
		$instance = $this->sanitize_widget_js_instance( $updated_widget['instance'] );
1518
1519
		wp_send_json_success( compact( 'form', 'instance' ) );
1520
	}
1521
1522
	/*
1523
	 * Selective Refresh Methods
1524
	 */
1525
1526
	/**
1527
	 * Filters arguments for dynamic widget partials.
1528
	 *
1529
	 * @since 4.5.0
1530
	 * @access public
1531
	 *
1532
	 * @param array|false $partial_args Partial arguments.
1533
	 * @param string      $partial_id   Partial ID.
1534
	 * @return array (Maybe) modified partial arguments.
1535
	 */
1536
	public function customize_dynamic_partial_args( $partial_args, $partial_id ) {
1537
		if ( ! current_theme_supports( 'customize-selective-refresh-widgets' ) ) {
1538
			return $partial_args;
1539
		}
1540
1541 View Code Duplication
		if ( preg_match( '/^widget\[(?P<widget_id>.+)\]$/', $partial_id, $matches ) ) {
1542
			if ( false === $partial_args ) {
1543
				$partial_args = array();
1544
			}
1545
			$partial_args = array_merge(
1546
				$partial_args,
1547
				array(
1548
					'type'                => 'widget',
1549
					'render_callback'     => array( $this, 'render_widget_partial' ),
1550
					'container_inclusive' => true,
1551
					'settings'            => array( $this->get_setting_id( $matches['widget_id'] ) ),
1552
					'capability'          => 'edit_theme_options',
1553
				)
1554
			);
1555
		}
1556
1557
		return $partial_args;
1558
	}
1559
1560
	/**
1561
	 * Adds hooks for selective refresh.
1562
	 *
1563
	 * @since 4.5.0
1564
	 * @access public
1565
	 */
1566
	public function selective_refresh_init() {
1567
		if ( ! current_theme_supports( 'customize-selective-refresh-widgets' ) ) {
1568
			return;
1569
		}
1570
		add_filter( 'dynamic_sidebar_params', array( $this, 'filter_dynamic_sidebar_params' ) );
1571
		add_filter( 'wp_kses_allowed_html', array( $this, 'filter_wp_kses_allowed_data_attributes' ) );
1572
		add_action( 'dynamic_sidebar_before', array( $this, 'start_dynamic_sidebar' ) );
1573
		add_action( 'dynamic_sidebar_after', array( $this, 'end_dynamic_sidebar' ) );
1574
	}
1575
1576
	/**
1577
	 * Inject selective refresh data attributes into widget container elements.
1578
	 *
1579
	 * @param array $params {
1580
	 *     Dynamic sidebar params.
1581
	 *
1582
	 *     @type array $args        Sidebar args.
1583
	 *     @type array $widget_args Widget args.
1584
	 * }
1585
	 * @see WP_Customize_Nav_Menus_Partial_Refresh::filter_wp_nav_menu_args()
1586
	 *
1587
	 * @return array Params.
1588
	 */
1589
	public function filter_dynamic_sidebar_params( $params ) {
1590
		$sidebar_args = array_merge(
1591
			array(
1592
				'before_widget' => '',
1593
				'after_widget' => '',
1594
			),
1595
			$params[0]
1596
		);
1597
1598
		// Skip widgets not in a registered sidebar or ones which lack a proper wrapper element to attach the data-* attributes to.
1599
		$matches = array();
1600
		$is_valid = (
1601
			isset( $sidebar_args['id'] )
1602
			&&
1603
			is_registered_sidebar( $sidebar_args['id'] )
1604
			&&
1605
			( isset( $this->current_dynamic_sidebar_id_stack[0] ) && $this->current_dynamic_sidebar_id_stack[0] === $sidebar_args['id'] )
1606
			&&
1607
			preg_match( '#^<(?P<tag_name>\w+)#', $sidebar_args['before_widget'], $matches )
1608
		);
1609
		if ( ! $is_valid ) {
1610
			return $params;
1611
		}
1612
		$this->before_widget_tags_seen[ $matches['tag_name'] ] = true;
1613
1614
		$context = array(
1615
			'sidebar_id' => $sidebar_args['id'],
1616
		);
1617
		if ( isset( $this->context_sidebar_instance_number ) ) {
1618
			$context['sidebar_instance_number'] = $this->context_sidebar_instance_number;
1619
		} else if ( isset( $sidebar_args['id'] ) && isset( $this->sidebar_instance_count[ $sidebar_args['id'] ] ) ) {
1620
			$context['sidebar_instance_number'] = $this->sidebar_instance_count[ $sidebar_args['id'] ];
1621
		}
1622
1623
		$attributes = sprintf( ' data-customize-partial-id="%s"', esc_attr( 'widget[' . $sidebar_args['widget_id'] . ']' ) );
1624
		$attributes .= ' data-customize-partial-type="widget"';
1625
		$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...
1626
		$attributes .= sprintf( ' data-customize-widget-id="%s"', esc_attr( $sidebar_args['widget_id'] ) );
1627
		$sidebar_args['before_widget'] = preg_replace( '#^(<\w+)#', '$1 ' . $attributes, $sidebar_args['before_widget'] );
1628
1629
		$params[0] = $sidebar_args;
1630
		return $params;
1631
	}
1632
1633
	/**
1634
	 * List of the tag names seen for before_widget strings.
1635
	 *
1636
	 * This is used in the {@see 'filter_wp_kses_allowed_html'} filter to ensure that the
1637
	 * data-* attributes can be whitelisted.
1638
	 *
1639
	 * @since 4.5.0
1640
	 * @access protected
1641
	 * @var array
1642
	 */
1643
	protected $before_widget_tags_seen = array();
1644
1645
	/**
1646
	 * Ensures the HTML data-* attributes for selective refresh are allowed by kses.
1647
	 *
1648
	 * This is needed in case the `$before_widget` is run through wp_kses() when printed.
1649
	 *
1650
	 * @since 4.5.0
1651
	 * @access public
1652
	 *
1653
	 * @param array $allowed_html Allowed HTML.
1654
	 * @return array (Maybe) modified allowed HTML.
1655
	 */
1656
	public function filter_wp_kses_allowed_data_attributes( $allowed_html ) {
1657
		foreach ( array_keys( $this->before_widget_tags_seen ) as $tag_name ) {
1658
			if ( ! isset( $allowed_html[ $tag_name ] ) ) {
1659
				$allowed_html[ $tag_name ] = array();
1660
			}
1661
			$allowed_html[ $tag_name ] = array_merge(
1662
				$allowed_html[ $tag_name ],
1663
				array_fill_keys( array(
1664
					'data-customize-partial-id',
1665
					'data-customize-partial-type',
1666
					'data-customize-partial-placement-context',
1667
					'data-customize-partial-widget-id',
1668
					'data-customize-partial-options',
1669
				), true )
1670
			);
1671
		}
1672
		return $allowed_html;
1673
	}
1674
1675
	/**
1676
	 * Keep track of the number of times that dynamic_sidebar() was called for a given sidebar index.
1677
	 *
1678
	 * This helps facilitate the uncommon scenario where a single sidebar is rendered multiple times on a template.
1679
	 *
1680
	 * @since 4.5.0
1681
	 * @access protected
1682
	 * @var array
1683
	 */
1684
	protected $sidebar_instance_count = array();
1685
1686
	/**
1687
	 * The current request's sidebar_instance_number context.
1688
	 *
1689
	 * @since 4.5.0
1690
	 * @access protected
1691
	 * @var int
1692
	 */
1693
	protected $context_sidebar_instance_number;
1694
1695
	/**
1696
	 * Current sidebar ID being rendered.
1697
	 *
1698
	 * @since 4.5.0
1699
	 * @access protected
1700
	 * @var array
1701
	 */
1702
	protected $current_dynamic_sidebar_id_stack = array();
1703
1704
	/**
1705
	 * Begins keeping track of the current sidebar being rendered.
1706
	 *
1707
	 * Insert marker before widgets are rendered in a dynamic sidebar.
1708
	 *
1709
	 * @since 4.5.0
1710
	 * @access public
1711
	 *
1712
	 * @param int|string $index Index, name, or ID of the dynamic sidebar.
1713
	 */
1714
	public function start_dynamic_sidebar( $index ) {
1715
		array_unshift( $this->current_dynamic_sidebar_id_stack, $index );
1716
		if ( ! isset( $this->sidebar_instance_count[ $index ] ) ) {
1717
			$this->sidebar_instance_count[ $index ] = 0;
1718
		}
1719
		$this->sidebar_instance_count[ $index ] += 1;
1720 View Code Duplication
		if ( ! $this->manager->selective_refresh->is_render_partials_request() ) {
1721
			printf( "\n<!--dynamic_sidebar_before:%s:%d-->\n", esc_html( $index ), intval( $this->sidebar_instance_count[ $index ] ) );
1722
		}
1723
	}
1724
1725
	/**
1726
	 * Finishes keeping track of the current sidebar being rendered.
1727
	 *
1728
	 * Inserts a marker after widgets are rendered in a dynamic sidebar.
1729
	 *
1730
	 * @since 4.5.0
1731
	 * @access public
1732
	 *
1733
	 * @param int|string $index Index, name, or ID of the dynamic sidebar.
1734
	 */
1735
	public function end_dynamic_sidebar( $index ) {
1736
		array_shift( $this->current_dynamic_sidebar_id_stack );
1737 View Code Duplication
		if ( ! $this->manager->selective_refresh->is_render_partials_request() ) {
1738
			printf( "\n<!--dynamic_sidebar_after:%s:%d-->\n", esc_html( $index ), intval( $this->sidebar_instance_count[ $index ] ) );
1739
		}
1740
	}
1741
1742
	/**
1743
	 * Current sidebar being rendered.
1744
	 *
1745
	 * @since 4.5.0
1746
	 * @access protected
1747
	 * @var string
1748
	 */
1749
	protected $rendering_widget_id;
1750
1751
	/**
1752
	 * Current widget being rendered.
1753
	 *
1754
	 * @since 4.5.0
1755
	 * @access protected
1756
	 * @var string
1757
	 */
1758
	protected $rendering_sidebar_id;
1759
1760
	/**
1761
	 * Filters sidebars_widgets to ensure the currently-rendered widget is the only widget in the current sidebar.
1762
	 *
1763
	 * @since 4.5.0
1764
	 * @access protected
1765
	 *
1766
	 * @param array $sidebars_widgets Sidebars widgets.
1767
	 * @return array Filtered sidebars widgets.
1768
	 */
1769
	public function filter_sidebars_widgets_for_rendering_widget( $sidebars_widgets ) {
1770
		$sidebars_widgets[ $this->rendering_sidebar_id ] = array( $this->rendering_widget_id );
1771
		return $sidebars_widgets;
1772
	}
1773
1774
	/**
1775
	 * Renders a specific widget using the supplied sidebar arguments.
1776
	 *
1777
	 * @since 4.5.0
1778
	 * @access public
1779
	 *
1780
	 * @see dynamic_sidebar()
1781
	 *
1782
	 * @param WP_Customize_Partial $partial Partial.
1783
	 * @param array                $context {
1784
	 *     Sidebar args supplied as container context.
1785
	 *
1786
	 *     @type string $sidebar_id              ID for sidebar for widget to render into.
1787
	 *     @type int    $sidebar_instance_number Disambiguating instance number.
1788
	 * }
1789
	 * @return string|false
1790
	 */
1791
	public function render_widget_partial( $partial, $context ) {
1792
		$id_data   = $partial->id_data();
1793
		$widget_id = array_shift( $id_data['keys'] );
1794
1795
		if ( ! is_array( $context )
1796
			|| empty( $context['sidebar_id'] )
1797
			|| ! is_registered_sidebar( $context['sidebar_id'] )
1798
		) {
1799
			return false;
1800
		}
1801
1802
		$this->rendering_sidebar_id = $context['sidebar_id'];
1803
1804
		if ( isset( $context['sidebar_instance_number'] ) ) {
1805
			$this->context_sidebar_instance_number = intval( $context['sidebar_instance_number'] );
1806
		}
1807
1808
		// Filter sidebars_widgets so that only the queried widget is in the sidebar.
1809
		$this->rendering_widget_id = $widget_id;
1810
1811
		$filter_callback = array( $this, 'filter_sidebars_widgets_for_rendering_widget' );
1812
		add_filter( 'sidebars_widgets', $filter_callback, 1000 );
1813
1814
		// Render the widget.
1815
		ob_start();
1816
		dynamic_sidebar( $this->rendering_sidebar_id = $context['sidebar_id'] );
1817
		$container = ob_get_clean();
1818
1819
		// Reset variables for next partial render.
1820
		remove_filter( 'sidebars_widgets', $filter_callback, 1000 );
1821
1822
		$this->context_sidebar_instance_number = null;
1823
		$this->rendering_sidebar_id = null;
1824
		$this->rendering_widget_id = null;
1825
1826
		return $container;
1827
	}
1828
1829
	//
1830
	// Option Update Capturing
1831
	//
1832
1833
	/**
1834
	 * List of captured widget option updates.
1835
	 *
1836
	 * @since 3.9.0
1837
	 * @access protected
1838
	 * @var array $_captured_options Values updated while option capture is happening.
1839
	 */
1840
	protected $_captured_options = array();
1841
1842
	/**
1843
	 * Whether option capture is currently happening.
1844
	 *
1845
	 * @since 3.9.0
1846
	 * @access protected
1847
	 * @var bool $_is_current Whether option capture is currently happening or not.
1848
	 */
1849
	protected $_is_capturing_option_updates = false;
1850
1851
	/**
1852
	 * Determines whether the captured option update should be ignored.
1853
	 *
1854
	 * @since 3.9.0
1855
	 * @access protected
1856
	 *
1857
	 * @param string $option_name Option name.
1858
	 * @return bool Whether the option capture is ignored.
1859
	 */
1860
	protected function is_option_capture_ignored( $option_name ) {
1861
		return ( 0 === strpos( $option_name, '_transient_' ) );
1862
	}
1863
1864
	/**
1865
	 * Retrieves captured widget option updates.
1866
	 *
1867
	 * @since 3.9.0
1868
	 * @access protected
1869
	 *
1870
	 * @return array Array of captured options.
1871
	 */
1872
	protected function get_captured_options() {
1873
		return $this->_captured_options;
1874
	}
1875
1876
	/**
1877
	 * Retrieves the option that was captured from being saved.
1878
	 *
1879
	 * @since 4.2.0
1880
	 * @access protected
1881
	 *
1882
	 * @param string $option_name Option name.
1883
	 * @param mixed  $default     Optional. Default value to return if the option does not exist. Default false.
1884
	 * @return mixed Value set for the option.
1885
	 */
1886
	protected function get_captured_option( $option_name, $default = false ) {
1887
		if ( array_key_exists( $option_name, $this->_captured_options ) ) {
1888
			$value = $this->_captured_options[ $option_name ];
1889
		} else {
1890
			$value = $default;
1891
		}
1892
		return $value;
1893
	}
1894
1895
	/**
1896
	 * Retrieves the number of captured widget option updates.
1897
	 *
1898
	 * @since 3.9.0
1899
	 * @access protected
1900
	 *
1901
	 * @return int Number of updated options.
1902
	 */
1903
	protected function count_captured_options() {
1904
		return count( $this->_captured_options );
1905
	}
1906
1907
	/**
1908
	 * Begins keeping track of changes to widget options, caching new values.
1909
	 *
1910
	 * @since 3.9.0
1911
	 * @access protected
1912
	 */
1913
	protected function start_capturing_option_updates() {
1914
		if ( $this->_is_capturing_option_updates ) {
1915
			return;
1916
		}
1917
1918
		$this->_is_capturing_option_updates = true;
1919
1920
		add_filter( 'pre_update_option', array( $this, 'capture_filter_pre_update_option' ), 10, 3 );
1921
	}
1922
1923
	/**
1924
	 * Pre-filters captured option values before updating.
1925
	 *
1926
	 * @since 3.9.0
1927
	 * @access public
1928
	 *
1929
	 * @param mixed  $new_value   The new option value.
1930
	 * @param string $option_name Name of the option.
1931
	 * @param mixed  $old_value   The old option value.
1932
	 * @return mixed Filtered option value.
1933
	 */
1934
	public function capture_filter_pre_update_option( $new_value, $option_name, $old_value ) {
1935
		if ( $this->is_option_capture_ignored( $option_name ) ) {
1936
			return;
1937
		}
1938
1939
		if ( ! isset( $this->_captured_options[ $option_name ] ) ) {
1940
			add_filter( "pre_option_{$option_name}", array( $this, 'capture_filter_pre_get_option' ) );
1941
		}
1942
1943
		$this->_captured_options[ $option_name ] = $new_value;
1944
1945
		return $old_value;
1946
	}
1947
1948
	/**
1949
	 * Pre-filters captured option values before retrieving.
1950
	 *
1951
	 * @since 3.9.0
1952
	 * @access public
1953
	 *
1954
	 * @param mixed $value Value to return instead of the option value.
1955
	 * @return mixed Filtered option value.
1956
	 */
1957
	public function capture_filter_pre_get_option( $value ) {
1958
		$option_name = preg_replace( '/^pre_option_/', '', current_filter() );
1959
1960
		if ( isset( $this->_captured_options[ $option_name ] ) ) {
1961
			$value = $this->_captured_options[ $option_name ];
1962
1963
			/** This filter is documented in wp-includes/option.php */
1964
			$value = apply_filters( 'option_' . $option_name, $value );
1965
		}
1966
1967
		return $value;
1968
	}
1969
1970
	/**
1971
	 * Undoes any changes to the options since options capture began.
1972
	 *
1973
	 * @since 3.9.0
1974
	 * @access protected
1975
	 */
1976
	protected function stop_capturing_option_updates() {
1977
		if ( ! $this->_is_capturing_option_updates ) {
1978
			return;
1979
		}
1980
1981
		remove_filter( 'pre_update_option', array( $this, 'capture_filter_pre_update_option' ), 10 );
1982
1983
		foreach ( array_keys( $this->_captured_options ) as $option_name ) {
1984
			remove_filter( "pre_option_{$option_name}", array( $this, 'capture_filter_pre_get_option' ) );
1985
		}
1986
1987
		$this->_captured_options = array();
1988
		$this->_is_capturing_option_updates = false;
1989
	}
1990
1991
	/**
1992
	 * {@internal Missing Summary}
1993
	 *
1994
	 * See the {@see 'customize_dynamic_setting_args'} filter.
1995
	 *
1996
	 * @since 3.9.0
1997
	 * @deprecated 4.2.0 Deprecated in favor of the {@see 'customize_dynamic_setting_args'} filter.
1998
	 */
1999
	public function setup_widget_addition_previews() {
2000
		_deprecated_function( __METHOD__, '4.2.0' );
2001
	}
2002
2003
	/**
2004
	 * {@internal Missing Summary}
2005
	 *
2006
	 * See the {@see 'customize_dynamic_setting_args'} filter.
2007
	 *
2008
	 * @since 3.9.0
2009
	 * @deprecated 4.2.0 Deprecated in favor of the {@see 'customize_dynamic_setting_args'} filter.
2010
	 */
2011
	public function prepreview_added_sidebars_widgets() {
2012
		_deprecated_function( __METHOD__, '4.2.0' );
2013
	}
2014
2015
	/**
2016
	 * {@internal Missing Summary}
2017
	 *
2018
	 * See the {@see 'customize_dynamic_setting_args'} filter.
2019
	 *
2020
	 * @since 3.9.0
2021
	 * @deprecated 4.2.0 Deprecated in favor of the {@see 'customize_dynamic_setting_args'} filter.
2022
	 */
2023
	public function prepreview_added_widget_instance() {
2024
		_deprecated_function( __METHOD__, '4.2.0' );
2025
	}
2026
2027
	/**
2028
	 * {@internal Missing Summary}
2029
	 *
2030
	 * See the {@see 'customize_dynamic_setting_args'} filter.
2031
	 *
2032
	 * @since 3.9.0
2033
	 * @deprecated 4.2.0 Deprecated in favor of the {@see 'customize_dynamic_setting_args'} filter.
2034
	 */
2035
	public function remove_prepreview_filters() {
2036
		_deprecated_function( __METHOD__, '4.2.0' );
2037
	}
2038
}
2039