Completed
Push — master ( 8ff7a8...c0c536 )
by Stephen
77:04 queued 40:03
created

is_widget_selective_refreshable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 4
rs 10
c 1
b 0
f 0
cc 1
eloc 3
nc 1
nop 1
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( 'after_setup_theme',                       array( $this, 'register_settings' ) );
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
Bug introduced by
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 ) {
0 ignored issues
show
Unused Code introduced by
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...
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 ) {
0 ignored issues
show
Unused Code introduced by
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...
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 '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
		$sidebars_widgets = array_merge(
380
			array( 'wp_inactive_widgets' => array() ),
381
			array_fill_keys( array_keys( $wp_registered_sidebars ), array() ),
382
			wp_get_sidebars_widgets()
383
		);
384
385
		$new_setting_ids = array();
386
387
		/*
388
		 * Register a setting for all widgets, including those which are active,
389
		 * inactive, and orphaned since a widget may get suppressed from a sidebar
390
		 * via a plugin (like Widget Visibility).
391
		 */
392
		foreach ( array_keys( $wp_registered_widgets ) as $widget_id ) {
393
			$setting_id   = $this->get_setting_id( $widget_id );
394
			$setting_args = $this->get_setting_args( $setting_id );
395
			if ( ! $this->manager->get_setting( $setting_id ) ) {
396
				$this->manager->add_setting( $setting_id, $setting_args );
397
			}
398
			$new_setting_ids[] = $setting_id;
399
		}
400
401
		/*
402
		 * Add a setting which will be supplied for the theme's sidebars_widgets
403
		 * theme_mod when the theme is switched.
404
		 */
405
		if ( ! $this->manager->is_theme_active() ) {
406
			$setting_id = 'old_sidebars_widgets_data';
407
			$setting_args = $this->get_setting_args( $setting_id, array(
408
				'type' => 'global_variable',
409
				'dirty' => true,
410
			) );
411
			$this->manager->add_setting( $setting_id, $setting_args );
412
		}
413
414
		$this->manager->add_panel( 'widgets', array(
415
			'type'            => 'widgets',
416
			'title'           => __( 'Widgets' ),
417
			'description'     => __( 'Widgets are independent sections of content that can be placed into widgetized areas provided by your theme (commonly called sidebars).' ),
418
			'priority'        => 110,
419
			'active_callback' => array( $this, 'is_panel_active' ),
420
		) );
421
422
		foreach ( $sidebars_widgets as $sidebar_id => $sidebar_widget_ids ) {
423
			if ( empty( $sidebar_widget_ids ) ) {
424
				$sidebar_widget_ids = array();
425
			}
426
427
			$is_registered_sidebar = is_registered_sidebar( $sidebar_id );
428
			$is_inactive_widgets   = ( 'wp_inactive_widgets' === $sidebar_id );
429
			$is_active_sidebar     = ( $is_registered_sidebar && ! $is_inactive_widgets );
430
431
			// Add setting for managing the sidebar's widgets.
432
			if ( $is_registered_sidebar || $is_inactive_widgets ) {
433
				$setting_id   = sprintf( 'sidebars_widgets[%s]', $sidebar_id );
434
				$setting_args = $this->get_setting_args( $setting_id );
435
				if ( ! $this->manager->get_setting( $setting_id ) ) {
436
					if ( ! $this->manager->is_theme_active() ) {
437
						$setting_args['dirty'] = true;
438
					}
439
					$this->manager->add_setting( $setting_id, $setting_args );
440
				}
441
				$new_setting_ids[] = $setting_id;
442
443
				// Add section to contain controls.
444
				$section_id = sprintf( 'sidebar-widgets-%s', $sidebar_id );
445
				if ( $is_active_sidebar ) {
446
447
					$section_args = array(
448
						'title' => $wp_registered_sidebars[ $sidebar_id ]['name'],
449
						'description' => $wp_registered_sidebars[ $sidebar_id ]['description'],
450
						'priority' => array_search( $sidebar_id, array_keys( $wp_registered_sidebars ) ),
451
						'panel' => 'widgets',
452
						'sidebar_id' => $sidebar_id,
453
					);
454
455
					/**
456
					 * Filter Customizer widget section arguments for a given sidebar.
457
					 *
458
					 * @since 3.9.0
459
					 *
460
					 * @param array      $section_args Array of Customizer widget section arguments.
461
					 * @param string     $section_id   Customizer section ID.
462
					 * @param int|string $sidebar_id   Sidebar ID.
463
					 */
464
					$section_args = apply_filters( 'customizer_widgets_section_args', $section_args, $section_id, $sidebar_id );
465
466
					$section = new WP_Customize_Sidebar_Section( $this->manager, $section_id, $section_args );
467
					$this->manager->add_section( $section );
468
469
					$control = new WP_Widget_Area_Customize_Control( $this->manager, $setting_id, array(
470
						'section'    => $section_id,
471
						'sidebar_id' => $sidebar_id,
472
						'priority'   => count( $sidebar_widget_ids ), // place 'Add Widget' and 'Reorder' buttons at end.
473
					) );
474
					$new_setting_ids[] = $setting_id;
475
476
					$this->manager->add_control( $control );
477
				}
478
			}
479
480
			// Add a control for each active widget (located in a sidebar).
481
			foreach ( $sidebar_widget_ids as $i => $widget_id ) {
482
483
				// Skip widgets that may have gone away due to a plugin being deactivated.
484
				if ( ! $is_active_sidebar || ! isset( $wp_registered_widgets[$widget_id] ) ) {
485
					continue;
486
				}
487
488
				$registered_widget = $wp_registered_widgets[$widget_id];
489
				$setting_id        = $this->get_setting_id( $widget_id );
490
				$id_base           = $wp_registered_widget_controls[$widget_id]['id_base'];
491
492
				$control = new WP_Widget_Form_Customize_Control( $this->manager, $setting_id, array(
493
					'label'          => $registered_widget['name'],
494
					'section'        => $section_id,
0 ignored issues
show
Bug introduced by
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...
495
					'sidebar_id'     => $sidebar_id,
496
					'widget_id'      => $widget_id,
497
					'widget_id_base' => $id_base,
498
					'priority'       => $i,
499
					'width'          => $wp_registered_widget_controls[$widget_id]['width'],
500
					'height'         => $wp_registered_widget_controls[$widget_id]['height'],
501
					'is_wide'        => $this->is_wide_widget( $widget_id ),
502
				) );
503
				$this->manager->add_control( $control );
504
			}
505
		}
506
507
		if ( ! $this->manager->doing_ajax( 'customize_save' ) ) {
508
			foreach ( $new_setting_ids as $new_setting_id ) {
509
				$this->manager->get_setting( $new_setting_id )->preview();
510
			}
511
		}
512
513
		add_filter( 'sidebars_widgets', array( $this, 'preview_sidebars_widgets' ), 1 );
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 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
		 * Filter 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' );
804
805
		/** This action is documented in wp-admin/admin-footer.php */
806
		do_action( 'admin_footer-widgets.php' );
807
	}
