~canonical-sysadmins/wordpress/4.7.2

« back to all changes in this revision

Viewing changes to wp-includes/class-wp-customize-widgets.php

  • Committer: Jacek Nykis
  • Date: 2015-01-05 16:17:05 UTC
  • Revision ID: jacek.nykis@canonical.com-20150105161705-w544l1h5mcg7u4w9
Initial commit

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
<?php
 
2
/**
 
3
 * Customize Widgets Class
 
4
 *
 
5
 * Implements widget management in the Customizer.
 
6
 *
 
7
 * @package WordPress
 
8
 * @subpackage Customize
 
9
 * @since 3.9.0
 
10
 */
 
11
final class WP_Customize_Widgets {
 
12
 
 
13
        /**
 
14
         * WP_Customize_Manager instance.
 
15
         *
 
16
         * @since 3.9.0
 
17
         * @access public
 
18
         * @var WP_Customize_Manager
 
19
         */
 
20
        public $manager;
 
21
 
 
22
        /**
 
23
         * All id_bases for widgets defined in core.
 
24
         *
 
25
         * @since 3.9.0
 
26
         * @access protected
 
27
         * @var array
 
28
         */
 
29
        protected $core_widget_id_bases = array(
 
30
                'archives', 'calendar', 'categories', 'links', 'meta',
 
31
                'nav_menu', 'pages', 'recent-comments', 'recent-posts',
 
32
                'rss', 'search', 'tag_cloud', 'text',
 
33
        );
 
34
 
 
35
        /**
 
36
         * @since 3.9.0
 
37
         * @access protected
 
38
         * @var
 
39
         */
 
40
        protected $_customized;
 
41
 
 
42
        /**
 
43
         * @since 3.9.0
 
44
         * @access protected
 
45
         * @var array
 
46
         */
 
47
        protected $_prepreview_added_filters = array();
 
48
 
 
49
        /**
 
50
         * @since 3.9.0
 
51
         * @access protected
 
52
         * @var array
 
53
         */
 
54
        protected $rendered_sidebars = array();
 
55
 
 
56
        /**
 
57
         * @since 3.9.0
 
58
         * @access protected
 
59
         * @var array
 
60
         */
 
61
        protected $rendered_widgets = array();
 
62
 
 
63
        /**
 
64
         * @since 3.9.0
 
65
         * @access protected
 
66
         * @var array
 
67
         */
 
68
        protected $old_sidebars_widgets = array();
 
69
 
 
70
        /**
 
71
         * Initial loader.
 
72
         *
 
73
         * @since 3.9.0
 
74
         * @access public
 
75
         *
 
76
         * @param WP_Customize_Manager $manager Customize manager bootstrap instance.
 
77
         */
 
78
        public function __construct( $manager ) {
 
79
                $this->manager = $manager;
 
80
 
 
81
                add_action( 'after_setup_theme',                       array( $this, 'setup_widget_addition_previews' ) );
 
82
                add_action( 'wp_loaded',                               array( $this, 'override_sidebars_widgets_for_theme_switch' ) );
 
83
                add_action( 'customize_controls_init',                 array( $this, 'customize_controls_init' ) );
 
84
                add_action( 'customize_register',                      array( $this, 'schedule_customize_register' ), 1 );
 
85
                add_action( 'customize_controls_enqueue_scripts',      array( $this, 'enqueue_scripts' ) );
 
86
                add_action( 'customize_controls_print_styles',         array( $this, 'print_styles' ) );
 
87
                add_action( 'customize_controls_print_scripts',        array( $this, 'print_scripts' ) );
 
88
                add_action( 'customize_controls_print_footer_scripts', array( $this, 'print_footer_scripts' ) );
 
89
                add_action( 'customize_controls_print_footer_scripts', array( $this, 'output_widget_control_templates' ) );
 
90
                add_action( 'customize_preview_init',                  array( $this, 'customize_preview_init' ) );
 
91
 
 
92
                add_action( 'dynamic_sidebar',                         array( $this, 'tally_rendered_widgets' ) );
 
93
                add_filter( 'is_active_sidebar',                       array( $this, 'tally_sidebars_via_is_active_sidebar_calls' ), 10, 2 );
 
94
                add_filter( 'dynamic_sidebar_has_widgets',             array( $this, 'tally_sidebars_via_dynamic_sidebar_calls' ), 10, 2 );
 
95
        }
 
96
 
 
97
        /**
 
98
         * Get an unslashed post value or return a default.
 
99
         *
 
100
         * @since 3.9.0
 
101
         *
 
102
         * @access protected
 
103
         *
 
104
         * @param string $name    Post value.
 
105
         * @param mixed  $default Default post value.
 
106
         * @return mixed Unslashed post value or default value.
 
107
         */
 
108
        protected function get_post_value( $name, $default = null ) {
 
109
                if ( ! isset( $_POST[ $name ] ) ) {
 
110
                        return $default;
 
111
                }
 
112
 
 
113
                return wp_unslash( $_POST[$name] );
 
114
        }
 
115
 
 
116
        /**
 
117
         * Set up widget addition previews.
 
118
         *
 
119
         * Since the widgets get registered on 'widgets_init' before the customizer
 
120
         * settings are set up on 'customize_register', we have to filter the options
 
121
         * similarly to how the setting previewer will filter the options later.
 
122
         *
 
123
         * @since 3.9.0
 
124
         *
 
125
         * @access public
 
126
         */
 
127
        public function setup_widget_addition_previews() {
 
128
                $is_customize_preview = false;
 
129
 
 
130
                if ( ! empty( $this->manager ) && ! is_admin() && 'on' === $this->get_post_value( 'wp_customize' ) ) {
 
131
                        $is_customize_preview = check_ajax_referer( 'preview-customize_' . $this->manager->get_stylesheet(), 'nonce', false );
 
132
                }
 
133
 
 
134
                $is_ajax_widget_update = false;
 
135
                if ( $this->manager->doing_ajax() && 'update-widget' === $this->get_post_value( 'action' ) ) {
 
136
                        $is_ajax_widget_update = check_ajax_referer( 'update-widget', 'nonce', false );
 
137
                }
 
138
 
 
139
                $is_ajax_customize_save = false;
 
140
                if ( $this->manager->doing_ajax() && 'customize_save' === $this->get_post_value( 'action' ) ) {
 
141
                        $is_ajax_customize_save = check_ajax_referer( 'save-customize_' . $this->manager->get_stylesheet(), 'nonce', false );
 
142
                }
 
143
 
 
144
                $is_valid_request = ( $is_ajax_widget_update || $is_customize_preview || $is_ajax_customize_save );
 
145
                if ( ! $is_valid_request ) {
 
146
                        return;
 
147
                }
 
148
 
 
149
                // Input from customizer preview.
 
150
                if ( isset( $_POST['customized'] ) ) {
 
151
                        $this->_customized = json_decode( $this->get_post_value( 'customized' ), true );
 
152
                } else { // Input from ajax widget update request.
 
153
                        $this->_customized = array();
 
154
                        $id_base = $this->get_post_value( 'id_base' );
 
155
                        $widget_number = $this->get_post_value( 'widget_number', false );
 
156
                        $option_name = 'widget_' . $id_base;
 
157
                        $this->_customized[ $option_name ] = array();
 
158
                        if ( preg_match( '/^[0-9]+$/', $widget_number ) ) {
 
159
                                $option_name .= '[' . $widget_number . ']';
 
160
                                $this->_customized[ $option_name ][ $widget_number ] = array();
 
161
                        }
 
162
                }
 
163
 
 
164
                $function = array( $this, 'prepreview_added_sidebars_widgets' );
 
165
 
 
166
                $hook = 'option_sidebars_widgets';
 
167
                add_filter( $hook, $function );
 
168
                $this->_prepreview_added_filters[] = compact( 'hook', 'function' );
 
169
 
 
170
                $hook = 'default_option_sidebars_widgets';
 
171
                add_filter( $hook, $function );
 
172
                $this->_prepreview_added_filters[] = compact( 'hook', 'function' );
 
173
 
 
174
                $function = array( $this, 'prepreview_added_widget_instance' );
 
175
                foreach ( $this->_customized as $setting_id => $value ) {
 
176
                        if ( preg_match( '/^(widget_.+?)(?:\[(\d+)\])?$/', $setting_id, $matches ) ) {
 
177
                                $option = $matches[1];
 
178
 
 
179
                                $hook = sprintf( 'option_%s', $option );
 
180
                                if ( ! has_filter( $hook, $function ) ) {
 
181
                                        add_filter( $hook, $function );
 
182
                                        $this->_prepreview_added_filters[] = compact( 'hook', 'function' );
 
183
                                }
 
184
 
 
185
                                $hook = sprintf( 'default_option_%s', $option );
 
186
                                if ( ! has_filter( $hook, $function ) ) {
 
187
                                        add_filter( $hook, $function );
 
188
                                        $this->_prepreview_added_filters[] = compact( 'hook', 'function' );
 
189
                                }
 
190
 
 
191
                                /*
 
192
                                 * Make sure the option is registered so that the update_option()
 
193
                                 * won't fail due to the filters providing a default value, which
 
194
                                 * causes the update_option() to get confused.
 
195
                                 */
 
196
                                add_option( $option, array() );
 
197
                        }
 
198
                }
 
199
        }
 
200
 
 
201
        /**
 
202
         * Ensure that newly-added widgets will appear in the widgets_sidebars.
 
203
         *
 
204
         * This is necessary because the customizer's setting preview filters
 
205
         * are added after the widgets_init action, which is too late for the
 
206
         * widgets to be set up properly.
 
207
         *
 
208
         * @since 3.9.0
 
209
         * @access public
 
210
         *
 
211
         * @param array $sidebars_widgets Associative array of sidebars and their widgets.
 
212
         * @return array Filtered array of sidebars and their widgets.
 
213
         */
 
214
        public function prepreview_added_sidebars_widgets( $sidebars_widgets ) {
 
215
                foreach ( $this->_customized as $setting_id => $value ) {
 
216
                        if ( preg_match( '/^sidebars_widgets\[(.+?)\]$/', $setting_id, $matches ) ) {
 
217
                                $sidebar_id = $matches[1];
 
218
                                $sidebars_widgets[ $sidebar_id ] = $value;
 
219
                        }
 
220
                }
 
221
                return $sidebars_widgets;
 
222
        }
 
223
 
 
224
        /**
 
225
         * Ensure newly-added widgets have empty instances so they
 
226
         * will be recognized.
 
227
         *
 
228
         * This is necessary because the customizer's setting preview
 
229
         * filters are added after the widgets_init action, which is
 
230
         * too late for the widgets to be set up properly.
 
231
         *
 
232
         * @since 3.9.0
 
233
         * @access public
 
234
         *
 
235
         * @param array|bool|mixed $value Widget instance(s), false if open was empty.
 
236
         * @return array|mixed Widget instance(s) with additions.
 
237
         */
 
238
        public function prepreview_added_widget_instance( $value = false ) {
 
239
                if ( ! preg_match( '/^(?:default_)?option_(widget_(.+))/', current_filter(), $matches ) ) {
 
240
                        return $value;
 
241
                }
 
242
                $id_base = $matches[2];
 
243
 
 
244
                foreach ( $this->_customized as $setting_id => $setting ) {
 
245
                        $parsed_setting_id = $this->parse_widget_setting_id( $setting_id );
 
246
                        if ( is_wp_error( $parsed_setting_id ) || $id_base !== $parsed_setting_id['id_base'] ) {
 
247
                                continue;
 
248
                        }
 
249
                        $widget_number = $parsed_setting_id['number'];
 
250
 
 
251
                        if ( is_null( $widget_number ) ) {
 
252
                                // Single widget.
 
253
                                if ( false === $value ) {
 
254
                                        $value = array();
 
255
                                }
 
256
                        } else {
 
257
                                // Multi widget.
 
258
                                if ( empty( $value ) ) {
 
259
                                        $value = array( '_multiwidget' => 1 );
 
260
                                }
 
261
                                if ( ! isset( $value[ $widget_number ] ) ) {
 
262
                                        $value[ $widget_number ] = array();
 
263
                                }
 
264
                        }
 
265
                }
 
266
 
 
267
                return $value;
 
268
        }
 
269
 
 
270
        /**
 
271
         * Remove pre-preview filters.
 
272
         *
 
273
         * Removes filters added in setup_widget_addition_previews()
 
274
         * to ensure widgets are populating the options during
 
275
         * 'widgets_init'.
 
276
         *
 
277
         * @since 3.9.0
 
278
         * @access public
 
279
         */
 
280
        public function remove_prepreview_filters() {
 
281
                foreach ( $this->_prepreview_added_filters as $prepreview_added_filter ) {
 
282
                        remove_filter( $prepreview_added_filter['hook'], $prepreview_added_filter['function'] );
 
283
                }
 
284
                $this->_prepreview_added_filters = array();
 
285
        }
 
286
 
 
287
        /**
 
288
         * Override sidebars_widgets for theme switch.
 
289
         *
 
290
         * When switching a theme via the customizer, supply any previously-configured
 
291
         * sidebars_widgets from the target theme as the initial sidebars_widgets
 
292
         * setting. Also store the old theme's existing settings so that they can
 
293
         * be passed along for storing in the sidebars_widgets theme_mod when the
 
294
         * theme gets switched.
 
295
         *
 
296
         * @since 3.9.0
 
297
         * @access public
 
298
         */
 
299
        public function override_sidebars_widgets_for_theme_switch() {
 
300
                global $sidebars_widgets;
 
301
 
 
302
                if ( $this->manager->doing_ajax() || $this->manager->is_theme_active() ) {
 
303
                        return;
 
304
                }
 
305
 
 
306
                $this->old_sidebars_widgets = wp_get_sidebars_widgets();
 
307
                add_filter( 'customize_value_old_sidebars_widgets_data', array( $this, 'filter_customize_value_old_sidebars_widgets_data' ) );
 
308
 
 
309
                // retrieve_widgets() looks at the global $sidebars_widgets
 
310
                $sidebars_widgets = $this->old_sidebars_widgets;
 
311
                $sidebars_widgets = retrieve_widgets( 'customize' );
 
312
                add_filter( 'option_sidebars_widgets', array( $this, 'filter_option_sidebars_widgets_for_theme_switch' ), 1 );
 
313
        }
 
314
 
 
315
        /**
 
316
         * Filter old_sidebars_widgets_data customizer setting.
 
317
         *
 
318
         * When switching themes, filter the Customizer setting
 
319
         * old_sidebars_widgets_data to supply initial $sidebars_widgets before they
 
320
         * were overridden by retrieve_widgets(). The value for
 
321
         * old_sidebars_widgets_data gets set in the old theme's sidebars_widgets
 
322
         * theme_mod.
 
323
         *
 
324
         * @see WP_Customize_Widgets::handle_theme_switch()
 
325
         * @since 3.9.0
 
326
         * @access public
 
327
         *
 
328
         * @param array $sidebars_widgets
 
329
         */
 
330
        public function filter_customize_value_old_sidebars_widgets_data( $old_sidebars_widgets ) {
 
331
                return $this->old_sidebars_widgets;
 
332
        }
 
333
 
 
334
        /**
 
335
         * Filter sidebars_widgets option for theme switch.
 
336
         *
 
337
         * When switching themes, the retrieve_widgets() function is run when the
 
338
         * Customizer initializes, and then the new sidebars_widgets here get
 
339
         * supplied as the default value for the sidebars_widgets option.
 
340
         *
 
341
         * @see WP_Customize_Widgets::handle_theme_switch()
 
342
         * @since 3.9.0
 
343
         * @access public
 
344
         *
 
345
         * @param array $sidebars_widgets
 
346
         */
 
347
        public function filter_option_sidebars_widgets_for_theme_switch( $sidebars_widgets ) {
 
348
                $sidebars_widgets = $GLOBALS['sidebars_widgets'];
 
349
                $sidebars_widgets['array_version'] = 3;
 
350
                return $sidebars_widgets;
 
351
        }
 
352
 
 
353
        /**
 
354
         * Make sure all widgets get loaded into the Customizer.
 
355
         *
 
356
         * Note: these actions are also fired in wp_ajax_update_widget().
 
357
         *
 
358
         * @since 3.9.0
 
359
         * @access public
 
360
         */
 
361
        public function customize_controls_init() {
 
362
                /** This action is documented in wp-admin/includes/ajax-actions.php */
 
363
                do_action( 'load-widgets.php' );
 
364
 
 
365
                /** This action is documented in wp-admin/includes/ajax-actions.php */
 
366
                do_action( 'widgets.php' );
 
367
 
 
368
                /** This action is documented in wp-admin/widgets.php */
 
369
                do_action( 'sidebar_admin_setup' );
 
370
        }
 
371
 
 
372
        /**
 
373
         * Ensure widgets are available for all types of previews.
 
374
         *
 
375
         * When in preview, hook to 'customize_register' for settings
 
376
         * after WordPress is loaded so that all filters have been
 
377
         * initialized (e.g. Widget Visibility).
 
378
         *
 
379
         * @since 3.9.0
 
380
         * @access public
 
381
         */
 
382
        public function schedule_customize_register() {
 
383
                if ( is_admin() ) { // @todo for some reason, $wp_customize->is_preview() is true here?
 
384
                        $this->customize_register();
 
385
                } else {
 
386
                        add_action( 'wp', array( $this, 'customize_register' ) );
 
387
                }
 
388
        }
 
389
 
 
390
        /**
 
391
         * Register customizer settings and controls for all sidebars and widgets.
 
392
         *
 
393
         * @since 3.9.0
 
394
         * @access public
 
395
         */
 
396
        public function customize_register() {
 
397
                global $wp_registered_widgets, $wp_registered_widget_controls, $wp_registered_sidebars;
 
398
 
 
399
                $sidebars_widgets = array_merge(
 
400
                        array( 'wp_inactive_widgets' => array() ),
 
401
                        array_fill_keys( array_keys( $GLOBALS['wp_registered_sidebars'] ), array() ),
 
402
                        wp_get_sidebars_widgets()
 
403
                );
 
404
 
 
405
                $new_setting_ids = array();
 
406
 
 
407
                /*
 
408
                 * Register a setting for all widgets, including those which are active,
 
409
                 * inactive, and orphaned since a widget may get suppressed from a sidebar
 
410
                 * via a plugin (like Widget Visibility).
 
411
                 */
 
412
                foreach ( array_keys( $wp_registered_widgets ) as $widget_id ) {
 
413
                        $setting_id   = $this->get_setting_id( $widget_id );
 
414
                        $setting_args = $this->get_setting_args( $setting_id );
 
415
 
 
416
                        $setting_args['sanitize_callback']    = array( $this, 'sanitize_widget_instance' );
 
417
                        $setting_args['sanitize_js_callback'] = array( $this, 'sanitize_widget_js_instance' );
 
418
 
 
419
                        $this->manager->add_setting( $setting_id, $setting_args );
 
420
 
 
421
                        $new_setting_ids[] = $setting_id;
 
422
                }
 
423
 
 
424
                /*
 
425
                 * Add a setting which will be supplied for the theme's sidebars_widgets
 
426
                 * theme_mod when the the theme is switched.
 
427
                 */
 
428
                if ( ! $this->manager->is_theme_active() ) {
 
429
                        $setting_id = 'old_sidebars_widgets_data';
 
430
                        $setting_args = $this->get_setting_args( $setting_id, array(
 
431
                                'type' => 'global_variable',
 
432
                        ) );
 
433
                        $this->manager->add_setting( $setting_id, $setting_args );
 
434
                }
 
435
 
 
436
                $this->manager->add_panel( 'widgets', array(
 
437
                        'title'       => __( 'Widgets' ),
 
438
                        'description' => __( 'Widgets are independent sections of content that can be placed into widgetized areas provided by your theme (commonly called sidebars).' ),
 
439
                        'priority'    => 110,
 
440
                ) );
 
441
 
 
442
                foreach ( $sidebars_widgets as $sidebar_id => $sidebar_widget_ids ) {
 
443
                        if ( empty( $sidebar_widget_ids ) ) {
 
444
                                $sidebar_widget_ids = array();
 
445
                        }
 
446
 
 
447
                        $is_registered_sidebar = isset( $GLOBALS['wp_registered_sidebars'][$sidebar_id] );
 
448
                        $is_inactive_widgets   = ( 'wp_inactive_widgets' === $sidebar_id );
 
449
                        $is_active_sidebar     = ( $is_registered_sidebar && ! $is_inactive_widgets );
 
450
 
 
451
                        // Add setting for managing the sidebar's widgets.
 
452
                        if ( $is_registered_sidebar || $is_inactive_widgets ) {
 
453
                                $setting_id   = sprintf( 'sidebars_widgets[%s]', $sidebar_id );
 
454
                                $setting_args = $this->get_setting_args( $setting_id );
 
455
 
 
456
                                $setting_args['sanitize_callback']    = array( $this, 'sanitize_sidebar_widgets' );
 
457
                                $setting_args['sanitize_js_callback'] = array( $this, 'sanitize_sidebar_widgets_js_instance' );
 
458
 
 
459
                                $this->manager->add_setting( $setting_id, $setting_args );
 
460
                                $new_setting_ids[] = $setting_id;
 
461
 
 
462
                                // Add section to contain controls.
 
463
                                $section_id = sprintf( 'sidebar-widgets-%s', $sidebar_id );
 
464
                                if ( $is_active_sidebar ) {
 
465
 
 
466
                                        $section_args = array(
 
467
                                                'title' => $GLOBALS['wp_registered_sidebars'][ $sidebar_id ]['name'],
 
468
                                                'description' => $GLOBALS['wp_registered_sidebars'][ $sidebar_id ]['description'],
 
469
                                                'priority' => array_search( $sidebar_id, array_keys( $wp_registered_sidebars ) ),
 
470
                                                'panel' => 'widgets',
 
471
                                        );
 
472
 
 
473
                                        /**
 
474
                                         * Filter Customizer widget section arguments for a given sidebar.
 
475
                                         *
 
476
                                         * @since 3.9.0
 
477
                                         *
 
478
                                         * @param array      $section_args Array of Customizer widget section arguments.
 
479
                                         * @param string     $section_id   Customizer section ID.
 
480
                                         * @param int|string $sidebar_id   Sidebar ID.
 
481
                                         */
 
482
                                        $section_args = apply_filters( 'customizer_widgets_section_args', $section_args, $section_id, $sidebar_id );
 
483
 
 
484
                                        $this->manager->add_section( $section_id, $section_args );
 
485
 
 
486
                                        $control = new WP_Widget_Area_Customize_Control( $this->manager, $setting_id, array(
 
487
                                                'section'    => $section_id,
 
488
                                                'sidebar_id' => $sidebar_id,
 
489
                                                'priority'   => count( $sidebar_widget_ids ), // place 'Add Widget' and 'Reorder' buttons at end.
 
490
                                        ) );
 
491
                                        $new_setting_ids[] = $setting_id;
 
492
 
 
493
                                        $this->manager->add_control( $control );
 
494
                                }
 
495
                        }
 
496
 
 
497
                        // Add a control for each active widget (located in a sidebar).
 
498
                        foreach ( $sidebar_widget_ids as $i => $widget_id ) {
 
499
 
 
500
                                // Skip widgets that may have gone away due to a plugin being deactivated.
 
501
                                if ( ! $is_active_sidebar || ! isset( $GLOBALS['wp_registered_widgets'][$widget_id] ) ) {
 
502
                                        continue;
 
503
                                }
 
504
 
 
505
                                $registered_widget = $GLOBALS['wp_registered_widgets'][$widget_id];
 
506
                                $setting_id        = $this->get_setting_id( $widget_id );
 
507
                                $id_base           = $GLOBALS['wp_registered_widget_controls'][$widget_id]['id_base'];
 
508
 
 
509
                                $control = new WP_Widget_Form_Customize_Control( $this->manager, $setting_id, array(
 
510
                                        'label'          => $registered_widget['name'],
 
511
                                        'section'        => $section_id,
 
512
                                        'sidebar_id'     => $sidebar_id,
 
513
                                        'widget_id'      => $widget_id,
 
514
                                        'widget_id_base' => $id_base,
 
515
                                        'priority'       => $i,
 
516
                                        'width'          => $wp_registered_widget_controls[$widget_id]['width'],
 
517
                                        'height'         => $wp_registered_widget_controls[$widget_id]['height'],
 
518
                                        'is_wide'        => $this->is_wide_widget( $widget_id ),
 
519
                                ) );
 
520
                                $this->manager->add_control( $control );
 
521
                        }
 
522
                }
 
523
 
 
524
                /*
 
525
                 * We have to register these settings later than customize_preview_init
 
526
                 * so that other filters have had a chance to run.
 
527
                 */
 
528
                if ( did_action( 'customize_preview_init' ) ) {
 
529
                        foreach ( $new_setting_ids as $new_setting_id ) {
 
530
                                $this->manager->get_setting( $new_setting_id )->preview();
 
531
                        }
 
532
                }
 
533
                $this->remove_prepreview_filters();
 
534
        }
 
535
 
 
536
        /**
 
537
         * Covert a widget_id into its corresponding customizer setting ID (option name).
 
538
         *
 
539
         * @since 3.9.0
 
540
         * @access public
 
541
         *
 
542
         * @param string $widget_id Widget ID.
 
543
         * @return string Maybe-parsed widget ID.
 
544
         */
 
545
        public function get_setting_id( $widget_id ) {
 
546
                $parsed_widget_id = $this->parse_widget_id( $widget_id );
 
547
                $setting_id       = sprintf( 'widget_%s', $parsed_widget_id['id_base'] );
 
548
 
 
549
                if ( ! is_null( $parsed_widget_id['number'] ) ) {
 
550
                        $setting_id .= sprintf( '[%d]', $parsed_widget_id['number'] );
 
551
                }
 
552
                return $setting_id;
 
553
        }
 
554
 
 
555
        /**
 
556
         * Determine whether the widget is considered "wide".
 
557
         *
 
558
         * Core widgets which may have controls wider than 250, but can
 
559
         * still be shown in the narrow customizer panel. The RSS and Text
 
560
         * widgets in Core, for example, have widths of 400 and yet they
 
561
         * still render fine in the customizer panel. This method will
 
562
         * return all Core widgets as being not wide, but this can be
 
563
         * overridden with the is_wide_widget_in_customizer filter.
 
564
         *
 
565
         * @since 3.9.0
 
566
         * @access public
 
567
         *
 
568
         * @param string $widget_id Widget ID.
 
569
         * @return bool Whether or not the widget is a "wide" widget.
 
570
         */
 
571
        public function is_wide_widget( $widget_id ) {
 
572
                global $wp_registered_widget_controls;
 
573
 
 
574
                $parsed_widget_id = $this->parse_widget_id( $widget_id );
 
575
                $width            = $wp_registered_widget_controls[$widget_id]['width'];
 
576
                $is_core          = in_array( $parsed_widget_id['id_base'], $this->core_widget_id_bases );
 
577
                $is_wide          = ( $width > 250 && ! $is_core );
 
578
 
 
579
                /**
 
580
                 * Filter whether the given widget is considered "wide".
 
581
                 *
 
582
                 * @since 3.9.0
 
583
                 *
 
584
                 * @param bool   $is_wide   Whether the widget is wide, Default false.
 
585
                 * @param string $widget_id Widget ID.
 
586
                 */
 
587
                return apply_filters( 'is_wide_widget_in_customizer', $is_wide, $widget_id );
 
588
        }
 
589
 
 
590
        /**
 
591
         * Covert a widget ID into its id_base and number components.
 
592
         *
 
593
         * @since 3.9.0
 
594
         * @access public
 
595
         *
 
596
         * @param string $widget_id Widget ID.
 
597
         * @return array Array containing a widget's id_base and number components.
 
598
         */
 
599
        public function parse_widget_id( $widget_id ) {
 
600
                $parsed = array(
 
601
                        'number' => null,
 
602
                        'id_base' => null,
 
603
                );
 
604
 
 
605
                if ( preg_match( '/^(.+)-(\d+)$/', $widget_id, $matches ) ) {
 
606
                        $parsed['id_base'] = $matches[1];
 
607
                        $parsed['number']  = intval( $matches[2] );
 
608
                } else {
 
609
                        // likely an old single widget
 
610
                        $parsed['id_base'] = $widget_id;
 
611
                }
 
612
                return $parsed;
 
613
        }
 
614
 
 
615
        /**
 
616
         * Convert a widget setting ID (option path) to its id_base and number components.
 
617
         *
 
618
         * @since 3.9.0
 
619
         * @access public
 
620
         *
 
621
         * @param string $setting_id Widget setting ID.
 
622
         * @return WP_Error|array Array containing a widget's id_base and number components,
 
623
         *                        or a WP_Error object.
 
624
         */
 
625
        public function parse_widget_setting_id( $setting_id ) {
 
626
                if ( ! preg_match( '/^(widget_(.+?))(?:\[(\d+)\])?$/', $setting_id, $matches ) ) {
 
627
                        return new WP_Error( 'widget_setting_invalid_id' );
 
628
                }
 
629
 
 
630
                $id_base = $matches[2];
 
631
                $number  = isset( $matches[3] ) ? intval( $matches[3] ) : null;
 
632
 
 
633
                return compact( 'id_base', 'number' );
 
634
        }
 
635
 
 
636
        /**
 
637
         * Call admin_print_styles-widgets.php and admin_print_styles hooks to
 
638
         * allow custom styles from plugins.
 
639
         *
 
640
         * @since 3.9.0
 
641
         * @access public
 
642
         */
 
643
        public function print_styles() {
 
644
                /** This action is documented in wp-admin/admin-header.php */
 
645
                do_action( 'admin_print_styles-widgets.php' );
 
646
 
 
647
                /** This action is documented in wp-admin/admin-header.php */
 
648
                do_action( 'admin_print_styles' );
 
649
        }
 
650
 
 
651
        /**
 
652
         * Call admin_print_scripts-widgets.php and admin_print_scripts hooks to
 
653
         * allow custom scripts from plugins.
 
654
         *
 
655
         * @since 3.9.0
 
656
         * @access public
 
657
         */
 
658
        public function print_scripts() {
 
659
                /** This action is documented in wp-admin/admin-header.php */
 
660
                do_action( 'admin_print_scripts-widgets.php' );
 
661
 
 
662
                /** This action is documented in wp-admin/admin-header.php */
 
663
                do_action( 'admin_print_scripts' );
 
664
        }
 
665
 
 
666
        /**
 
667
         * Enqueue scripts and styles for customizer panel and export data to JavaScript.
 
668
         *
 
669
         * @since 3.9.0
 
670
         * @access public
 
671
         */
 
672
        public function enqueue_scripts() {
 
673
                wp_enqueue_style( 'customize-widgets' );
 
674
                wp_enqueue_script( 'customize-widgets' );
 
675
 
 
676
                /** This action is documented in wp-admin/admin-header.php */
 
677
                do_action( 'admin_enqueue_scripts', 'widgets.php' );
 
678
 
 
679
                /*
 
680
                 * Export available widgets with control_tpl removed from model
 
681
                 * since plugins need templates to be in the DOM.
 
682
                 */
 
683
                $available_widgets = array();
 
684
 
 
685
                foreach ( $this->get_available_widgets() as $available_widget ) {
 
686
                        unset( $available_widget['control_tpl'] );
 
687
                        $available_widgets[] = $available_widget;
 
688
                }
 
689
 
 
690
                $widget_reorder_nav_tpl = sprintf(
 
691
                        '<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>',
 
692
                        __( 'Move to another area&hellip;' ),
 
693
                        __( 'Move down' ),
 
694
                        __( 'Move up' )
 
695
                );
 
696
 
 
697
                $move_widget_area_tpl = str_replace(
 
698
                        array( '{description}', '{btn}' ),
 
699
                        array(
 
700
                                __( 'Select an area to move this widget into:' ),
 
701
                                _x( 'Move', 'Move widget' ),
 
702
                        ),
 
703
                        '<div class="move-widget-area">
 
704
                                <p class="description">{description}</p>
 
705
                                <ul class="widget-area-select">
 
706
                                        <% _.each( sidebars, function ( sidebar ){ %>
 
707
                                                <li class="" data-id="<%- sidebar.id %>" title="<%- sidebar.description %>" tabindex="0"><%- sidebar.name %></li>
 
708
                                        <% }); %>
 
709
                                </ul>
 
710
                                <div class="move-widget-actions">
 
711
                                        <button class="move-widget-btn button-secondary" type="button">{btn}</button>
 
712
                                </div>
 
713
                        </div>'
 
714
                );
 
715
 
 
716
                global $wp_scripts;
 
717
 
 
718
                $settings = array(
 
719
                        'nonce'                => wp_create_nonce( 'update-widget' ),
 
720
                        'registeredSidebars'   => array_values( $GLOBALS['wp_registered_sidebars'] ),
 
721
                        'registeredWidgets'    => $GLOBALS['wp_registered_widgets'],
 
722
                        'availableWidgets'     => $available_widgets, // @todo Merge this with registered_widgets
 
723
                        'l10n' => array(
 
724
                                'saveBtnLabel'     => __( 'Apply' ),
 
725
                                'saveBtnTooltip'   => __( 'Save and preview changes before publishing them.' ),
 
726
                                'removeBtnLabel'   => __( 'Remove' ),
 
727
                                'removeBtnTooltip' => __( 'Trash widget by moving it to the inactive widgets sidebar.' ),
 
728
                                'error'            => __( 'An error has occurred. Please reload the page and try again.' ),
 
729
                        ),
 
730
                        'tpl' => array(
 
731
                                'widgetReorderNav' => $widget_reorder_nav_tpl,
 
732
                                'moveWidgetArea'   => $move_widget_area_tpl,
 
733
                        ),
 
734
                );
 
735
 
 
736
                foreach ( $settings['registeredWidgets'] as &$registered_widget ) {
 
737
                        unset( $registered_widget['callback'] ); // may not be JSON-serializeable
 
738
                }
 
739
 
 
740
                $wp_scripts->add_data(
 
741
                        'customize-widgets',
 
742
                        'data',
 
743
                        sprintf( 'var _wpCustomizeWidgetsSettings = %s;', json_encode( $settings ) )
 
744
                );
 
745
        }
 
746
 
 
747
        /**
 
748
         * Render the widget form control templates into the DOM.
 
749
         *
 
750
         * @since 3.9.0
 
751
         * @access public
 
752
         */
 
753
        public function output_widget_control_templates() {
 
754
                ?>
 
755
                <div id="widgets-left"><!-- compatibility with JS which looks for widget templates here -->
 
756
                <div id="available-widgets">
 
757
                        <div id="available-widgets-filter">
 
758
                                <label class="screen-reader-text" for="widgets-search"><?php _e( 'Search Widgets' ); ?></label>
 
759
                                <input type="search" id="widgets-search" placeholder="<?php esc_attr_e( 'Search widgets&hellip;' ) ?>" />
 
760
                        </div>
 
761
                        <?php foreach ( $this->get_available_widgets() as $available_widget ): ?>
 
762
                                <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">
 
763
                                        <?php echo $available_widget['control_tpl']; ?>
 
764
                                </div>
 
765
                        <?php endforeach; ?>
 
766
                </div><!-- #available-widgets -->
 
767
                </div><!-- #widgets-left -->
 
768
                <?php
 
769
        }
 
770
 
 
771
        /**
 
772
         * Call admin_print_footer_scripts and admin_print_scripts hooks to
 
773
         * allow custom scripts from plugins.
 
774
         *
 
775
         * @since 3.9.0
 
776
         * @access public
 
777
         */
 
778
        public function print_footer_scripts() {
 
779
                /** This action is documented in wp-admin/admin-footer.php */
 
780
                do_action( 'admin_print_footer_scripts' );
 
781
 
 
782
                /** This action is documented in wp-admin/admin-footer.php */
 
783
                do_action( 'admin_footer-widgets.php' );
 
784
        }
 
785
 
 
786
        /**
 
787
         * Get common arguments to supply when constructing a Customizer setting.
 
788
         *
 
789
         * @since 3.9.0
 
790
         * @access public
 
791
         *
 
792
         * @param string $id        Widget setting ID.
 
793
         * @param array  $overrides Array of setting overrides.
 
794
         * @return array Possibly modified setting arguments.
 
795
         */
 
796
        public function get_setting_args( $id, $overrides = array() ) {
 
797
                $args = array(
 
798
                        'type'       => 'option',
 
799
                        'capability' => 'edit_theme_options',
 
800
                        'transport'  => 'refresh',
 
801
                        'default'    => array(),
 
802
                );
 
803
                $args = array_merge( $args, $overrides );
 
804
 
 
805
                /**
 
806
                 * Filter the common arguments supplied when constructing a Customizer setting.
 
807
                 *
 
808
                 * @since 3.9.0
 
809
                 *
 
810
                 * @see WP_Customize_Setting
 
811
                 *
 
812
                 * @param array  $args Array of Customizer setting arguments.
 
813
                 * @param string $id   Widget setting ID.
 
814
                 */
 
815
                return apply_filters( 'widget_customizer_setting_args', $args, $id );
 
816
        }
 
817
 
 
818
        /**
 
819
         * Make sure that sidebar widget arrays only ever contain widget IDS.
 
820
         *
 
821
         * Used as the 'sanitize_callback' for each $sidebars_widgets setting.
 
822
         *
 
823
         * @since 3.9.0
 
824
         * @access public
 
825
         *
 
826
         * @param array $widget_ids Array of widget IDs.
 
827
         * @return array Array of sanitized widget IDs.
 
828
         */
 
829
        public function sanitize_sidebar_widgets( $widget_ids ) {
 
830
                global $wp_registered_widgets;
 
831
 
 
832
                $widget_ids           = array_map( 'strval', (array) $widget_ids );
 
833
                $sanitized_widget_ids = array();
 
834
 
 
835
                foreach ( $widget_ids as $widget_id ) {
 
836
                        if ( array_key_exists( $widget_id, $wp_registered_widgets ) ) {
 
837
                                $sanitized_widget_ids[] = $widget_id;
 
838
                        }
 
839
                }
 
840
                return $sanitized_widget_ids;
 
841
        }
 
842
 
 
843
        /**
 
844
         * Build up an index of all available widgets for use in Backbone models.
 
845
         *
 
846
         * @since 3.9.0
 
847
         * @access public
 
848
         *
 
849
         * @see wp_list_widgets()
 
850
         *
 
851
         * @return array List of available widgets.
 
852
         */
 
853
        public function get_available_widgets() {
 
854
                static $available_widgets = array();
 
855
                if ( ! empty( $available_widgets ) ) {
 
856
                        return $available_widgets;
 
857
                }
 
858
 
 
859
                global $wp_registered_widgets, $wp_registered_widget_controls;
 
860
                require_once ABSPATH . '/wp-admin/includes/widgets.php'; // for next_widget_id_number()
 
861
 
 
862
                $sort = $wp_registered_widgets;
 
863
                usort( $sort, array( $this, '_sort_name_callback' ) );
 
864
                $done = array();
 
865
 
 
866
                foreach ( $sort as $widget ) {
 
867
                        if ( in_array( $widget['callback'], $done, true ) ) { // We already showed this multi-widget
 
868
                                continue;
 
869
                        }
 
870
 
 
871
                        $sidebar = is_active_widget( $widget['callback'], $widget['id'], false, false );
 
872
                        $done[]  = $widget['callback'];
 
873
 
 
874
                        if ( ! isset( $widget['params'][0] ) ) {
 
875
                                $widget['params'][0] = array();
 
876
                        }
 
877
 
 
878
                        $available_widget = $widget;
 
879
                        unset( $available_widget['callback'] ); // not serializable to JSON
 
880
 
 
881
                        $args = array(
 
882
                                'widget_id'   => $widget['id'],
 
883
                                'widget_name' => $widget['name'],
 
884
                                '_display'    => 'template',
 
885
                        );
 
886
 
 
887
                        $is_disabled     = false;
 
888
                        $is_multi_widget = ( isset( $wp_registered_widget_controls[$widget['id']]['id_base'] ) && isset( $widget['params'][0]['number'] ) );
 
889
                        if ( $is_multi_widget ) {
 
890
                                $id_base            = $wp_registered_widget_controls[$widget['id']]['id_base'];
 
891
                                $args['_temp_id']   = "$id_base-__i__";
 
892
                                $args['_multi_num'] = next_widget_id_number( $id_base );
 
893
                                $args['_add']       = 'multi';
 
894
                        } else {
 
895
                                $args['_add'] = 'single';
 
896
 
 
897
                                if ( $sidebar && 'wp_inactive_widgets' !== $sidebar ) {
 
898
                                        $is_disabled = true;
 
899
                                }
 
900
                                $id_base = $widget['id'];
 
901
                        }
 
902
 
 
903
                        $list_widget_controls_args = wp_list_widget_controls_dynamic_sidebar( array( 0 => $args, 1 => $widget['params'][0] ) );
 
904
                        $control_tpl = $this->get_widget_control( $list_widget_controls_args );
 
905
 
 
906
                        // The properties here are mapped to the Backbone Widget model.
 
907
                        $available_widget = array_merge( $available_widget, array(
 
908
                                'temp_id'      => isset( $args['_temp_id'] ) ? $args['_temp_id'] : null,
 
909
                                'is_multi'     => $is_multi_widget,
 
910
                                'control_tpl'  => $control_tpl,
 
911
                                'multi_number' => ( $args['_add'] === 'multi' ) ? $args['_multi_num'] : false,
 
912
                                'is_disabled'  => $is_disabled,
 
913
                                'id_base'      => $id_base,
 
914
                                'transport'    => 'refresh',
 
915
                                'width'        => $wp_registered_widget_controls[$widget['id']]['width'],
 
916
                                'height'       => $wp_registered_widget_controls[$widget['id']]['height'],
 
917
                                'is_wide'      => $this->is_wide_widget( $widget['id'] ),
 
918
                        ) );
 
919
 
 
920
                        $available_widgets[] = $available_widget;
 
921
                }
 
922
 
 
923
                return $available_widgets;
 
924
        }
 
925
 
 
926
        /**
 
927
         * Naturally order available widgets by name.
 
928
         *
 
929
         * @since 3.9.0
 
930
         * @static
 
931
         * @access protected
 
932
         *
 
933
         * @param array $widget_a The first widget to compare.
 
934
         * @param array $widget_b The second widget to compare.
 
935
         * @return int Reorder position for the current widget comparison.
 
936
         */
 
937
        protected function _sort_name_callback( $widget_a, $widget_b ) {
 
938
                return strnatcasecmp( $widget_a['name'], $widget_b['name'] );
 
939
        }
 
940
 
 
941
        /**
 
942
         * Get the widget control markup.
 
943
         *
 
944
         * @since 3.9.0
 
945
         * @access public
 
946
         *
 
947
         * @param array $args Widget control arguments.
 
948
         * @return string Widget control form HTML markup.
 
949
         */
 
950
        public function get_widget_control( $args ) {
 
951
                ob_start();
 
952
 
 
953
                call_user_func_array( 'wp_widget_control', $args );
 
954
                $replacements = array(
 
955
                        '<form action="" method="post">' => '<div class="form">',
 
956
                        '</form>' => '</div><!-- .form -->',
 
957
                );
 
958
 
 
959
                $control_tpl = ob_get_clean();
 
960
 
 
961
                $control_tpl = str_replace( array_keys( $replacements ), array_values( $replacements ), $control_tpl );
 
962
 
 
963
                return $control_tpl;
 
964
        }
 
965
 
 
966
        /**
 
967
         * Add hooks for the customizer preview.
 
968
         *
 
969
         * @since 3.9.0
 
970
         * @access public
 
971
         */
 
972
        public function customize_preview_init() {
 
973
                add_filter( 'sidebars_widgets',   array( $this, 'preview_sidebars_widgets' ), 1 );
 
974
                add_action( 'wp_enqueue_scripts', array( $this, 'customize_preview_enqueue' ) );
 
975
                add_action( 'wp_print_styles',    array( $this, 'print_preview_css' ), 1 );
 
976
                add_action( 'wp_footer',          array( $this, 'export_preview_data' ), 20 );
 
977
        }
 
978
 
 
979
        /**
 
980
         * When previewing, make sure the proper previewing widgets are used.
 
981
         *
 
982
         * Because wp_get_sidebars_widgets() gets called early at init
 
983
         * (via wp_convert_widget_settings()) and can set global variable
 
984
         * $_wp_sidebars_widgets to the value of get_option( 'sidebars_widgets' )
 
985
         * before the customizer preview filter is added, we have to reset
 
986
         * it after the filter has been added.
 
987
         *
 
988
         * @since 3.9.0
 
989
         * @access public
 
990
         *
 
991
         * @param array $sidebars_widgets List of widgets for the current sidebar.
 
992
         */
 
993
        public function preview_sidebars_widgets( $sidebars_widgets ) {
 
994
                $sidebars_widgets = get_option( 'sidebars_widgets' );
 
995
 
 
996
                unset( $sidebars_widgets['array_version'] );
 
997
                return $sidebars_widgets;
 
998
        }
 
999
 
 
1000
        /**
 
1001
         * Enqueue scripts for the Customizer preview.
 
1002
         *
 
1003
         * @since 3.9.0
 
1004
         * @access public
 
1005
         */
 
1006
        public function customize_preview_enqueue() {
 
1007
                wp_enqueue_script( 'customize-preview-widgets' );
 
1008
        }
 
1009
 
 
1010
        /**
 
1011
         * Insert default style for highlighted widget at early point so theme
 
1012
         * stylesheet can override.
 
1013
         *
 
1014
         * @since 3.9.0
 
1015
         * @access public
 
1016
         *
 
1017
         * @action wp_print_styles
 
1018
         */
 
1019
        public function print_preview_css() {
 
1020
                ?>
 
1021
                <style>
 
1022
                .widget-customizer-highlighted-widget {
 
1023
                        outline: none;
 
1024
                        -webkit-box-shadow: 0 0 2px rgba(30,140,190,0.8);
 
1025
                        box-shadow: 0 0 2px rgba(30,140,190,0.8);
 
1026
                        position: relative;
 
1027
                        z-index: 1;
 
1028
                }
 
1029
                </style>
 
1030
                <?php
 
1031
        }
 
1032
 
 
1033
        /**
 
1034
         * At the very end of the page, at the very end of the wp_footer,
 
1035
         * communicate the sidebars that appeared on the page.
 
1036
         *
 
1037
         * @since 3.9.0
 
1038
         * @access public
 
1039
         */
 
1040
        public function export_preview_data() {
 
1041
 
 
1042
                // Prepare customizer settings to pass to Javascript.
 
1043
                $settings = array(
 
1044
                        'renderedSidebars'   => array_fill_keys( array_unique( $this->rendered_sidebars ), true ),
 
1045
                        'renderedWidgets'    => array_fill_keys( array_keys( $this->rendered_widgets ), true ),
 
1046
                        'registeredSidebars' => array_values( $GLOBALS['wp_registered_sidebars'] ),
 
1047
                        'registeredWidgets'  => $GLOBALS['wp_registered_widgets'],
 
1048
                        'l10n'               => array(
 
1049
                                'widgetTooltip' => __( 'Shift-click to edit this widget.' ),
 
1050
                        ),
 
1051
                );
 
1052
                foreach ( $settings['registeredWidgets'] as &$registered_widget ) {
 
1053
                        unset( $registered_widget['callback'] ); // may not be JSON-serializeable
 
1054
                }
 
1055
 
 
1056
                ?>
 
1057
                <script type="text/javascript">
 
1058
                        var _wpWidgetCustomizerPreviewSettings = <?php echo json_encode( $settings ); ?>;
 
1059
                </script>
 
1060
                <?php
 
1061
        }
 
1062
 
 
1063
        /**
 
1064
         * Keep track of the widgets that were rendered.
 
1065
         *
 
1066
         * @since 3.9.0
 
1067
         * @access public
 
1068
         *
 
1069
         * @param array $widget Rendered widget to tally.
 
1070
         */
 
1071
        public function tally_rendered_widgets( $widget ) {
 
1072
                $this->rendered_widgets[ $widget['id'] ] = true;
 
1073
        }
 
1074
 
 
1075
        /**
 
1076
         * Determine if a widget is rendered on the page.
 
1077
         *
 
1078
         * @since 4.0.0
 
1079
         * @access public
 
1080
         *
 
1081
         * @param string $widget_id Widget ID to check.
 
1082
         * @return bool Whether the widget is rendered.
 
1083
         */
 
1084
        public function is_widget_rendered( $widget_id ) {
 
1085
                return in_array( $widget_id, $this->rendered_widgets );
 
1086
        }
 
1087
 
 
1088
        /**
 
1089
         * Determine if a sidebar is rendered on the page.
 
1090
         *
 
1091
         * @since 4.0.0
 
1092
         * @access public
 
1093
         *
 
1094
         * @param string $sidebar_id Sidebar ID to check.
 
1095
         * @return bool Whether the sidebar is rendered.
 
1096
         */
 
1097
        public function is_sidebar_rendered( $sidebar_id ) {
 
1098
                return in_array( $sidebar_id, $this->rendered_sidebars );
 
1099
        }
 
1100
 
 
1101
        /**
 
1102
         * Tally the sidebars rendered via is_active_sidebar().
 
1103
         *
 
1104
         * Keep track of the times that is_active_sidebar() is called
 
1105
         * in the template, and assume that this means that the sidebar
 
1106
         * would be rendered on the template if there were widgets
 
1107
         * populating it.
 
1108
         *
 
1109
         * @since 3.9.0
 
1110
         * @access public
 
1111
         *
 
1112
         * @param bool   $is_active  Whether the sidebar is active.
 
1113
         * @param string $sidebar_id Sidebar ID.
 
1114
         */
 
1115
        public function tally_sidebars_via_is_active_sidebar_calls( $is_active, $sidebar_id ) {
 
1116
                if ( isset( $GLOBALS['wp_registered_sidebars'][$sidebar_id] ) ) {
 
1117
                        $this->rendered_sidebars[] = $sidebar_id;
 
1118
                }
 
1119
                /*
 
1120
                 * We may need to force this to true, and also force-true the value
 
1121
                 * for 'dynamic_sidebar_has_widgets' if we want to ensure that there
 
1122
                 * is an area to drop widgets into, if the sidebar is empty.
 
1123
                 */
 
1124
                return $is_active;
 
1125
        }
 
1126
 
 
1127
        /**
 
1128
         * Tally the sidebars rendered via dynamic_sidebar().
 
1129
         *
 
1130
         * Keep track of the times that dynamic_sidebar() is called in the template,
 
1131
         * and assume this means the sidebar would be rendered on the template if
 
1132
         * there were widgets populating it.
 
1133
         *
 
1134
         * @since 3.9.0
 
1135
         * @access public
 
1136
         *
 
1137
         * @param bool   $has_widgets Whether the current sidebar has widgets.
 
1138
         * @param string $sidebar_id  Sidebar ID.
 
1139
         */
 
1140
        public function tally_sidebars_via_dynamic_sidebar_calls( $has_widgets, $sidebar_id ) {
 
1141
                if ( isset( $GLOBALS['wp_registered_sidebars'][$sidebar_id] ) ) {
 
1142
                        $this->rendered_sidebars[] = $sidebar_id;
 
1143
                }
 
1144
 
 
1145
                /*
 
1146
                 * We may need to force this to true, and also force-true the value
 
1147
                 * for 'is_active_sidebar' if we want to ensure there is an area to
 
1148
                 * drop widgets into, if the sidebar is empty.
 
1149
                 */
 
1150
                return $has_widgets;
 
1151
        }
 
1152
 
 
1153
        /**
 
1154
         * Get MAC for a serialized widget instance string.
 
1155
         *
 
1156
         * Allows values posted back from JS to be rejected if any tampering of the
 
1157
         * data has occurred.
 
1158
         *
 
1159
         * @since 3.9.0
 
1160
         * @access protected
 
1161
         *
 
1162
         * @param string $serialized_instance Widget instance.
 
1163
         * @return string MAC for serialized widget instance.
 
1164
         */
 
1165
        protected function get_instance_hash_key( $serialized_instance ) {
 
1166
                return wp_hash( $serialized_instance );
 
1167
        }
 
1168
 
 
1169
        /**
 
1170
         * Sanitize a widget instance.
 
1171
         *
 
1172
         * Unserialize the JS-instance for storing in the options. It's important
 
1173
         * that this filter only get applied to an instance once.
 
1174
         *
 
1175
         * @since 3.9.0
 
1176
         * @access public
 
1177
         *
 
1178
         * @param array $value Widget instance to sanitize.
 
1179
         * @return array Sanitized widget instance.
 
1180
         */
 
1181
        public function sanitize_widget_instance( $value ) {
 
1182
                if ( $value === array() ) {
 
1183
                        return $value;
 
1184
                }
 
1185
 
 
1186
                if ( empty( $value['is_widget_customizer_js_value'] )
 
1187
                        || empty( $value['instance_hash_key'] )
 
1188
                        || empty( $value['encoded_serialized_instance'] ) )
 
1189
                {
 
1190
                        return null;
 
1191
                }
 
1192
 
 
1193
                $decoded = base64_decode( $value['encoded_serialized_instance'], true );
 
1194
                if ( false === $decoded ) {
 
1195
                        return null;
 
1196
                }
 
1197
 
 
1198
                if ( $this->get_instance_hash_key( $decoded ) !== $value['instance_hash_key'] ) {
 
1199
                        return null;
 
1200
                }
 
1201
 
 
1202
                $instance = unserialize( $decoded );
 
1203
                if ( false === $instance ) {
 
1204
                        return null;
 
1205
                }
 
1206
 
 
1207
                return $instance;
 
1208
        }
 
1209
 
 
1210
        /**
 
1211
         * Convert widget instance into JSON-representable format.
 
1212
         *
 
1213
         * @since 3.9.0
 
1214
         * @access public
 
1215
         *
 
1216
         * @param array $value Widget instance to convert to JSON.
 
1217
         * @return array JSON-converted widget instance.
 
1218
         */
 
1219
        public function sanitize_widget_js_instance( $value ) {
 
1220
                if ( empty( $value['is_widget_customizer_js_value'] ) ) {
 
1221
                        $serialized = serialize( $value );
 
1222
 
 
1223
                        $value = array(
 
1224
                                'encoded_serialized_instance'   => base64_encode( $serialized ),
 
1225
                                'title'                         => empty( $value['title'] ) ? '' : $value['title'],
 
1226
                                'is_widget_customizer_js_value' => true,
 
1227
                                'instance_hash_key'             => $this->get_instance_hash_key( $serialized ),
 
1228
                        );
 
1229
                }
 
1230
                return $value;
 
1231
        }
 
1232
 
 
1233
        /**
 
1234
         * Strip out widget IDs for widgets which are no longer registered.
 
1235
         *
 
1236
         * One example where this might happen is when a plugin orphans a widget
 
1237
         * in a sidebar upon deactivation.
 
1238
         *
 
1239
         * @since 3.9.0
 
1240
         * @access public
 
1241
         *
 
1242
         * @param array $widget_ids List of widget IDs.
 
1243
         * @return array Parsed list of widget IDs.
 
1244
         */
 
1245
        public function sanitize_sidebar_widgets_js_instance( $widget_ids ) {
 
1246
                global $wp_registered_widgets;
 
1247
                $widget_ids = array_values( array_intersect( $widget_ids, array_keys( $wp_registered_widgets ) ) );
 
1248
                return $widget_ids;
 
1249
        }
 
1250
 
 
1251
        /**
 
1252
         * Find and invoke the widget update and control callbacks.
 
1253
         *
 
1254
         * Requires that $_POST be populated with the instance data.
 
1255
         *
 
1256
         * @since 3.9.0
 
1257
         * @access public
 
1258
         *
 
1259
         * @param  string $widget_id Widget ID.
 
1260
         * @return WP_Error|array Array containing the updated widget information.
 
1261
         *                        A WP_Error object, otherwise.
 
1262
         */
 
1263
        public function call_widget_update( $widget_id ) {
 
1264
                global $wp_registered_widget_updates, $wp_registered_widget_controls;
 
1265
 
 
1266
                $this->start_capturing_option_updates();
 
1267
                $parsed_id   = $this->parse_widget_id( $widget_id );
 
1268
                $option_name = 'widget_' . $parsed_id['id_base'];
 
1269
 
 
1270
                /*
 
1271
                 * If a previously-sanitized instance is provided, populate the input vars
 
1272
                 * with its values so that the widget update callback will read this instance
 
1273
                 */
 
1274
                $added_input_vars = array();
 
1275
                if ( ! empty( $_POST['sanitized_widget_setting'] ) ) {
 
1276
                        $sanitized_widget_setting = json_decode( $this->get_post_value( 'sanitized_widget_setting' ), true );
 
1277
                        if ( false === $sanitized_widget_setting ) {
 
1278
                                $this->stop_capturing_option_updates();
 
1279
                                return new WP_Error( 'widget_setting_malformed' );
 
1280
                        }
 
1281
 
 
1282
                        $instance = $this->sanitize_widget_instance( $sanitized_widget_setting );
 
1283
                        if ( is_null( $instance ) ) {
 
1284
                                $this->stop_capturing_option_updates();
 
1285
                                return new WP_Error( 'widget_setting_unsanitized' );
 
1286
                        }
 
1287
 
 
1288
                        if ( ! is_null( $parsed_id['number'] ) ) {
 
1289
                                $value = array();
 
1290
                                $value[$parsed_id['number']] = $instance;
 
1291
                                $key = 'widget-' . $parsed_id['id_base'];
 
1292
                                $_REQUEST[$key] = $_POST[$key] = wp_slash( $value );
 
1293
                                $added_input_vars[] = $key;
 
1294
                        } else {
 
1295
                                foreach ( $instance as $key => $value ) {
 
1296
                                        $_REQUEST[$key] = $_POST[$key] = wp_slash( $value );
 
1297
                                        $added_input_vars[] = $key;
 
1298
                                }
 
1299
                        }
 
1300
                }
 
1301
 
 
1302
                // Invoke the widget update callback.
 
1303
                foreach ( (array) $wp_registered_widget_updates as $name => $control ) {
 
1304
                        if ( $name === $parsed_id['id_base'] && is_callable( $control['callback'] ) ) {
 
1305
                                ob_start();
 
1306
                                call_user_func_array( $control['callback'], $control['params'] );
 
1307
                                ob_end_clean();
 
1308
                                break;
 
1309
                        }
 
1310
                }
 
1311
 
 
1312
                // Clean up any input vars that were manually added
 
1313
                foreach ( $added_input_vars as $key ) {
 
1314
                        unset( $_POST[$key] );
 
1315
                        unset( $_REQUEST[$key] );
 
1316
                }
 
1317
 
 
1318
                // Make sure the expected option was updated.
 
1319
                if ( 0 !== $this->count_captured_options() ) {
 
1320
                        if ( $this->count_captured_options() > 1 ) {
 
1321
                                $this->stop_capturing_option_updates();
 
1322
                                return new WP_Error( 'widget_setting_too_many_options' );
 
1323
                        }
 
1324
 
 
1325
                        $updated_option_name = key( $this->get_captured_options() );
 
1326
                        if ( $updated_option_name !== $option_name ) {
 
1327
                                $this->stop_capturing_option_updates();
 
1328
                                return new WP_Error( 'widget_setting_unexpected_option' );
 
1329
                        }
 
1330
                }
 
1331
 
 
1332
                // Obtain the widget control with the updated instance in place.
 
1333
                ob_start();
 
1334
 
 
1335
                $form = $wp_registered_widget_controls[$widget_id];
 
1336
                if ( $form ) {
 
1337
                        call_user_func_array( $form['callback'], $form['params'] );
 
1338
                }
 
1339
 
 
1340
                $form = ob_get_clean();
 
1341
 
 
1342
                // Obtain the widget instance.
 
1343
                $option = get_option( $option_name );
 
1344
 
 
1345
                if ( null !== $parsed_id['number'] ) {
 
1346
                        $instance = $option[$parsed_id['number']];
 
1347
                } else {
 
1348
                        $instance = $option;
 
1349
                }
 
1350
 
 
1351
                $this->stop_capturing_option_updates();
 
1352
 
 
1353
                return compact( 'instance', 'form' );
 
1354
        }
 
1355
 
 
1356
        /**
 
1357
         * Update widget settings asynchronously.
 
1358
         *
 
1359
         * Allows the Customizer to update a widget using its form, but return the new
 
1360
         * instance info via Ajax instead of saving it to the options table.
 
1361
         *
 
1362
         * Most code here copied from wp_ajax_save_widget()
 
1363
         *
 
1364
         * @since 3.9.0
 
1365
         * @access public
 
1366
         *
 
1367
         * @see wp_ajax_save_widget()
 
1368
         *
 
1369
         */
 
1370
        public function wp_ajax_update_widget() {
 
1371
 
 
1372
                if ( ! is_user_logged_in() ) {
 
1373
                        wp_die( 0 );
 
1374
                }
 
1375
 
 
1376
                check_ajax_referer( 'update-widget', 'nonce' );
 
1377
 
 
1378
                if ( ! current_user_can( 'edit_theme_options' ) ) {
 
1379
                        wp_die( -1 );
 
1380
                }
 
1381
 
 
1382
                if ( ! isset( $_POST['widget-id'] ) ) {
 
1383
                        wp_send_json_error();
 
1384
                }
 
1385
 
 
1386
                /** This action is documented in wp-admin/includes/ajax-actions.php */
 
1387
                do_action( 'load-widgets.php' );
 
1388
 
 
1389
                /** This action is documented in wp-admin/includes/ajax-actions.php */
 
1390
                do_action( 'widgets.php' );
 
1391
 
 
1392
                /** This action is documented in wp-admin/widgets.php */
 
1393
                do_action( 'sidebar_admin_setup' );
 
1394
 
 
1395
                $widget_id = $this->get_post_value( 'widget-id' );
 
1396
                $parsed_id = $this->parse_widget_id( $widget_id );
 
1397
                $id_base   = $parsed_id['id_base'];
 
1398
 
 
1399
                if ( isset( $_POST['widget-' . $id_base] ) && is_array( $_POST['widget-' . $id_base] ) && preg_match( '/__i__|%i%/', key( $_POST['widget-' . $id_base] ) ) ) {
 
1400
                        wp_send_json_error();
 
1401
                }
 
1402
 
 
1403
                $updated_widget = $this->call_widget_update( $widget_id ); // => {instance,form}
 
1404
                if ( is_wp_error( $updated_widget ) ) {
 
1405
                        wp_send_json_error();
 
1406
                }
 
1407
 
 
1408
                $form = $updated_widget['form'];
 
1409
                $instance = $this->sanitize_widget_js_instance( $updated_widget['instance'] );
 
1410
 
 
1411
                wp_send_json_success( compact( 'form', 'instance' ) );
 
1412
        }
 
1413
 
 
1414
        /***************************************************************************
 
1415
         * Option Update Capturing
 
1416
         ***************************************************************************/
 
1417
 
 
1418
        /**
 
1419
         * List of captured widget option updates.
 
1420
         *
 
1421
         * @since 3.9.0
 
1422
         * @access protected
 
1423
         * @var array $_captured_options Values updated while option capture is happening.
 
1424
         */
 
1425
        protected $_captured_options = array();
 
1426
 
 
1427
        /**
 
1428
         * Whether option capture is currently happening.
 
1429
         *
 
1430
         * @since 3.9.0
 
1431
         * @access protected
 
1432
         * @var bool $_is_current Whether option capture is currently happening or not.
 
1433
         */
 
1434
        protected $_is_capturing_option_updates = false;
 
1435
 
 
1436
        /**
 
1437
         * Determine whether the captured option update should be ignored.
 
1438
         *
 
1439
         * @since 3.9.0
 
1440
         * @access protected
 
1441
         *
 
1442
         * @param string $option_name Option name.
 
1443
         * @return boolean Whether the option capture is ignored.
 
1444
         */
 
1445
        protected function is_option_capture_ignored( $option_name ) {
 
1446
                return ( 0 === strpos( $option_name, '_transient_' ) );
 
1447
        }
 
1448
 
 
1449
        /**
 
1450
         * Retrieve captured widget option updates.
 
1451
         *
 
1452
         * @since 3.9.0
 
1453
         * @access protected
 
1454
         *
 
1455
         * @return array Array of captured options.
 
1456
         */
 
1457
        protected function get_captured_options() {
 
1458
                return $this->_captured_options;
 
1459
        }
 
1460
 
 
1461
        /**
 
1462
         * Get the number of captured widget option updates.
 
1463
         *
 
1464
         * @since 3.9.0
 
1465
         * @access protected
 
1466
         *
 
1467
         * @return int Number of updated options.
 
1468
         */
 
1469
        protected function count_captured_options() {
 
1470
                return count( $this->_captured_options );
 
1471
        }
 
1472
 
 
1473
        /**
 
1474
         * Start keeping track of changes to widget options, caching new values.
 
1475
         *
 
1476
         * @since 3.9.0
 
1477
         * @access protected
 
1478
         */
 
1479
        protected function start_capturing_option_updates() {
 
1480
                if ( $this->_is_capturing_option_updates ) {
 
1481
                        return;
 
1482
                }
 
1483
 
 
1484
                $this->_is_capturing_option_updates = true;
 
1485
 
 
1486
                add_filter( 'pre_update_option', array( $this, 'capture_filter_pre_update_option' ), 10, 3 );
 
1487
        }
 
1488
 
 
1489
        /**
 
1490
         * Pre-filter captured option values before updating.
 
1491
         *
 
1492
         * @since 3.9.0
 
1493
         * @access public
 
1494
         *
 
1495
         * @param mixed $new_value
 
1496
         * @param string $option_name
 
1497
         * @param mixed $old_value
 
1498
         * @return mixed
 
1499
         */
 
1500
        public function capture_filter_pre_update_option( $new_value, $option_name, $old_value ) {
 
1501
                if ( $this->is_option_capture_ignored( $option_name ) ) {
 
1502
                        return;
 
1503
                }
 
1504
 
 
1505
                if ( ! isset( $this->_captured_options[$option_name] ) ) {
 
1506
                        add_filter( "pre_option_{$option_name}", array( $this, 'capture_filter_pre_get_option' ) );
 
1507
                }
 
1508
 
 
1509
                $this->_captured_options[$option_name] = $new_value;
 
1510
 
 
1511
                return $old_value;
 
1512
        }
 
1513
 
 
1514
        /**
 
1515
         * Pre-filter captured option values before retrieving.
 
1516
         *
 
1517
         * @since 3.9.0
 
1518
         * @access public
 
1519
         *
 
1520
         * @param mixed $value Option
 
1521
         * @return mixed
 
1522
         */
 
1523
        public function capture_filter_pre_get_option( $value ) {
 
1524
                $option_name = preg_replace( '/^pre_option_/', '', current_filter() );
 
1525
 
 
1526
                if ( isset( $this->_captured_options[$option_name] ) ) {
 
1527
                        $value = $this->_captured_options[$option_name];
 
1528
 
 
1529
                        /** This filter is documented in wp-includes/option.php */
 
1530
                        $value = apply_filters( 'option_' . $option_name, $value );
 
1531
                }
 
1532
 
 
1533
                return $value;
 
1534
        }
 
1535
 
 
1536
        /**
 
1537
         * Undo any changes to the options since options capture began.
 
1538
         *
 
1539
         * @since 3.9.0
 
1540
         * @access protected
 
1541
         */
 
1542
        protected function stop_capturing_option_updates() {
 
1543
                if ( ! $this->_is_capturing_option_updates ) {
 
1544
                        return;
 
1545
                }
 
1546
 
 
1547
                remove_filter( 'pre_update_option', array( $this, 'capture_filter_pre_update_option' ), 10, 3 );
 
1548
 
 
1549
                foreach ( array_keys( $this->_captured_options ) as $option_name ) {
 
1550
                        remove_filter( "pre_option_{$option_name}", array( $this, 'capture_filter_pre_get_option' ) );
 
1551
                }
 
1552
 
 
1553
                $this->_captured_options = array();
 
1554
                $this->_is_capturing_option_updates = false;
 
1555
        }
 
1556
}