808
809
	/**
810
	 * Retrieves common arguments to supply when constructing a Customizer setting.
811
	 *
812
	 * @since 3.9.0
813
	 * @access public
814
	 *
815
	 * @param string $id        Widget setting ID.
816
	 * @param array  $overrides Array of setting overrides.
817
	 * @return array Possibly modified setting arguments.
818
	 */
819
	public function get_setting_args( $id, $overrides = array() ) {
820
		$args = array(
821
			'type'       => 'option',
822
			'capability' => 'edit_theme_options',
823
			'default'    => array(),
824
		);
825
826
		if ( preg_match( $this->setting_id_patterns['sidebar_widgets'], $id, $matches ) ) {
827
			$args['sanitize_callback'] = array( $this, 'sanitize_sidebar_widgets' );
828
			$args['sanitize_js_callback'] = array( $this, 'sanitize_sidebar_widgets_js_instance' );
829
			$args['transport'] = current_theme_supports( 'customize-selective-refresh-widgets' ) ? 'postMessage' : 'refresh';
830
		} elseif ( preg_match( $this->setting_id_patterns['widget_instance'], $id, $matches ) ) {
831
			$args['sanitize_callback'] = array( $this, 'sanitize_widget_instance' );
832
			$args['sanitize_js_callback'] = array( $this, 'sanitize_widget_js_instance' );
833
			$args['transport'] = $this->is_widget_selective_refreshable( $matches['id_base'] ) ? 'postMessage' : 'refresh';
834
		}
835
836
		$args = array_merge( $args, $overrides );
837
838
		/**
839
		 * Filter the common arguments supplied when constructing a Customizer setting.
840
		 *
841
		 * @since 3.9.0
842
		 *
843
		 * @see WP_Customize_Setting
844
		 *
845
		 * @param array  $args Array of Customizer setting arguments.
846
		 * @param string $id   Widget setting ID.
847
		 */
848
		return apply_filters( 'widget_customizer_setting_args', $args, $id );
849
	}
850
851
	/**
852
	 * Ensures sidebar widget arrays only ever contain widget IDS.
853
	 *
854
	 * Used as the 'sanitize_callback' for each $sidebars_widgets setting.
855
	 *
856
	 * @since 3.9.0
857
	 * @access public
858
	 *
859
	 * @param array $widget_ids Array of widget IDs.
860
	 * @return array Array of sanitized widget IDs.
861
	 */
862
	public function sanitize_sidebar_widgets( $widget_ids ) {
863
		$widget_ids = array_map( 'strval', (array) $widget_ids );
864
		$sanitized_widget_ids = array();
865
		foreach ( $widget_ids as $widget_id ) {
866
			$sanitized_widget_ids[] = preg_replace( '/[^a-z0-9_\-]/', '', $widget_id );
867
		}
868
		return $sanitized_widget_ids;
869
	}
870
871
	/**
872
	 * Builds up an index of all available widgets for use in Backbone models.
873
	 *
874
	 * @since 3.9.0
875
	 * @access public
876
	 *
877
	 * @global array $wp_registered_widgets
878
	 * @global array $wp_registered_widget_controls
879
	 * @staticvar array $available_widgets
880
	 *
881
	 * @see wp_list_widgets()
882
	 *
883
	 * @return array List of available widgets.
884
	 */
885
	public function get_available_widgets() {
886
		static $available_widgets = array();
887
		if ( ! empty( $available_widgets ) ) {
888
			return $available_widgets;
889
		}
890
891
		global $wp_registered_widgets, $wp_registered_widget_controls;
892
		require_once ABSPATH . '/wp-admin/includes/widgets.php'; // for next_widget_id_number()
893
894
		$sort = $wp_registered_widgets;
895
		usort( $sort, array( $this, '_sort_name_callback' ) );
896
		$done = array();
897
898
		foreach ( $sort as $widget ) {
899
			if ( in_array( $widget['callback'], $done, true ) ) { // We already showed this multi-widget
900
				continue;
901
			}
902
903
			$sidebar = is_active_widget( $widget['callback'], $widget['id'], false, false );
904
			$done[]  = $widget['callback'];
905
906
			if ( ! isset( $widget['params'][0] ) ) {
907
				$widget['params'][0] = array();
908
			}
909
910
			$available_widget = $widget;
911
			unset( $available_widget['callback'] ); // not serializable to JSON
912
913
			$args = array(
914
				'widget_id'   => $widget['id'],
915
				'widget_name' => $widget['name'],
916
				'_display'    => 'template',
917
			);
918
919
			$is_disabled     = false;
920
			$is_multi_widget = ( isset( $wp_registered_widget_controls[$widget['id']]['id_base'] ) && isset( $widget['params'][0]['number'] ) );
921
			if ( $is_multi_widget ) {
922
				$id_base            = $wp_registered_widget_controls[$widget['id']]['id_base'];
923
				$args['_temp_id']   = "$id_base-__i__";
924
				$args['_multi_num'] = next_widget_id_number( $id_base );
925
				$args['_add']       = 'multi';
926
			} else {
927
				$args['_add'] = 'single';
928
929
				if ( $sidebar && 'wp_inactive_widgets' !== $sidebar ) {
930
					$is_disabled = true;
931
				}
932
				$id_base = $widget['id'];
933
			}
934
935
			$list_widget_controls_args = wp_list_widget_controls_dynamic_sidebar( array( 0 => $args, 1 => $widget['params'][0] ) );
936
			$control_tpl = $this->get_widget_control( $list_widget_controls_args );
937
938
			// The properties here are mapped to the Backbone Widget model.
939
			$available_widget = array_merge( $available_widget, array(
940
				'temp_id'      => isset( $args['_temp_id'] ) ? $args['_temp_id'] : null,
941
				'is_multi'     => $is_multi_widget,
942
				'control_tpl'  => $control_tpl,
943
				'multi_number' => ( $args['_add'] === 'multi' ) ? $args['_multi_num'] : false,
944
				'is_disabled'  => $is_disabled,
945
				'id_base'      => $id_base,
946
				'transport'    => $this->is_widget_selective_refreshable( $id_base ) ? 'postMessage' : 'refresh',
947
				'width'        => $wp_registered_widget_controls[$widget['id']]['width'],
948
				'height'       => $wp_registered_widget_controls[$widget['id']]['height'],
949
				'is_wide'      => $this->is_wide_widget( $widget['id'] ),
950
			) );
951
952
			$available_widgets[] = $available_widget;
953
		}
954
955
		return $available_widgets;
956
	}
957
958
	/**
959
	 * Naturally orders available widgets by name.
960
	 *
961
	 * @since 3.9.0
962
	 * @access protected
963
	 *
964
	 * @param array $widget_a The first widget to compare.
965
	 * @param array $widget_b The second widget to compare.
966
	 * @return int Reorder position for the current widget comparison.
967
	 */
968
	protected function _sort_name_callback( $widget_a, $widget_b ) {
969
		return strnatcasecmp( $widget_a['name'], $widget_b['name'] );
970
	}
971
972
	/**
973
	 * Retrieves the widget control markup.
974
	 *
975
	 * @since 3.9.0
976
	 * @access public
977
	 *
978
	 * @param array $args Widget control arguments.
979
	 * @return string Widget control form HTML markup.
980
	 */
981
	public function get_widget_control( $args ) {
982
		$args[0]['before_form'] = '<div class="form">';
983
		$args[0]['after_form'] = '</div><!-- .form -->';
984
		$args[0]['before_widget_content'] = '<div class="widget-content">';
985
		$args[0]['after_widget_content'] = '</div><!-- .widget-content -->';
986
		ob_start();
987
		call_user_func_array( 'wp_widget_control', $args );
988
		$control_tpl = ob_get_clean();
989
		return $control_tpl;
990
	}
991
992
	/**
993
	 * Retrieves the widget control markup parts.
994
	 *
995
	 * @since 4.4.0
996
	 * @access public
997
	 *
998
	 * @param array $args Widget control arguments.
999
	 * @return array {
1000
	 *     @type string $control Markup for widget control wrapping form.
1001
	 *     @type string $content The contents of the widget form itself.
1002
	 * }
1003
	 */
1004
	public function get_widget_control_parts( $args ) {
1005
		$args[0]['before_widget_content'] = '<div class="widget-content">';
1006
		$args[0]['after_widget_content'] = '</div><!-- .widget-content -->';
1007
		$control_markup = $this->get_widget_control( $args );
1008
1009
		$content_start_pos = strpos( $control_markup, $args[0]['before_widget_content'] );
1010
		$content_end_pos = strrpos( $control_markup, $args[0]['after_widget_content'] );
1011
1012
		$control = substr( $control_markup, 0, $content_start_pos + strlen( $args[0]['before_widget_content'] ) );
1013
		$control .= substr( $control_markup, $content_end_pos );
1014
		$content = trim( substr(
1015
			$control_markup,
1016
			$content_start_pos + strlen( $args[0]['before_widget_content'] ),
1017
			$content_end_pos - $content_start_pos - strlen( $args[0]['before_widget_content'] )
1018
		) );
1019
1020
		return compact( 'control', 'content' );
1021
	}
1022
1023
	/**
1024
	 * Adds hooks for the Customizer preview.
1025
	 *
1026
	 * @since 3.9.0
1027
	 * @access public
1028
	 */
1029
	public function customize_preview_init() {
1030
		add_action( 'wp_enqueue_scripts', array( $this, 'customize_preview_enqueue' ) );
1031
		add_action( 'wp_print_styles',    array( $this, 'print_preview_css' ), 1 );
1032
		add_action( 'wp_footer',          array( $this, 'export_preview_data' ), 20 );
1033
	}
1034
1035
	/**
1036
	 * Refreshes the nonce for widget updates.
1037
	 *
1038
	 * @since 4.2.0
1039
	 * @access public
1040
	 *
1041
	 * @param  array $nonces Array of nonces.
1042
	 * @return array $nonces Array of nonces.
1043
	 */
1044
	public function refresh_nonces( $nonces ) {
1045
		$nonces['update-widget'] = wp_create_nonce( 'update-widget' );
1046
		return $nonces;
1047
	}
1048
1049
	/**
1050
	 * When previewing, ensures the proper previewing widgets are used.
1051
	 *
1052
	 * Because wp_get_sidebars_widgets() gets called early at {@see 'init' } (via
1053
	 * wp_convert_widget_settings()) and can set global variable `$_wp_sidebars_widgets`
1054
	 * to the value of `get_option( 'sidebars_widgets' )` before the Customizer preview
1055
	 * filter is added, it has to be reset after the filter has been added.
1056
	 *
1057
	 * @since 3.9.0
1058
	 * @access public
1059
	 *
1060
	 * @param array $sidebars_widgets List of widgets for the current sidebar.
1061
	 * @return array
1062
	 */
1063
	public function preview_sidebars_widgets( $sidebars_widgets ) {
0 ignored issues
show
Unused Code introduced by
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...
1064
		$sidebars_widgets = get_option( 'sidebars_widgets' );
1065
1066
		unset( $sidebars_widgets['array_version'] );
1067
		return $sidebars_widgets;
1068
	}
1069
1070
	/**
1071
	 * Enqueues scripts for the Customizer preview.
1072
	 *
1073
	 * @since 3.9.0
1074
	 * @access public
1075
	 */
1076
	public function customize_preview_enqueue() {
1077
		wp_enqueue_script( 'customize-preview-widgets' );
1078
		wp_enqueue_style( 'customize-preview' );
1079
	}
1080
1081
	/**
1082
	 * Inserts default style for highlighted widget at early point so theme
1083
	 * stylesheet can override.
1084
	 *
1085
	 * @since 3.9.0
1086
	 * @access public
1087
	 */
1088
	public function print_preview_css() {
1089
		?>
1090
		<style>
1091
		.widget-customizer-highlighted-widget {
1092
			outline: none;
1093
			-webkit-box-shadow: 0 0 2px rgba(30,140,190,0.8);
1094
			box-shadow: 0 0 2px rgba(30,140,190,0.8);
1095
			position: relative;
1096
			z-index: 1;
1097
		}
1098
		</style>
1099
		<?php
1100
	}
1101
1102
	/**
1103
	 * Communicates the sidebars that appeared on the page at the very end of the page,
1104
	 * and at the very end of the wp_footer,
1105
	 *
1106
	 * @since 3.9.0
1107
	 * @access public
1108
     *
1109
	 * @global array $wp_registered_sidebars
1110
	 * @global array $wp_registered_widgets
1111
	 */
1112
	public function export_preview_data() {
1113
		global $wp_registered_sidebars, $wp_registered_widgets;
1114
1115
		// Prepare Customizer settings to pass to JavaScript.
1116
		$settings = array(
1117
			'renderedSidebars'   => array_fill_keys( array_unique( $this->rendered_sidebars ), true ),
1118
			'renderedWidgets'    => array_fill_keys( array_keys( $this->rendered_widgets ), true ),
1119
			'registeredSidebars' => array_values( $wp_registered_sidebars ),
1120
			'registeredWidgets'  => $wp_registered_widgets,
1121
			'l10n'               => array(
1122
				'widgetTooltip'  => __( 'Shift-click to edit this widget.' ),
1123
			),
1124
			'selectiveRefreshableWidgets' => $this->get_selective_refreshable_widgets(),
1125
		);
1126
		foreach ( $settings['registeredWidgets'] as &$registered_widget ) {
1127
			unset( $registered_widget['callback'] ); // may not be JSON-serializeable
1128
		}
1129
1130
		?>
1131
		<script type="text/javascript">
1132
			var _wpWidgetCustomizerPreviewSettings = <?php echo wp_json_encode( $settings ); ?>;
1133
		</script>
1134
		<?php
1135
	}
1136
1137
	/**
1138
	 * Tracks the widgets that were rendered.
1139
	 *
1140
	 * @since 3.9.0
1141
	 * @access public
1142
	 *
1143
	 * @param array $widget Rendered widget to tally.
1144
	 */
1145
	public function tally_rendered_widgets( $widget ) {
1146
		$this->rendered_widgets[ $widget['id'] ] = true;
1147
	}
1148
1149
	/**
1150
	 * Determine if a widget is rendered on the page.
1151
	 *
1152
	 * @since 4.0.0
1153
	 * @access public
1154
	 *
1155
	 * @param string $widget_id Widget ID to check.
1156
	 * @return bool Whether the widget is rendered.
1157
	 */
1158
	public function is_widget_rendered( $widget_id ) {
1159
		return in_array( $widget_id, $this->rendered_widgets );
1160
	}
1161
1162
	/**
1163
	 * Determines if a sidebar is rendered on the page.
1164
	 *
1165
	 * @since 4.0.0
1166
	 * @access public
1167
	 *
1168
	 * @param string $sidebar_id Sidebar ID to check.
1169
	 * @return bool Whether the sidebar is rendered.
1170
	 */
1171
	public function is_sidebar_rendered( $sidebar_id ) {
1172
		return in_array( $sidebar_id, $this->rendered_sidebars );
1173
	}
1174
1175
	/**
1176
	 * Tallies the sidebars rendered via is_active_sidebar().
1177
	 *
1178
	 * Keep track of the times that is_active_sidebar() is called in the template,
1179
	 * and assume that this means that the sidebar would be rendered on the template
1180
	 * if there were widgets populating it.
1181
	 *
1182
	 * @since 3.9.0
1183
	 * @access public
1184
	 *
1185
	 * @param bool   $is_active  Whether the sidebar is active.
1186
	 * @param string $sidebar_id Sidebar ID.
1187
	 * @return bool Whether the sidebar is active.
1188
	 */
1189
	public function tally_sidebars_via_is_active_sidebar_calls( $is_active, $sidebar_id ) {
1190
		if ( is_registered_sidebar( $sidebar_id ) ) {
1191
			$this->rendered_sidebars[] = $sidebar_id;
1192
		}
1193
		/*
1194
		 * We may need to force this to true, and also force-true the value
1195
		 * for 'dynamic_sidebar_has_widgets' if we want to ensure that there
1196
		 * is an area to drop widgets into, if the sidebar is empty.
1197
		 */
1198
		return $is_active;
1199
	}
1200
1201
	/**
1202
	 * Tallies the sidebars rendered via dynamic_sidebar().
1203
	 *
1204
	 * Keep track of the times that dynamic_sidebar() is called in the template,
1205
	 * and assume this means the sidebar would be rendered on the template if
1206
	 * there were widgets populating it.
1207
	 *
1208
	 * @since 3.9.0
1209
	 * @access public
1210
	 *
1211
	 * @param bool   $has_widgets Whether the current sidebar has widgets.
1212
	 * @param string $sidebar_id  Sidebar ID.
1213
	 * @return bool Whether the current sidebar has widgets.
1214
	 */
1215
	public function tally_sidebars_via_dynamic_sidebar_calls( $has_widgets, $sidebar_id ) {
1216
		if ( is_registered_sidebar( $sidebar_id ) ) {
1217
			$this->rendered_sidebars[] = $sidebar_id;
1218
		}
1219
1220
		/*
1221
		 * We may need to force this to true, and also force-true the value
1222
		 * for 'is_active_sidebar' if we want to ensure there is an area to
1223
		 * drop widgets into, if the sidebar is empty.
1224
		 */
1225
		return $has_widgets;
1226
	}
1227
1228
	/**
1229
	 * Retrieves MAC for a serialized widget instance string.
1230
	 *
1231
	 * Allows values posted back from JS to be rejected if any tampering of the
1232
	 * data has occurred.
1233
	 *
1234
	 * @since 3.9.0
1235
	 * @access protected
1236
	 *
1237
	 * @param string $serialized_instance Widget instance.
1238
	 * @return string MAC for serialized widget instance.
1239
	 */
1240
	protected function get_instance_hash_key( $serialized_instance ) {
1241
		return wp_hash( $serialized_instance );
1242
	}
1243
1244
	/**
1245
	 * Sanitizes a widget instance.
1246
	 *
1247
	 * Unserialize the JS-instance for storing in the options. It's important that this filter
1248
	 * only get applied to an instance *once*.
1249
	 *
1250
	 * @since 3.9.0
1251
	 * @access public
1252
	 *
1253
	 * @param array $value Widget instance to sanitize.
1254
	 * @return array|void Sanitized widget instance.
1255
	 */
1256
	public function sanitize_widget_instance( $value ) {
1257
		if ( $value === array() ) {
1258
			return $value;
1259
		}
1260
1261
		if ( empty( $value['is_widget_customizer_js_value'] )
1262
			|| empty( $value['instance_hash_key'] )
1263
			|| empty( $value['encoded_serialized_instance'] ) )
1264
		{
1265
			return;
1266
		}
1267
1268
		$decoded = base64_decode( $value['encoded_serialized_instance'], true );
1269
		if ( false === $decoded ) {
1270
			return;
1271
		}
1272
1273
		if ( ! hash_equals( $this->get_instance_hash_key( $decoded ), $value['instance_hash_key'] ) ) {
0 ignored issues
show
Security Bug introduced by
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...
1274
			return;
1275
		}
1276
1277
		$instance = unserialize( $decoded );
1278
		if ( false === $instance ) {
1279
			return;
1280
		}
1281
1282
		return $instance;
1283
	}
1284
1285
	/**
1286
	 * Converts a widget instance into JSON-representable format.
1287
	 *
1288
	 * @since 3.9.0
1289
	 * @access public
1290
	 *
1291
	 * @param array $value Widget instance to convert to JSON.
1292
	 * @return array JSON-converted widget instance.
1293
	 */
1294
	public function sanitize_widget_js_instance( $value ) {
1295
		if ( empty( $value['is_widget_customizer_js_value'] ) ) {
1296
			$serialized = serialize( $value );
1297
1298
			$value = array(
1299
				'encoded_serialized_instance'   => base64_encode( $serialized ),
1300
				'title'                         => empty( $value['title'] ) ? '' : $value['title'],
1301
				'is_widget_customizer_js_value' => true,
1302
				'instance_hash_key'             => $this->get_instance_hash_key( $serialized ),
1303
			);
1304
		}
1305
		return $value;
1306
	}
1307
1308
	/**
1309
	 * Strips out widget IDs for widgets which are no longer registered.
1310
	 *
1311
	 * One example where this might happen is when a plugin orphans a widget
1312
	 * in a sidebar upon deactivation.
1313
	 *
1314
	 * @since 3.9.0
1315
	 * @access public
1316
	 *
1317
	 * @global array $wp_registered_widgets
1318
	 *
1319
	 * @param array $widget_ids List of widget IDs.
1320
	 * @return array Parsed list of widget IDs.
1321
	 */
1322
	public function sanitize_sidebar_widgets_js_instance( $widget_ids ) {
1323
		global $wp_registered_widgets;
1324
		$widget_ids = array_values( array_intersect( $widget_ids, array_keys( $wp_registered_widgets ) ) );
1325
		return $widget_ids;
1326
	}
1327
1328
	/**
1329
	 * Finds and invokes the widget update and control callbacks.
1330
	 *
1331
	 * Requires that `$_POST` be populated with the instance data.
1332
	 *
1333
	 * @since 3.9.0
1334
	 * @access public
1335
	 *
1336
	 * @global array $wp_registered_widget_updates
1337
	 * @global array $wp_registered_widget_controls
1338
	 *
1339
	 * @param  string $widget_id Widget ID.
1340
	 * @return WP_Error|array Array containing the updated widget information.
1341
	 *                        A WP_Error object, otherwise.
1342
	 */
1343
	public function call_widget_update( $widget_id ) {
1344
		global $wp_registered_widget_updates, $wp_registered_widget_controls;
1345
1346
		$setting_id = $this->get_setting_id( $widget_id );
1347
1348
		/*
1349
		 * Make sure that other setting changes have previewed since this widget
1350
		 * may depend on them (e.g. Menus being present for Custom Menu widget).
1351
		 */
1352
		if ( ! did_action( 'customize_preview_init' ) ) {
1353
			foreach ( $this->manager->settings() as $setting ) {
1354
				if ( $setting->id !== $setting_id ) {
1355
					$setting->preview();
1356
				}
1357
			}
1358
		}
1359
1360
		$this->start_capturing_option_updates();
1361
		$parsed_id   = $this->parse_widget_id( $widget_id );
1362
		$option_name = 'widget_' . $parsed_id['id_base'];
1363
1364
		/*
1365
		 * If a previously-sanitized instance is provided, populate the input vars
1366
		 * with its values so that the widget update callback will read this instance
1367
		 */
1368
		$added_input_vars = array();
1369
		if ( ! empty( $_POST['sanitized_widget_setting'] ) ) {
1370
			$sanitized_widget_setting = json_decode( $this->get_post_value( 'sanitized_widget_setting' ), true );
1371
			if ( false === $sanitized_widget_setting ) {
1372
				$this->stop_capturing_option_updates();
1373
				return new WP_Error( 'widget_setting_malformed' );
1374
			}
1375
1376
			$instance = $this->sanitize_widget_instance( $sanitized_widget_setting );
1377
			if ( is_null( $instance ) ) {
1378
				$this->stop_capturing_option_updates();
1379
				return new WP_Error( 'widget_setting_unsanitized' );
1380
			}
1381
1382
			if ( ! is_null( $parsed_id['number'] ) ) {
1383
				$value = array();
1384
				$value[$parsed_id['number']] = $instance;
1385
				$key = 'widget-' . $parsed_id['id_base'];
1386
				$_REQUEST[$key] = $_POST[$key] = wp_slash( $value );
1387
				$added_input_vars[] = $key;
1388
			} else {
1389
				foreach ( $instance as $key => $value ) {
1390
					$_REQUEST[$key] = $_POST[$key] = wp_slash( $value );
1391
					$added_input_vars[] = $key;
1392
				}
1393
			}
1394
		}
1395
1396
		// Invoke the widget update callback.
1397 View Code Duplication
		foreach ( (array) $wp_registered_widget_updates as $name => $control ) {
1398
			if ( $name === $parsed_id['id_base'] && is_callable( $control['callback'] ) ) {
1399
				ob_start();
1400
				call_user_func_array( $control['callback'], $control['params'] );
1401
				ob_end_clean();
1402
				break;
1403
			}
1404
		}
1405
1406
		// Clean up any input vars that were manually added
1407
		foreach ( $added_input_vars as $key ) {
1408
			unset( $_POST[ $key ] );
1409
			unset( $_REQUEST[ $key ] );
1410
		}
1411
1412
		// Make sure the expected option was updated.
1413
		if ( 0 !== $this->count_captured_options() ) {
1414
			if ( $this->count_captured_options() > 1 ) {
1415
				$this->stop_capturing_option_updates();
1416
				return new WP_Error( 'widget_setting_too_many_options' );
1417
			}
1418
1419
			$updated_option_name = key( $this->get_captured_options() );
1420
			if ( $updated_option_name !== $option_name ) {
1421
				$this->stop_capturing_option_updates();
1422
				return new WP_Error( 'widget_setting_unexpected_option' );
1423
			}
1424
		}
1425
1426
		// Obtain the widget instance.
1427
		$option = $this->get_captured_option( $option_name );
1428
		if ( null !== $parsed_id['number'] ) {
1429
			$instance = $option[ $parsed_id['number'] ];
1430
		} else {
1431
			$instance = $option;
1432
		}
1433
1434
		/*
1435
		 * Override the incoming $_POST['customized'] for a newly-created widget's
1436
		 * setting with the new $instance so that the preview filter currently
1437
		 * in place from WP_Customize_Setting::preview() will use this value
1438
		 * instead of the default widget instance value (an empty array).
1439
		 */
1440
		$this->manager->set_post_value( $setting_id, $this->sanitize_widget_js_instance( $instance ) );
1441
1442
		// Obtain the widget control with the updated instance in place.
1443
		ob_start();
1444
		$form = $wp_registered_widget_controls[ $widget_id ];
1445
		if ( $form ) {
1446
			call_user_func_array( $form['callback'], $form['params'] );
1447
		}
1448
		$form = ob_get_clean();
1449
1450
		$this->stop_capturing_option_updates();
1451
1452
		return compact( 'instance', 'form' );
1453
	}
1454
1455
	/**
1456
	 * Updates widget settings asynchronously.
1457
	 *
1458
	 * Allows the Customizer to update a widget using its form, but return the new
1459
	 * instance info via Ajax instead of saving it to the options table.
1460
	 *
1461
	 * Most code here copied from wp_ajax_save_widget().
1462
	 *
1463
	 * @since 3.9.0
1464
	 * @access public
1465
	 *
1466
	 * @see wp_ajax_save_widget()
1467
	 */
1468
	public function wp_ajax_update_widget() {
1469
1470
		if ( ! is_user_logged_in() ) {
1471
			wp_die( 0 );
1472
		}
1473
1474
		check_ajax_referer( 'update-widget', 'nonce' );
1475
1476
		if ( ! current_user_can( 'edit_theme_options' ) ) {
1477
			wp_die( -1 );
1478
		}
1479
1480
		if ( empty( $_POST['widget-id'] ) ) {
1481
			wp_send_json_error( 'missing_widget-id' );
1482
		}
1483
1484
		/** This action is documented in wp-admin/includes/ajax-actions.php */
1485
		do_action( 'load-widgets.php' );
1486
1487
		/** This action is documented in wp-admin/includes/ajax-actions.php */
1488
		do_action( 'widgets.php' );
1489
1490
		/** This action is documented in wp-admin/widgets.php */
1491
		do_action( 'sidebar_admin_setup' );
1492
1493
		$widget_id = $this->get_post_value( 'widget-id' );
1494
		$parsed_id = $this->parse_widget_id( $widget_id );
1495
		$id_base = $parsed_id['id_base'];
1496
1497
		$is_updating_widget_template = (
1498
			isset( $_POST[ 'widget-' . $id_base ] )
1499
			&&
1500
			is_array( $_POST[ 'widget-' . $id_base ] )
1501
			&&
1502
			preg_match( '/__i__|%i%/', key( $_POST[ 'widget-' . $id_base ] ) )
1503
		);
1504
		if ( $is_updating_widget_template ) {
1505
			wp_send_json_error( 'template_widget_not_updatable' );
1506
		}
1507
1508
		$updated_widget = $this->call_widget_update( $widget_id ); // => {instance,form}
1509
		if ( is_wp_error( $updated_widget ) ) {
1510
			wp_send_json_error( $updated_widget->get_error_code() );
1511
		}
1512
1513
		$form = $updated_widget['form'];
1514
		$instance = $this->sanitize_widget_js_instance( $updated_widget['instance'] );
1515
1516
		wp_send_json_success( compact( 'form', 'instance' ) );
1517
	}
1518
1519
	/*
1520
	 * Selective Refresh Methods
1521
	 */
1522
1523
	/**
1524
	 * Filters arguments for dynamic widget partials.
1525
	 *
1526
	 * @since 4.5.0
1527
	 * @access public
1528
	 *
1529
	 * @param array|false $partial_args Partial arguments.
1530
	 * @param string      $partial_id   Partial ID.
1531
	 * @return array (Maybe) modified partial arguments.
1532
	 */
1533
	public function customize_dynamic_partial_args( $partial_args, $partial_id ) {
1534
		if ( ! current_theme_supports( 'customize-selective-refresh-widgets' ) ) {
1535
			return $partial_args;
1536
		}
1537
1538
		if ( preg_match( '/^widget\[(?P<widget_id>.+)\]$/', $partial_id, $matches ) ) {
1539
			if ( false === $partial_args ) {
1540
				$partial_args = array();
1541
			}
1542
			$partial_args = array_merge(
1543
				$partial_args,
1544
				array(
1545
					'type'                => 'widget',
1546
					'render_callback'     => array( $this, 'render_widget_partial' ),
1547
					'container_inclusive' => true,
1548
					'settings'            => array( $this->get_setting_id( $matches['widget_id'] ) ),
1549
					'capability'          => 'edit_theme_options',
1550
				)
1551
			);
1552
		}
1553
1554
		return $partial_args;
1555
	}
1556
1557
	/**
1558
	 * Adds hooks for selective refresh.
1559
	 *
1560
	 * @since 4.5.0
1561
	 * @access public
1562
	 */
1563
	public function selective_refresh_init() {
1564
		if ( ! current_theme_supports( 'customize-selective-refresh-widgets' ) ) {
1565
			return;
1566
		}
1567
		add_filter( 'dynamic_sidebar_params', array( $this, 'filter_dynamic_sidebar_params' ) );
1568
		add_filter( 'wp_kses_allowed_html', array( $this, 'filter_wp_kses_allowed_data_attributes' ) );
1569
		add_action( 'dynamic_sidebar_before', array( $this, 'start_dynamic_sidebar' ) );
1570
		add_action( 'dynamic_sidebar_after', array( $this, 'end_dynamic_sidebar' ) );
1571
	}
1572
1573
	/**
1574
	 * Inject selective refresh data attributes into widget container elements.
1575
	 *
1576
	 * @param array $params {
1577
	 *     Dynamic sidebar params.
1578
	 *
1579
	 *     @type array $args        Sidebar args.
1580
	 *     @type array $widget_args Widget args.
1581
	 * }
1582
	 * @see WP_Customize_Nav_Menus_Partial_Refresh::filter_wp_nav_menu_args()
1583
	 *
1584
	 * @return array Params.
1585
	 */
1586
	public function filter_dynamic_sidebar_params( $params ) {
1587
		$sidebar_args = array_merge(
1588
			array(
1589
				'before_widget' => '',
1590
				'after_widget' => '',
1591
			),
1592
			$params[0]
1593
		);
1594
1595
		// Skip widgets not in a registered sidebar or ones which lack a proper wrapper element to attach the data-* attributes to.
1596
		$matches = array();
1597
		$is_valid = (
1598
			isset( $sidebar_args['id'] )
1599
			&&
1600
			is_registered_sidebar( $sidebar_args['id'] )
1601
			&&
1602
			( isset( $this->current_dynamic_sidebar_id_stack[0] ) && $this->current_dynamic_sidebar_id_stack[0] === $sidebar_args['id'] )
1603
			&&
1604
			preg_match( '#^<(?P<tag_name>\w+)#', $sidebar_args['before_widget'], $matches )
1605
		);
1606
		if ( ! $is_valid ) {
1607
			return $params;
1608
		}
1609
		$this->before_widget_tags_seen[ $matches['tag_name'] ] = true;
1610
1611
		$context = array(
1612
			'sidebar_id' => $sidebar_args['id'],
1613
		);
1614
		if ( isset( $this->context_sidebar_instance_number ) ) {
1615
			$context['sidebar_instance_number'] = $this->context_sidebar_instance_number;
1616
		} else if ( isset( $sidebar_args['id'] ) && isset( $this->sidebar_instance_count[ $sidebar_args['id'] ] ) ) {
1617
			$context['sidebar_instance_number'] = $this->sidebar_instance_count[ $sidebar_args['id'] ];
1618
		}
1619
1620
		$attributes = sprintf( ' data-customize-partial-id="%s"', esc_attr( 'widget[' . $sidebar_args['widget_id'] . ']' ) );
1621
		$attributes .= ' data-customize-partial-type="widget"';
1622
		$attributes .= sprintf( ' data-customize-partial-placement-context="%s"', esc_attr( wp_json_encode( $context ) ) );
1623
		$attributes .= sprintf( ' data-customize-widget-id="%s"', esc_attr( $sidebar_args['widget_id'] ) );
1624
		$sidebar_args['before_widget'] = preg_replace( '#^(<\w+)#', '$1 ' . $attributes, $sidebar_args['before_widget'] );
1625
1626
		$params[0] = $sidebar_args;
1627
		return $params;
1628
	}
1629
1630
	/**
1631
	 * List of the tag names seen for before_widget strings.
1632
	 *
1633
	 * This is used in the {@see 'filter_wp_kses_allowed_html'} filter to ensure that the
1634
	 * data-* attributes can be whitelisted.
1635
	 *
1636
	 * @since 4.5.0
1637
	 * @access protected
1638
	 * @var array
1639
	 */
1640
	protected $before_widget_tags_seen = array();
1641
1642
	/**
1643
	 * Ensures the HTML data-* attributes for selective refresh are allowed by kses.
1644
	 *
1645
	 * This is needed in case the `$before_widget` is run through wp_kses() when printed.
1646
	 *
1647
	 * @since 4.5.0
1648
	 * @access public
1649
	 *
1650
	 * @param array $allowed_html Allowed HTML.
1651
	 * @return array (Maybe) modified allowed HTML.
1652
	 */
1653
	public function filter_wp_kses_allowed_data_attributes( $allowed_html ) {
1654
		foreach ( array_keys( $this->before_widget_tags_seen ) as $tag_name ) {
1655
			if ( ! isset( $allowed_html[ $tag_name ] ) ) {
1656
				$allowed_html[ $tag_name ] = array();
1657
			}
1658
			$allowed_html[ $tag_name ] = array_merge(
1659
				$allowed_html[ $tag_name ],
1660
				array_fill_keys( array(
1661
					'data-customize-partial-id',
1662
					'data-customize-partial-type',
1663
					'data-customize-partial-placement-context',
1664
					'data-customize-partial-widget-id',
1665
					'data-customize-partial-options',
1666
				), true )
1667
			);
1668
		}
1669
		return $allowed_html;
1670
	}
1671
1672
	/**
1673
	 * Keep track of the number of times that dynamic_sidebar() was called for a given sidebar index.
1674
	 *
1675
	 * This helps facilitate the uncommon scenario where a single sidebar is rendered multiple times on a template.
1676
	 *
1677
	 * @since 4.5.0
1678
	 * @access protected
1679
	 * @var array
1680
	 */
1681
	protected $sidebar_instance_count = array();
1682
1683
	/**
1684
	 * The current request's sidebar_instance_number context.
1685
	 *
1686
	 * @since 4.5.0
1687
	 * @access protected
1688
	 * @var int
1689
	 */
1690
	protected $context_sidebar_instance_number;
1691
1692
	/**
1693
	 * Current sidebar ID being rendered.
1694
	 *
1695
	 * @since 4.5.0
1696
	 * @access protected
1697
	 * @var array
1698
	 */
1699
	protected $current_dynamic_sidebar_id_stack = array();
1700
1701
	/**
1702
	 * Begins keeping track of the current sidebar being rendered.
1703
	 *
1704
	 * Insert marker before widgets are rendered in a dynamic sidebar.
1705
	 *
1706
	 * @since 4.5.0
1707
	 * @access public
1708
	 *
1709
	 * @param int|string $index Index, name, or ID of the dynamic sidebar.
1710
	 */
1711
	public function start_dynamic_sidebar( $index ) {
1712
		array_unshift( $this->current_dynamic_sidebar_id_stack, $index );
1713
		if ( ! isset( $this->sidebar_instance_count[ $index ] ) ) {
1714
			$this->sidebar_instance_count[ $index ] = 0;
1715
		}
1716
		$this->sidebar_instance_count[ $index ] += 1;
1717 View Code Duplication
		if ( ! $this->manager->selective_refresh->is_render_partials_request() ) {
1718
			printf( "\n<!--dynamic_sidebar_before:%s:%d-->\n", esc_html( $index ), intval( $this->sidebar_instance_count[ $index ] ) );
1719
		}
1720
	}
1721
1722
	/**
1723
	 * Finishes keeping track of the current sidebar being rendered.
1724
	 *
1725
	 * Inserts a marker after widgets are rendered in a dynamic sidebar.
1726
	 *
1727
	 * @since 4.5.0
1728
	 * @access public
1729
	 *
1730
	 * @param int|string $index Index, name, or ID of the dynamic sidebar.
1731
	 */
1732
	public function end_dynamic_sidebar( $index ) {
1733
		array_shift( $this->current_dynamic_sidebar_id_stack );
1734 View Code Duplication
		if ( ! $this->manager->selective_refresh->is_render_partials_request() ) {
1735
			printf( "\n<!--dynamic_sidebar_after:%s:%d-->\n", esc_html( $index ), intval( $this->sidebar_instance_count[ $index ] ) );
1736
		}
1737
	}
1738
1739
	/**
1740
	 * Current sidebar being rendered.
1741
	 *
1742
	 * @since 4.5.0
1743
	 * @access protected
1744
	 * @var string
1745
	 */
1746
	protected $rendering_widget_id;
1747
1748
	/**
1749
	 * Current widget being rendered.
1750
	 *
1751
	 * @since 4.5.0
1752
	 * @access protected
1753
	 * @var string
1754
	 */
1755
	protected $rendering_sidebar_id;
1756
1757
	/**
1758
	 * Filters sidebars_widgets to ensure the currently-rendered widget is the only widget in the current sidebar.
1759
	 *
1760
	 * @since 4.5.0
1761
	 * @access protected
1762
	 *
1763
	 * @param array $sidebars_widgets Sidebars widgets.
1764
	 * @return array Filtered sidebars widgets.
1765
	 */
1766
	public function filter_sidebars_widgets_for_rendering_widget( $sidebars_widgets ) {
1767
		$sidebars_widgets[ $this->rendering_sidebar_id ] = array( $this->rendering_widget_id );
1768
		return $sidebars_widgets;
1769
	}
1770
1771
	/**
1772
	 * Renders a specific widget using the supplied sidebar arguments.
1773
	 *
1774
	 * @since 4.5.0
1775
	 * @access public
1776
	 *
1777
	 * @see dynamic_sidebar()
1778
	 *
1779
	 * @param WP_Customize_Partial $partial Partial.
1780
	 * @param array                $context {
1781
	 *     Sidebar args supplied as container context.
1782
	 *
1783
	 *     @type string $sidebar_id              ID for sidebar for widget to render into.
1784
	 *     @type int    $sidebar_instance_number Disambiguating instance number.
1785
	 * }
1786
	 * @return string|false
1787
	 */
1788
	public function render_widget_partial( $partial, $context ) {
1789
		$id_data   = $partial->id_data();
1790
		$widget_id = array_shift( $id_data['keys'] );
1791
1792
		if ( ! is_array( $context )
1793
			|| empty( $context['sidebar_id'] )
1794
			|| ! is_registered_sidebar( $context['sidebar_id'] )
1795
		) {
1796
			return false;
1797
		}
1798
1799
		$this->rendering_sidebar_id = $context['sidebar_id'];
1800
1801
		if ( isset( $context['sidebar_instance_number'] ) ) {
1802
			$this->context_sidebar_instance_number = intval( $context['sidebar_instance_number'] );
1803
		}
1804
1805
		// Filter sidebars_widgets so that only the queried widget is in the sidebar.
1806
		$this->rendering_widget_id = $widget_id;
1807
1808
		$filter_callback = array( $this, 'filter_sidebars_widgets_for_rendering_widget' );
1809
		add_filter( 'sidebars_widgets', $filter_callback, 1000 );
1810
1811
		// Render the widget.
1812
		ob_start();
1813
		dynamic_sidebar( $this->rendering_sidebar_id = $context['sidebar_id'] );
1814
		$container = ob_get_clean();
1815
1816
		// Reset variables for next partial render.
1817
		remove_filter( 'sidebars_widgets', $filter_callback, 1000 );
1818
1819
		$this->context_sidebar_instance_number = null;
1820
		$this->rendering_sidebar_id = null;
1821
		$this->rendering_widget_id = null;
1822
1823
		return $container;
1824
	}
1825
1826
	//
1827
	// Option Update Capturing
1828
	//
1829
1830
	/**
1831
	 * List of captured widget option updates.
1832
	 *
1833
	 * @since 3.9.0
1834
	 * @access protected
1835
	 * @var array $_captured_options Values updated while option capture is happening.
1836
	 */
1837
	protected $_captured_options = array();
1838
1839
	/**
1840
	 * Whether option capture is currently happening.
1841
	 *
1842
	 * @since 3.9.0
1843
	 * @access protected
1844
	 * @var bool $_is_current Whether option capture is currently happening or not.
1845
	 */
1846
	protected $_is_capturing_option_updates = false;
1847
1848
	/**
1849
	 * Determines whether the captured option update should be ignored.
1850
	 *
1851
	 * @since 3.9.0
1852
	 * @access protected
1853
	 *
1854
	 * @param string $option_name Option name.
1855
	 * @return bool Whether the option capture is ignored.
1856
	 */
1857
	protected function is_option_capture_ignored( $option_name ) {
1858
		return ( 0 === strpos( $option_name, '_transient_' ) );
1859
	}
1860
1861
	/**
1862
	 * Retrieves captured widget option updates.
1863
	 *
1864
	 * @since 3.9.0
1865
	 * @access protected
1866
	 *
1867
	 * @return array Array of captured options.
1868
	 */
1869
	protected function get_captured_options() {
1870
		return $this->_captured_options;
1871
	}
1872
1873
	/**
1874
	 * Retrieves the option that was captured from being saved.
1875
	 *
1876
	 * @since 4.2.0
1877
	 * @access protected
1878
	 *
1879
	 * @param string $option_name Option name.
1880
	 * @param mixed  $default     Optional. Default value to return if the option does not exist. Default false.
1881
	 * @return mixed Value set for the option.
1882
	 */
1883
	protected function get_captured_option( $option_name, $default = false ) {
1884
		if ( array_key_exists( $option_name, $this->_captured_options ) ) {
1885
			$value = $this->_captured_options[ $option_name ];
1886
		} else {
1887
			$value = $default;
1888
		}
1889
		return $value;
1890
	}
1891
1892
	/**
1893
	 * Retrieves the number of captured widget option updates.
1894
	 *
1895
	 * @since 3.9.0
1896
	 * @access protected
1897
	 *
1898
	 * @return int Number of updated options.
1899
	 */
1900
	protected function count_captured_options() {
1901
		return count( $this->_captured_options );
1902
	}
1903
1904
	/**
1905
	 * Begins keeping track of changes to widget options, caching new values.
1906
	 *
1907
	 * @since 3.9.0
1908
	 * @access protected
1909
	 */
1910
	protected function start_capturing_option_updates() {
1911
		if ( $this->_is_capturing_option_updates ) {
1912
			return;
1913
		}
1914
1915
		$this->_is_capturing_option_updates = true;
1916
1917
		add_filter( 'pre_update_option', array( $this, 'capture_filter_pre_update_option' ), 10, 3 );
1918
	}
1919
1920
	/**
1921
	 * Pre-filters captured option values before updating.
1922
	 *
1923
	 * @since 3.9.0
1924
	 * @access public
1925
	 *
1926
	 * @param mixed  $new_value   The new option value.
1927
	 * @param string $option_name Name of the option.
1928
	 * @param mixed  $old_value   The old option value.
1929
	 * @return mixed Filtered option value.
1930
	 */
1931
	public function capture_filter_pre_update_option( $new_value, $option_name, $old_value ) {
1932
		if ( $this->is_option_capture_ignored( $option_name ) ) {
1933
			return;
1934
		}
1935
1936
		if ( ! isset( $this->_captured_options[ $option_name ] ) ) {
1937
			add_filter( "pre_option_{$option_name}", array( $this, 'capture_filter_pre_get_option' ) );
1938
		}
1939
1940
		$this->_captured_options[ $option_name ] = $new_value;
1941
1942
		return $old_value;
1943
	}
1944
1945
	/**
1946
	 * Pre-filters captured option values before retrieving.
1947
	 *
1948
	 * @since 3.9.0
1949
	 * @access public
1950
	 *
1951
	 * @param mixed $value Value to return instead of the option value.
1952
	 * @return mixed Filtered option value.
1953
	 */
1954
	public function capture_filter_pre_get_option( $value ) {
1955
		$option_name = preg_replace( '/^pre_option_/', '', current_filter() );
1956
1957
		if ( isset( $this->_captured_options[ $option_name ] ) ) {
1958
			$value = $this->_captured_options[ $option_name ];
1959
1960
			/** This filter is documented in wp-includes/option.php */
1961
			$value = apply_filters( 'option_' . $option_name, $value );
1962
		}
1963
1964
		return $value;
1965
	}
1966
1967
	/**
1968
	 * Undoes any changes to the options since options capture began.
1969
	 *
1970
	 * @since 3.9.0
1971
	 * @access protected
1972
	 */
1973
	protected function stop_capturing_option_updates() {
1974
		if ( ! $this->_is_capturing_option_updates ) {
1975
			return;
1976
		}
1977
1978
		remove_filter( 'pre_update_option', array( $this, 'capture_filter_pre_update_option' ), 10 );
1979
1980
		foreach ( array_keys( $this->_captured_options ) as $option_name ) {
1981
			remove_filter( "pre_option_{$option_name}", array( $this, 'capture_filter_pre_get_option' ) );
1982
		}
1983
1984
		$this->_captured_options = array();
1985
		$this->_is_capturing_option_updates = false;
1986
	}
1987
1988
	/**
1989
	 * @since 3.9.0
1990
	 * @deprecated 4.2.0 Deprecated in favor of customize_dynamic_setting_args filter.
1991
	 */
1992
	public function setup_widget_addition_previews() {
1993
		_deprecated_function( __METHOD__, '4.2.0' );
1994
	}
1995
1996
	/**
1997
	 * @since 3.9.0
1998
	 * @deprecated 4.2.0 Deprecated in favor of customize_dynamic_setting_args filter.
1999
	 */
2000
	public function prepreview_added_sidebars_widgets() {
2001
		_deprecated_function( __METHOD__, '4.2.0' );
2002
	}
2003
2004
	/**
2005
	 * @since 3.9.0
2006
	 * @deprecated 4.2.0 Deprecated in favor of customize_dynamic_setting_args filter.
2007
	 */
2008
	public function prepreview_added_widget_instance() {
2009
		_deprecated_function( __METHOD__, '4.2.0' );
2010
	}
2011
2012
	/**
2013
	 * @since 3.9.0
2014
	 * @deprecated 4.2.0 Deprecated in favor of customize_dynamic_setting_args filter.
2015
	 */
2016
	public function remove_prepreview_filters() {
2017
		_deprecated_function( __METHOD__, '4.2.0' );
2018
	}
2019
}
2020