~ubuntu-branches/debian/jessie/wordpress/jessie

« back to all changes in this revision

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

  • Committer: Package Import Robot
  • Author(s): Craig Small
  • Date: 2014-04-17 20:56:19 UTC
  • mfrom: (1.2.35)
  • Revision ID: package-import@ubuntu.com-20140417205619-nurbet6eho4yvwfv
Tags: 3.9+dfsg-1
* New upstream release
* 3.9 seems to handle different locations for plugins so the
  plugin directory handling patches have been cut back.

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
                foreach ( $sidebars_widgets as $sidebar_id => $sidebar_widget_ids ) {
 
437
                        if ( empty( $sidebar_widget_ids ) ) {
 
438
                                $sidebar_widget_ids = array();
 
439
                        }
 
440
 
 
441
                        $is_registered_sidebar = isset( $GLOBALS['wp_registered_sidebars'][$sidebar_id] );
 
442
                        $is_inactive_widgets   = ( 'wp_inactive_widgets' === $sidebar_id );
 
443
                        $is_active_sidebar     = ( $is_registered_sidebar && ! $is_inactive_widgets );
 
444
 
 
445
                        // Add setting for managing the sidebar's widgets.
 
446
                        if ( $is_registered_sidebar || $is_inactive_widgets ) {
 
447
                                $setting_id   = sprintf( 'sidebars_widgets[%s]', $sidebar_id );
 
448
                                $setting_args = $this->get_setting_args( $setting_id );
 
449
 
 
450
                                $setting_args['sanitize_callback']    = array( $this, 'sanitize_sidebar_widgets' );
 
451
                                $setting_args['sanitize_js_callback'] = array( $this, 'sanitize_sidebar_widgets_js_instance' );
 
452
 
 
453
                                $this->manager->add_setting( $setting_id, $setting_args );
 
454
                                $new_setting_ids[] = $setting_id;
 
455
 
 
456
                                // Add section to contain controls.
 
457
                                $section_id = sprintf( 'sidebar-widgets-%s', $sidebar_id );
 
458
                                if ( $is_active_sidebar ) {
 
459
 
 
460
                                        $section_args = array(
 
461
                                                /* translators: %s: sidebar name */
 
462
                                                'title' => sprintf( __( 'Widgets: %s' ), $GLOBALS['wp_registered_sidebars'][$sidebar_id]['name'] ),
 
463
                                                'description' => $GLOBALS['wp_registered_sidebars'][$sidebar_id]['description'],
 
464
                                                'priority' => 1000 + array_search( $sidebar_id, array_keys( $wp_registered_sidebars ) ),
 
465
                                        );
 
466
 
 
467
                                        /**
 
468
                                         * Filter Customizer widget section arguments for a given sidebar.
 
469
                                         *
 
470
                                         * @since 3.9.0
 
471
                                         *
 
472
                                         * @param array      $section_args Array of Customizer widget section arguments.
 
473
                                         * @param string     $section_id   Customizer section ID.
 
474
                                         * @param int|string $sidebar_id   Sidebar ID.
 
475
                                         */
 
476
                                        $section_args = apply_filters( 'customizer_widgets_section_args', $section_args, $section_id, $sidebar_id );
 
477
 
 
478
                                        $this->manager->add_section( $section_id, $section_args );
 
479
 
 
480
                                        $control = new WP_Widget_Area_Customize_Control( $this->manager, $setting_id, array(
 
481
                                                'section'    => $section_id,
 
482
                                                'sidebar_id' => $sidebar_id,
 
483
                                                'priority'   => count( $sidebar_widget_ids ), // place 'Add Widget' and 'Reorder' buttons at end.
 
484
                                        ) );
 
485
                                        $new_setting_ids[] = $setting_id;
 
486
 
 
487
                                        $this->manager->add_control( $control );
 
488
                                }
 
489
                        }
 
490
 
 
491
                        // Add a control for each active widget (located in a sidebar).
 
492
                        foreach ( $sidebar_widget_ids as $i => $widget_id ) {
 
493
 
 
494
                                // Skip widgets that may have gone away due to a plugin being deactivated.
 
495
                                if ( ! $is_active_sidebar || ! isset( $GLOBALS['wp_registered_widgets'][$widget_id] ) ) {
 
496
                                        continue;
 
497
                                }
 
498
 
 
499
                                $registered_widget = $GLOBALS['wp_registered_widgets'][$widget_id];
 
500
                                $setting_id        = $this->get_setting_id( $widget_id );
 
501
                                $id_base           = $GLOBALS['wp_registered_widget_controls'][$widget_id]['id_base'];
 
502
 
 
503
                                $control = new WP_Widget_Form_Customize_Control( $this->manager, $setting_id, array(
 
504
                                        'label'          => $registered_widget['name'],
 
505
                                        'section'        => $section_id,
 
506
                                        'sidebar_id'     => $sidebar_id,
 
507
                                        'widget_id'      => $widget_id,
 
508
                                        'widget_id_base' => $id_base,
 
509
                                        'priority'       => $i,
 
510
                                        'width'          => $wp_registered_widget_controls[$widget_id]['width'],
 
511
                                        'height'         => $wp_registered_widget_controls[$widget_id]['height'],
 
512
                                        'is_wide'        => $this->is_wide_widget( $widget_id ),
 
513
                                ) );
 
514
                                $this->manager->add_control( $control );
 
515
                        }
 
516
                }
 
517
 
 
518
                /*
 
519
                 * We have to register these settings later than customize_preview_init
 
520
                 * so that other filters have had a chance to run.
 
521
                 */
 
522
                if ( did_action( 'customize_preview_init' ) ) {
 
523
                        foreach ( $new_setting_ids as $new_setting_id ) {
 
524
                                $this->manager->get_setting( $new_setting_id )->preview();
 
525
                        }
 
526
                }
 
527
                $this->remove_prepreview_filters();
 
528
        }
 
529
 
 
530
        /**
 
531
         * Covert a widget_id into its corresponding customizer setting ID (option name).
 
532
         *
 
533
         * @since 3.9.0
 
534
         * @access public
 
535
         *
 
536
         * @param string $widget_id Widget ID.
 
537
         * @return string Maybe-parsed widget ID.
 
538
         */
 
539
        public function get_setting_id( $widget_id ) {
 
540
                $parsed_widget_id = $this->parse_widget_id( $widget_id );
 
541
                $setting_id       = sprintf( 'widget_%s', $parsed_widget_id['id_base'] );
 
542
 
 
543
                if ( ! is_null( $parsed_widget_id['number'] ) ) {
 
544
                        $setting_id .= sprintf( '[%d]', $parsed_widget_id['number'] );
 
545
                }
 
546
                return $setting_id;
 
547
        }
 
548
 
 
549
        /**
 
550
         * Determine whether the widget is considered "wide".
 
551
         *
 
552
         * Core widgets which may have controls wider than 250, but can
 
553
         * still be shown in the narrow customizer panel. The RSS and Text
 
554
         * widgets in Core, for example, have widths of 400 and yet they
 
555
         * still render fine in the customizer panel. This method will
 
556
         * return all Core widgets as being not wide, but this can be
 
557
         * overridden with the is_wide_widget_in_customizer filter.
 
558
         *
 
559
         * @since 3.9.0
 
560
         * @access public
 
561
         *
 
562
         * @param string $widget_id Widget ID.
 
563
         * @return bool Whether or not the widget is a "wide" widget.
 
564
         */
 
565
        public function is_wide_widget( $widget_id ) {
 
566
                global $wp_registered_widget_controls;
 
567
 
 
568
                $parsed_widget_id = $this->parse_widget_id( $widget_id );
 
569
                $width            = $wp_registered_widget_controls[$widget_id]['width'];
 
570
                $is_core          = in_array( $parsed_widget_id['id_base'], $this->core_widget_id_bases );
 
571
                $is_wide          = ( $width > 250 && ! $is_core );
 
572
 
 
573
                /**
 
574
                 * Filter whether the given widget is considered "wide".
 
575
                 *
 
576
                 * @since 3.9.0
 
577
                 *
 
578
                 * @param bool   $is_wide   Whether the widget is wide, Default false.
 
579
                 * @param string $widget_id Widget ID.
 
580
                 */
 
581
                return apply_filters( 'is_wide_widget_in_customizer', $is_wide, $widget_id );
 
582
        }
 
583
 
 
584
        /**
 
585
         * Covert a widget ID into its id_base and number components.
 
586
         *
 
587
         * @since 3.9.0
 
588
         * @access public
 
589
         *
 
590
         * @param string $widget_id Widget ID.
 
591
         * @return array Array containing a widget's id_base and number components.
 
592
         */
 
593
        public function parse_widget_id( $widget_id ) {
 
594
                $parsed = array(
 
595
                        'number' => null,
 
596
                        'id_base' => null,
 
597
                );
 
598
 
 
599
                if ( preg_match( '/^(.+)-(\d+)$/', $widget_id, $matches ) ) {
 
600
                        $parsed['id_base'] = $matches[1];
 
601
                        $parsed['number']  = intval( $matches[2] );
 
602
                } else {
 
603
                        // likely an old single widget
 
604
                        $parsed['id_base'] = $widget_id;
 
605
                }
 
606
                return $parsed;
 
607
        }
 
608
 
 
609
        /**
 
610
         * Convert a widget setting ID (option path) to its id_base and number components.
 
611
         *
 
612
         * @since 3.9.0
 
613
         * @access public
 
614
         *
 
615
         * @param string $setting_id Widget setting ID.
 
616
         * @return WP_Error|array Array containing a widget's id_base and number components,
 
617
         *                        or a WP_Error object.
 
618
         */
 
619
        public function parse_widget_setting_id( $setting_id ) {
 
620
                if ( ! preg_match( '/^(widget_(.+?))(?:\[(\d+)\])?$/', $setting_id, $matches ) ) {
 
621
                        return new WP_Error( 'widget_setting_invalid_id' );
 
622
                }
 
623
 
 
624
                $id_base = $matches[2];
 
625
                $number  = isset( $matches[3] ) ? intval( $matches[3] ) : null;
 
626
 
 
627
                return compact( 'id_base', 'number' );
 
628
        }
 
629
 
 
630
        /**
 
631
         * Call admin_print_styles-widgets.php and admin_print_styles hooks to
 
632
         * allow custom styles from plugins.
 
633
         *
 
634
         * @since 3.9.0
 
635
         * @access public
 
636
         */
 
637
        public function print_styles() {
 
638
                /** This action is documented in wp-admin/admin-header.php */
 
639
                do_action( 'admin_print_styles-widgets.php' );
 
640
 
 
641
                /** This action is documented in wp-admin/admin-header.php */
 
642
                do_action( 'admin_print_styles' );
 
643
        }
 
644
 
 
645
        /**
 
646
         * Call admin_print_scripts-widgets.php and admin_print_scripts hooks to
 
647
         * allow custom scripts from plugins.
 
648
         *
 
649
         * @since 3.9.0
 
650
         * @access public
 
651
         */
 
652
        public function print_scripts() {
 
653
                /** This action is documented in wp-admin/admin-header.php */
 
654
                do_action( 'admin_print_scripts-widgets.php' );
 
655
 
 
656
                /** This action is documented in wp-admin/admin-header.php */
 
657
                do_action( 'admin_print_scripts' );
 
658
        }
 
659
 
 
660
        /**
 
661
         * Enqueue scripts and styles for customizer panel and export data to JavaScript.
 
662
         *
 
663
         * @since 3.9.0
 
664
         * @access public
 
665
         */
 
666
        public function enqueue_scripts() {
 
667
                wp_enqueue_style( 'customize-widgets' );
 
668
                wp_enqueue_script( 'customize-widgets' );
 
669
 
 
670
                /** This action is documented in wp-admin/admin-header.php */
 
671
                do_action( 'admin_enqueue_scripts', 'widgets.php' );
 
672
 
 
673
                /*
 
674
                 * Export available widgets with control_tpl removed from model
 
675
                 * since plugins need templates to be in the DOM.
 
676
                 */
 
677
                $available_widgets = array();
 
678
 
 
679
                foreach ( $this->get_available_widgets() as $available_widget ) {
 
680
                        unset( $available_widget['control_tpl'] );
 
681
                        $available_widgets[] = $available_widget;
 
682
                }
 
683
 
 
684
                $widget_reorder_nav_tpl = sprintf(
 
685
                        '<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>',
 
686
                        __( 'Move to another area&hellip;' ),
 
687
                        __( 'Move down' ),
 
688
                        __( 'Move up' )
 
689
                );
 
690
 
 
691
                $move_widget_area_tpl = str_replace(
 
692
                        array( '{description}', '{btn}' ),
 
693
                        array(
 
694
                                __( 'Select an area to move this widget into:' ),
 
695
                                _x( 'Move', 'Move widget' ),
 
696
                        ),
 
697
                        '<div class="move-widget-area">
 
698
                                <p class="description">{description}</p>
 
699
                                <ul class="widget-area-select">
 
700
                                        <% _.each( sidebars, function ( sidebar ){ %>
 
701
                                                <li class="" data-id="<%- sidebar.id %>" title="<%- sidebar.description %>" tabindex="0"><%- sidebar.name %></li>
 
702
                                        <% }); %>
 
703
                                </ul>
 
704
                                <div class="move-widget-actions">
 
705
                                        <button class="move-widget-btn button-secondary" type="button">{btn}</button>
 
706
                                </div>
 
707
                        </div>'
 
708
                );
 
709
 
 
710
                global $wp_scripts;
 
711
 
 
712
                $settings = array(
 
713
                        'nonce'                => wp_create_nonce( 'update-widget' ),
 
714
                        'registeredSidebars'   => array_values( $GLOBALS['wp_registered_sidebars'] ),
 
715
                        'registeredWidgets'    => $GLOBALS['wp_registered_widgets'],
 
716
                        'availableWidgets'     => $available_widgets, // @todo Merge this with registered_widgets
 
717
                        'l10n' => array(
 
718
                                'saveBtnLabel'     => __( 'Apply' ),
 
719
                                'saveBtnTooltip'   => __( 'Save and preview changes before publishing them.' ),
 
720
                                'removeBtnLabel'   => __( 'Remove' ),
 
721
                                'removeBtnTooltip' => __( 'Trash widget by moving it to the inactive widgets sidebar.' ),
 
722
                                'error'            => __( 'An error has occurred. Please reload the page and try again.' ),
 
723
                        ),
 
724
                        'tpl' => array(
 
725
                                'widgetReorderNav' => $widget_reorder_nav_tpl,
 
726
                                'moveWidgetArea'   => $move_widget_area_tpl,
 
727
                        ),
 
728
                );
 
729
 
 
730
                foreach ( $settings['registeredWidgets'] as &$registered_widget ) {
 
731
                        unset( $registered_widget['callback'] ); // may not be JSON-serializeable
 
732
                }
 
733
 
 
734
                $wp_scripts->add_data(
 
735
                        'customize-widgets',
 
736
                        'data',
 
737
                        sprintf( 'var _wpCustomizeWidgetsSettings = %s;', json_encode( $settings ) )
 
738
                );
 
739
        }
 
740
 
 
741
        /**
 
742
         * Render the widget form control templates into the DOM.
 
743
         *
 
744
         * @since 3.9.0
 
745
         * @access public
 
746
         */
 
747
        public function output_widget_control_templates() {
 
748
                ?>
 
749
                <div id="widgets-left"><!-- compatibility with JS which looks for widget templates here -->
 
750
                <div id="available-widgets">
 
751
                        <div id="available-widgets-filter">
 
752
                                <label class="screen-reader-text" for="widgets-search"><?php _e( 'Search Widgets' ); ?></label>
 
753
                                <input type="search" id="widgets-search" placeholder="<?php esc_attr_e( 'Search widgets&hellip;' ) ?>" />
 
754
                        </div>
 
755
                        <?php foreach ( $this->get_available_widgets() as $available_widget ): ?>
 
756
                                <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">
 
757
                                        <?php echo $available_widget['control_tpl']; ?>
 
758
                                </div>
 
759
                        <?php endforeach; ?>
 
760
                </div><!-- #available-widgets -->
 
761
                </div><!-- #widgets-left -->
 
762
                <?php
 
763
        }
 
764
 
 
765
        /**
 
766
         * Call admin_print_footer_scripts and admin_print_scripts hooks to
 
767
         * allow custom scripts from plugins.
 
768
         *
 
769
         * @since 3.9.0
 
770
         * @access public
 
771
         */
 
772
        public function print_footer_scripts() {
 
773
                /** This action is documented in wp-admin/admin-footer.php */
 
774
                do_action( 'admin_print_footer_scripts' );
 
775
 
 
776
                /** This action is documented in wp-admin/admin-footer.php */
 
777
                do_action( 'admin_footer-widgets.php' );
 
778
        }
 
779
 
 
780
        /**
 
781
         * Get common arguments to supply when constructing a Customizer setting.
 
782
         *
 
783
         * @since 3.9.0
 
784
         * @access public
 
785
         *
 
786
         * @param string $id        Widget setting ID.
 
787
         * @param array  $overrides Array of setting overrides.
 
788
         * @return array Possibly modified setting arguments.
 
789
         */
 
790
        public function get_setting_args( $id, $overrides = array() ) {
 
791
                $args = array(
 
792
                        'type'       => 'option',
 
793
                        'capability' => 'edit_theme_options',
 
794
                        'transport'  => 'refresh',
 
795
                        'default'    => array(),
 
796
                );
 
797
                $args = array_merge( $args, $overrides );
 
798
 
 
799
                /**
 
800
                 * Filter the common arguments supplied when constructing a Customizer setting.
 
801
                 *
 
802
                 * @since 3.9.0
 
803
                 *
 
804
                 * @see WP_Customize_Setting
 
805
                 *
 
806
                 * @param array  $args Array of Customizer setting arguments.
 
807
                 * @param string $id   Widget setting ID.
 
808
                 */
 
809
                return apply_filters( 'widget_customizer_setting_args', $args, $id );
 
810
        }
 
811
 
 
812
        /**
 
813
         * Make sure that sidebar widget arrays only ever contain widget IDS.
 
814
         *
 
815
         * Used as the 'sanitize_callback' for each $sidebars_widgets setting.
 
816
         *
 
817
         * @since 3.9.0
 
818
         * @access public
 
819
         *
 
820
         * @param array $widget_ids Array of widget IDs.
 
821
         * @return array Array of sanitized widget IDs.
 
822
         */
 
823
        public function sanitize_sidebar_widgets( $widget_ids ) {
 
824
                global $wp_registered_widgets;
 
825
 
 
826
                $widget_ids           = array_map( 'strval', (array) $widget_ids );
 
827
                $sanitized_widget_ids = array();
 
828
 
 
829
                foreach ( $widget_ids as $widget_id ) {
 
830
                        if ( array_key_exists( $widget_id, $wp_registered_widgets ) ) {
 
831
                                $sanitized_widget_ids[] = $widget_id;
 
832
                        }
 
833
                }
 
834
                return $sanitized_widget_ids;
 
835
        }
 
836
 
 
837
        /**
 
838
         * Build up an index of all available widgets for use in Backbone models.
 
839
         *
 
840
         * @since 3.9.0
 
841
         * @access public
 
842
         *
 
843
         * @see wp_list_widgets()
 
844
         *
 
845
         * @return array List of available widgets.
 
846
         */
 
847
        public function get_available_widgets() {
 
848
                static $available_widgets = array();
 
849
                if ( ! empty( $available_widgets ) ) {
 
850
                        return $available_widgets;
 
851
                }
 
852
 
 
853
                global $wp_registered_widgets, $wp_registered_widget_controls;
 
854
                require_once ABSPATH . '/wp-admin/includes/widgets.php'; // for next_widget_id_number()
 
855
 
 
856
                $sort = $wp_registered_widgets;
 
857
                usort( $sort, array( $this, '_sort_name_callback' ) );
 
858
                $done = array();
 
859
 
 
860
                foreach ( $sort as $widget ) {
 
861
                        if ( in_array( $widget['callback'], $done, true ) ) { // We already showed this multi-widget
 
862
                                continue;
 
863
                        }
 
864
 
 
865
                        $sidebar = is_active_widget( $widget['callback'], $widget['id'], false, false );
 
866
                        $done[]  = $widget['callback'];
 
867
 
 
868
                        if ( ! isset( $widget['params'][0] ) ) {
 
869
                                $widget['params'][0] = array();
 
870
                        }
 
871
 
 
872
                        $available_widget = $widget;
 
873
                        unset( $available_widget['callback'] ); // not serializable to JSON
 
874
 
 
875
                        $args = array(
 
876
                                'widget_id'   => $widget['id'],
 
877
                                'widget_name' => $widget['name'],
 
878
                                '_display'    => 'template',
 
879
                        );
 
880
 
 
881
                        $is_disabled     = false;
 
882
                        $is_multi_widget = ( isset( $wp_registered_widget_controls[$widget['id']]['id_base'] ) && isset( $widget['params'][0]['number'] ) );
 
883
                        if ( $is_multi_widget ) {
 
884
                                $id_base            = $wp_registered_widget_controls[$widget['id']]['id_base'];
 
885
                                $args['_temp_id']   = "$id_base-__i__";
 
886
                                $args['_multi_num'] = next_widget_id_number( $id_base );
 
887
                                $args['_add']       = 'multi';
 
888
                        } else {
 
889
                                $args['_add'] = 'single';
 
890
 
 
891
                                if ( $sidebar && 'wp_inactive_widgets' !== $sidebar ) {
 
892
                                        $is_disabled = true;
 
893
                                }
 
894
                                $id_base = $widget['id'];
 
895
                        }
 
896
 
 
897
                        $list_widget_controls_args = wp_list_widget_controls_dynamic_sidebar( array( 0 => $args, 1 => $widget['params'][0] ) );
 
898
                        $control_tpl = $this->get_widget_control( $list_widget_controls_args );
 
899
 
 
900
                        // The properties here are mapped to the Backbone Widget model.
 
901
                        $available_widget = array_merge( $available_widget, array(
 
902
                                'temp_id'      => isset( $args['_temp_id'] ) ? $args['_temp_id'] : null,
 
903
                                'is_multi'     => $is_multi_widget,
 
904
                                'control_tpl'  => $control_tpl,
 
905
                                'multi_number' => ( $args['_add'] === 'multi' ) ? $args['_multi_num'] : false,
 
906
                                'is_disabled'  => $is_disabled,
 
907
                                'id_base'      => $id_base,
 
908
                                'transport'    => 'refresh',
 
909
                                'width'        => $wp_registered_widget_controls[$widget['id']]['width'],
 
910
                                'height'       => $wp_registered_widget_controls[$widget['id']]['height'],
 
911
                                'is_wide'      => $this->is_wide_widget( $widget['id'] ),
 
912
                        ) );
 
913
 
 
914
                        $available_widgets[] = $available_widget;
 
915
                }
 
916
 
 
917
                return $available_widgets;
 
918
        }
 
919
 
 
920
        /**
 
921
         * Naturally order available widgets by name.
 
922
         *
 
923
         * @since 3.9.0
 
924
         * @static
 
925
         * @access protected
 
926
         *
 
927
         * @param array $widget_a The first widget to compare.
 
928
         * @param array $widget_b The second widget to compare.
 
929
         * @return int Reorder position for the current widget comparison.
 
930
         */
 
931
        protected function _sort_name_callback( $widget_a, $widget_b ) {
 
932
                return strnatcasecmp( $widget_a['name'], $widget_b['name'] );
 
933
        }
 
934
 
 
935
        /**
 
936
         * Get the widget control markup.
 
937
         *
 
938
         * @since 3.9.0
 
939
         * @access public
 
940
         *
 
941
         * @param array $args Widget control arguments.
 
942
         * @return string Widget control form HTML markup.
 
943
         */
 
944
        public function get_widget_control( $args ) {
 
945
                ob_start();
 
946
 
 
947
                call_user_func_array( 'wp_widget_control', $args );
 
948
                $replacements = array(
 
949
                        '<form action="" method="post">' => '<div class="form">',
 
950
                        '</form>' => '</div><!-- .form -->',
 
951
                );
 
952
 
 
953
                $control_tpl = ob_get_clean();
 
954
 
 
955
                $control_tpl = str_replace( array_keys( $replacements ), array_values( $replacements ), $control_tpl );
 
956
 
 
957
                return $control_tpl;
 
958
        }
 
959
 
 
960
        /**
 
961
         * Add hooks for the customizer preview.
 
962
         *
 
963
         * @since 3.9.0
 
964
         * @access public
 
965
         */
 
966
        public function customize_preview_init() {
 
967
                add_filter( 'sidebars_widgets',   array( $this, 'preview_sidebars_widgets' ), 1 );
 
968
                add_action( 'wp_enqueue_scripts', array( $this, 'customize_preview_enqueue' ) );
 
969
                add_action( 'wp_print_styles',    array( $this, 'print_preview_css' ), 1 );
 
970
                add_action( 'wp_footer',          array( $this, 'export_preview_data' ), 20 );
 
971
        }
 
972
 
 
973
        /**
 
974
         * When previewing, make sure the proper previewing widgets are used.
 
975
         *
 
976
         * Because wp_get_sidebars_widgets() gets called early at init
 
977
         * (via wp_convert_widget_settings()) and can set global variable
 
978
         * $_wp_sidebars_widgets to the value of get_option( 'sidebars_widgets' )
 
979
         * before the customizer preview filter is added, we have to reset
 
980
         * it after the filter has been added.
 
981
         *
 
982
         * @since 3.9.0
 
983
         * @access public
 
984
         *
 
985
         * @param array $sidebars_widgets List of widgets for the current sidebar.
 
986
         */
 
987
        public function preview_sidebars_widgets( $sidebars_widgets ) {
 
988
                $sidebars_widgets = get_option( 'sidebars_widgets' );
 
989
 
 
990
                unset( $sidebars_widgets['array_version'] );
 
991
                return $sidebars_widgets;
 
992
        }
 
993
 
 
994
        /**
 
995
         * Enqueue scripts for the Customizer preview.
 
996
         *
 
997
         * @since 3.9.0
 
998
         * @access public
 
999
         */
 
1000
        public function customize_preview_enqueue() {
 
1001
                wp_enqueue_script( 'customize-preview-widgets' );
 
1002
        }
 
1003
 
 
1004
        /**
 
1005
         * Insert default style for highlighted widget at early point so theme
 
1006
         * stylesheet can override.
 
1007
         *
 
1008
         * @since 3.9.0
 
1009
         * @access public
 
1010
         *
 
1011
         * @action wp_print_styles
 
1012
         */
 
1013
        public function print_preview_css() {
 
1014
                ?>
 
1015
                <style>
 
1016
                .widget-customizer-highlighted-widget {
 
1017
                        outline: none;
 
1018
                        -webkit-box-shadow: 0 0 2px rgba(30,140,190,0.8);
 
1019
                        box-shadow: 0 0 2px rgba(30,140,190,0.8);
 
1020
                        position: relative;
 
1021
                        z-index: 1;
 
1022
                }
 
1023
                </style>
 
1024
                <?php
 
1025
        }
 
1026
 
 
1027
        /**
 
1028
         * At the very end of the page, at the very end of the wp_footer,
 
1029
         * communicate the sidebars that appeared on the page.
 
1030
         *
 
1031
         * @since 3.9.0
 
1032
         * @access public
 
1033
         */
 
1034
        public function export_preview_data() {
 
1035
 
 
1036
                // Prepare customizer settings to pass to Javascript.
 
1037
                $settings = array(
 
1038
                        'renderedSidebars'   => array_fill_keys( array_unique( $this->rendered_sidebars ), true ),
 
1039
                        'renderedWidgets'    => array_fill_keys( array_keys( $this->rendered_widgets ), true ),
 
1040
                        'registeredSidebars' => array_values( $GLOBALS['wp_registered_sidebars'] ),
 
1041
                        'registeredWidgets'  => $GLOBALS['wp_registered_widgets'],
 
1042
                        'l10n'               => array(
 
1043
                                'widgetTooltip' => __( 'Shift-click to edit this widget.' ),
 
1044
                        ),
 
1045
                );
 
1046
                foreach ( $settings['registeredWidgets'] as &$registered_widget ) {
 
1047
                        unset( $registered_widget['callback'] ); // may not be JSON-serializeable
 
1048
                }
 
1049
 
 
1050
                ?>
 
1051
                <script type="text/javascript">
 
1052
                        var _wpWidgetCustomizerPreviewSettings = <?php echo json_encode( $settings ); ?>;
 
1053
                </script>
 
1054
                <?php
 
1055
        }
 
1056
 
 
1057
        /**
 
1058
         * Keep track of the widgets that were rendered.
 
1059
         *
 
1060
         * @since 3.9.0
 
1061
         * @access public
 
1062
         *
 
1063
         * @param array $widget Rendered widget to tally.
 
1064
         */
 
1065
        public function tally_rendered_widgets( $widget ) {
 
1066
                $this->rendered_widgets[$widget['id']] = true;
 
1067
        }
 
1068
 
 
1069
        /**
 
1070
         * Tally the sidebars rendered via is_active_sidebar().
 
1071
         *
 
1072
         * Keep track of the times that is_active_sidebar() is called
 
1073
         * in the template, and assume that this means that the sidebar
 
1074
         * would be rendered on the template if there were widgets
 
1075
         * populating it.
 
1076
         *
 
1077
         * @since 3.9.0
 
1078
         * @access public
 
1079
         *
 
1080
         * @param bool    $is_active  Whether the sidebar is active.
 
1081
         * @pasram string $sidebar_id Sidebar ID.
 
1082
         */
 
1083
        public function tally_sidebars_via_is_active_sidebar_calls( $is_active, $sidebar_id ) {
 
1084
                if ( isset( $GLOBALS['wp_registered_sidebars'][$sidebar_id] ) ) {
 
1085
                        $this->rendered_sidebars[] = $sidebar_id;
 
1086
                }
 
1087
                /*
 
1088
                 * We may need to force this to true, and also force-true the value
 
1089
                 * for 'dynamic_sidebar_has_widgets' if we want to ensure that there
 
1090
                 * is an area to drop widgets into, if the sidebar is empty.
 
1091
                 */
 
1092
                return $is_active;
 
1093
        }
 
1094
 
 
1095
        /**
 
1096
         * Tally the sidebars rendered via dynamic_sidebar().
 
1097
         *
 
1098
         * Keep track of the times that dynamic_sidebar() is called in the template,
 
1099
         * and assume this means the sidebar would be rendered on the template if
 
1100
         * there were widgets populating it.
 
1101
         *
 
1102
         * @since 3.9.0
 
1103
         * @access public
 
1104
         *
 
1105
         * @param bool   $has_widgets Whether the current sidebar has widgets.
 
1106
         * @param string $sidebar_id  Sidebar ID.
 
1107
         */
 
1108
        public function tally_sidebars_via_dynamic_sidebar_calls( $has_widgets, $sidebar_id ) {
 
1109
                if ( isset( $GLOBALS['wp_registered_sidebars'][$sidebar_id] ) ) {
 
1110
                        $this->rendered_sidebars[] = $sidebar_id;
 
1111
                }
 
1112
 
 
1113
                /*
 
1114
                 * We may need to force this to true, and also force-true the value
 
1115
                 * for 'is_active_sidebar' if we want to ensure there is an area to
 
1116
                 * drop widgets into, if the sidebar is empty.
 
1117
                 */
 
1118
                return $has_widgets;
 
1119
        }
 
1120
 
 
1121
        /**
 
1122
         * Get a widget instance's hash key.
 
1123
         *
 
1124
         * Serialize an instance and hash it with the AUTH_KEY; when a JS value is
 
1125
         * posted back to save, this instance hash key is used to ensure that the
 
1126
         * serialized_instance was not tampered with, but that it had originated
 
1127
         * from WordPress and so is sanitized.
 
1128
         *
 
1129
         * @since 3.9.0
 
1130
         * @access protected
 
1131
         *
 
1132
         * @param array $instance Widget instance.
 
1133
         * @return string Widget instance's hash key.
 
1134
         */
 
1135
        protected function get_instance_hash_key( $instance ) {
 
1136
                $hash = md5( AUTH_KEY . serialize( $instance ) );
 
1137
                return $hash;
 
1138
        }
 
1139
 
 
1140
        /**
 
1141
         * Sanitize a widget instance.
 
1142
         *
 
1143
         * Unserialize the JS-instance for storing in the options. It's important
 
1144
         * that this filter only get applied to an instance once.
 
1145
         *
 
1146
         * @since 3.9.0
 
1147
         * @access public
 
1148
         *
 
1149
         * @param array $value Widget instance to sanitize.
 
1150
         * @return array Sanitized widget instance.
 
1151
         */
 
1152
        public function sanitize_widget_instance( $value ) {
 
1153
                if ( $value === array() ) {
 
1154
                        return $value;
 
1155
                }
 
1156
 
 
1157
                if ( empty( $value['is_widget_customizer_js_value'] )
 
1158
                        || empty( $value['instance_hash_key'] )
 
1159
                        || empty( $value['encoded_serialized_instance'] ) )
 
1160
                {
 
1161
                        return null;
 
1162
                }
 
1163
 
 
1164
                $decoded = base64_decode( $value['encoded_serialized_instance'], true );
 
1165
 
 
1166
                if ( false === $decoded ) {
 
1167
                        return null;
 
1168
                }
 
1169
                $instance = unserialize( $decoded );
 
1170
 
 
1171
                if ( false === $instance ) {
 
1172
                        return null;
 
1173
                }
 
1174
                if ( $this->get_instance_hash_key( $instance ) !== $value['instance_hash_key'] ) {
 
1175
                        return null;
 
1176
                }
 
1177
                return $instance;
 
1178
        }
 
1179
 
 
1180
        /**
 
1181
         * Convert widget instance into JSON-representable format.
 
1182
         *
 
1183
         * @since 3.9.0
 
1184
         * @access public
 
1185
         *
 
1186
         * @param array $value Widget instance to convert to JSON.
 
1187
         * @return array JSON-converted widget instance.
 
1188
         */
 
1189
        public function sanitize_widget_js_instance( $value ) {
 
1190
                if ( empty( $value['is_widget_customizer_js_value'] ) ) {
 
1191
                        $serialized = serialize( $value );
 
1192
 
 
1193
                        $value = array(
 
1194
                                'encoded_serialized_instance'   => base64_encode( $serialized ),
 
1195
                                'title'                         => empty( $value['title'] ) ? '' : $value['title'],
 
1196
                                'is_widget_customizer_js_value' => true,
 
1197
                                'instance_hash_key'             => $this->get_instance_hash_key( $value ),
 
1198
                        );
 
1199
                }
 
1200
                return $value;
 
1201
        }
 
1202
 
 
1203
        /**
 
1204
         * Strip out widget IDs for widgets which are no longer registered.
 
1205
         *
 
1206
         * One example where this might happen is when a plugin orphans a widget
 
1207
         * in a sidebar upon deactivation.
 
1208
         *
 
1209
         * @since 3.9.0
 
1210
         * @access public
 
1211
         *
 
1212
         * @param array $widget_ids List of widget IDs.
 
1213
         * @return array Parsed list of widget IDs.
 
1214
         */
 
1215
        public function sanitize_sidebar_widgets_js_instance( $widget_ids ) {
 
1216
                global $wp_registered_widgets;
 
1217
                $widget_ids = array_values( array_intersect( $widget_ids, array_keys( $wp_registered_widgets ) ) );
 
1218
                return $widget_ids;
 
1219
        }
 
1220
 
 
1221
        /**
 
1222
         * Find and invoke the widget update and control callbacks.
 
1223
         *
 
1224
         * Requires that $_POST be populated with the instance data.
 
1225
         *
 
1226
         * @since 3.9.0
 
1227
         * @access public
 
1228
         *
 
1229
         * @param  string $widget_id Widget ID.
 
1230
         * @return WP_Error|array Array containing the updated widget information.
 
1231
         *                        A WP_Error object, otherwise.
 
1232
         */
 
1233
        public function call_widget_update( $widget_id ) {
 
1234
                global $wp_registered_widget_updates, $wp_registered_widget_controls;
 
1235
 
 
1236
                $this->start_capturing_option_updates();
 
1237
                $parsed_id   = $this->parse_widget_id( $widget_id );
 
1238
                $option_name = 'widget_' . $parsed_id['id_base'];
 
1239
 
 
1240
                /*
 
1241
                 * If a previously-sanitized instance is provided, populate the input vars
 
1242
                 * with its values so that the widget update callback will read this instance
 
1243
                 */
 
1244
                $added_input_vars = array();
 
1245
                if ( ! empty( $_POST['sanitized_widget_setting'] ) ) {
 
1246
                        $sanitized_widget_setting = json_decode( $this->get_post_value( 'sanitized_widget_setting' ), true );
 
1247
                        if ( false === $sanitized_widget_setting ) {
 
1248
                                $this->stop_capturing_option_updates();
 
1249
                                return new WP_Error( 'widget_setting_malformed' );
 
1250
                        }
 
1251
 
 
1252
                        $instance = $this->sanitize_widget_instance( $sanitized_widget_setting );
 
1253
                        if ( is_null( $instance ) ) {
 
1254
                                $this->stop_capturing_option_updates();
 
1255
                                return new WP_Error( 'widget_setting_unsanitized' );
 
1256
                        }
 
1257
 
 
1258
                        if ( ! is_null( $parsed_id['number'] ) ) {
 
1259
                                $value = array();
 
1260
                                $value[$parsed_id['number']] = $instance;
 
1261
                                $key = 'widget-' . $parsed_id['id_base'];
 
1262
                                $_REQUEST[$key] = $_POST[$key] = wp_slash( $value );
 
1263
                                $added_input_vars[] = $key;
 
1264
                        } else {
 
1265
                                foreach ( $instance as $key => $value ) {
 
1266
                                        $_REQUEST[$key] = $_POST[$key] = wp_slash( $value );
 
1267
                                        $added_input_vars[] = $key;
 
1268
                                }
 
1269
                        }
 
1270
                }
 
1271
 
 
1272
                // Invoke the widget update callback.
 
1273
                foreach ( (array) $wp_registered_widget_updates as $name => $control ) {
 
1274
                        if ( $name === $parsed_id['id_base'] && is_callable( $control['callback'] ) ) {
 
1275
                                ob_start();
 
1276
                                call_user_func_array( $control['callback'], $control['params'] );
 
1277
                                ob_end_clean();
 
1278
                                break;
 
1279
                        }
 
1280
                }
 
1281
 
 
1282
                // Clean up any input vars that were manually added
 
1283
                foreach ( $added_input_vars as $key ) {
 
1284
                        unset( $_POST[$key] );
 
1285
                        unset( $_REQUEST[$key] );
 
1286
                }
 
1287
 
 
1288
                // Make sure the expected option was updated.
 
1289
                if ( 0 !== $this->count_captured_options() ) {
 
1290
                        if ( $this->count_captured_options() > 1 ) {
 
1291
                                $this->stop_capturing_option_updates();
 
1292
                                return new WP_Error( 'widget_setting_too_many_options' );
 
1293
                        }
 
1294
 
 
1295
                        $updated_option_name = key( $this->get_captured_options() );
 
1296
                        if ( $updated_option_name !== $option_name ) {
 
1297
                                $this->stop_capturing_option_updates();
 
1298
                                return new WP_Error( 'widget_setting_unexpected_option' );
 
1299
                        }
 
1300
                }
 
1301
 
 
1302
                // Obtain the widget control with the updated instance in place.
 
1303
                ob_start();
 
1304
 
 
1305
                $form = $wp_registered_widget_controls[$widget_id];
 
1306
                if ( $form ) {
 
1307
                        call_user_func_array( $form['callback'], $form['params'] );
 
1308
                }
 
1309
 
 
1310
                $form = ob_get_clean();
 
1311
 
 
1312
                // Obtain the widget instance.
 
1313
                $option = get_option( $option_name );
 
1314
 
 
1315
                if ( null !== $parsed_id['number'] ) {
 
1316
                        $instance = $option[$parsed_id['number']];
 
1317
                } else {
 
1318
                        $instance = $option;
 
1319
                }
 
1320
 
 
1321
                $this->stop_capturing_option_updates();
 
1322
 
 
1323
                return compact( 'instance', 'form' );
 
1324
        }
 
1325
 
 
1326
        /**
 
1327
         * Update widget settings asynchronously.
 
1328
         *
 
1329
         * Allows the Customizer to update a widget using its form, but return the new
 
1330
         * instance info via Ajax instead of saving it to the options table.
 
1331
         *
 
1332
         * Most code here copied from wp_ajax_save_widget()
 
1333
         *
 
1334
         * @since 3.9.0
 
1335
         * @access public
 
1336
         *
 
1337
         * @see wp_ajax_save_widget()
 
1338
         *
 
1339
         */
 
1340
        public function wp_ajax_update_widget() {
 
1341
 
 
1342
                if ( ! is_user_logged_in() ) {
 
1343
                        wp_die( 0 );
 
1344
                }
 
1345
 
 
1346
                check_ajax_referer( 'update-widget', 'nonce' );
 
1347
 
 
1348
                if ( ! current_user_can( 'edit_theme_options' ) ) {
 
1349
                        wp_die( -1 );
 
1350
                }
 
1351
 
 
1352
                if ( ! isset( $_POST['widget-id'] ) ) {
 
1353
                        wp_send_json_error();
 
1354
                }
 
1355
 
 
1356
                /** This action is documented in wp-admin/includes/ajax-actions.php */
 
1357
                do_action( 'load-widgets.php' );
 
1358
 
 
1359
                /** This action is documented in wp-admin/includes/ajax-actions.php */
 
1360
                do_action( 'widgets.php' );
 
1361
 
 
1362
                /** This action is documented in wp-admin/widgets.php */
 
1363
                do_action( 'sidebar_admin_setup' );
 
1364
 
 
1365
                $widget_id = $this->get_post_value( 'widget-id' );
 
1366
                $parsed_id = $this->parse_widget_id( $widget_id );
 
1367
                $id_base   = $parsed_id['id_base'];
 
1368
 
 
1369
                if ( isset( $_POST['widget-' . $id_base] ) && is_array( $_POST['widget-' . $id_base] ) && preg_match( '/__i__|%i%/', key( $_POST['widget-' . $id_base] ) ) ) {
 
1370
                        wp_send_json_error();
 
1371
                }
 
1372
 
 
1373
                $updated_widget = $this->call_widget_update( $widget_id ); // => {instance,form}
 
1374
                if ( is_wp_error( $updated_widget ) ) {
 
1375
                        wp_send_json_error();
 
1376
                }
 
1377
 
 
1378
                $form = $updated_widget['form'];
 
1379
                $instance = $this->sanitize_widget_js_instance( $updated_widget['instance'] );
 
1380
 
 
1381
                wp_send_json_success( compact( 'form', 'instance' ) );
 
1382
        }
 
1383
 
 
1384
        /***************************************************************************
 
1385
         * Option Update Capturing
 
1386
         ***************************************************************************/
 
1387
 
 
1388
        /**
 
1389
         * List of captured widget option updates.
 
1390
         *
 
1391
         * @since 3.9.0
 
1392
         * @access protected
 
1393
         * @var array $_captured_options Values updated while option capture is happening.
 
1394
         */
 
1395
        protected $_captured_options = array();
 
1396
 
 
1397
        /**
 
1398
         * Whether option capture is currently happening.
 
1399
         *
 
1400
         * @since 3.9.0
 
1401
         * @access protected
 
1402
         * @var bool $_is_current Whether option capture is currently happening or not.
 
1403
         */
 
1404
        protected $_is_capturing_option_updates = false;
 
1405
 
 
1406
        /**
 
1407
         * Determine whether the captured option update should be ignored.
 
1408
         *
 
1409
         * @since 3.9.0
 
1410
         * @access protected
 
1411
         *
 
1412
         * @param string $option_name Option name.
 
1413
         * @return boolean Whether the option capture is ignored.
 
1414
         */
 
1415
        protected function is_option_capture_ignored( $option_name ) {
 
1416
                return ( 0 === strpos( $option_name, '_transient_' ) );
 
1417
        }
 
1418
 
 
1419
        /**
 
1420
         * Retrieve captured widget option updates.
 
1421
         *
 
1422
         * @since 3.9.0
 
1423
         * @access protected
 
1424
         *
 
1425
         * @return array Array of captured options.
 
1426
         */
 
1427
        protected function get_captured_options() {
 
1428
                return $this->_captured_options;
 
1429
        }
 
1430
 
 
1431
        /**
 
1432
         * Get the number of captured widget option updates.
 
1433
         *
 
1434
         * @since 3.9.0
 
1435
         * @access protected
 
1436
         *
 
1437
         * @return int Number of updated options.
 
1438
         */
 
1439
        protected function count_captured_options() {
 
1440
                return count( $this->_captured_options );
 
1441
        }
 
1442
 
 
1443
        /**
 
1444
         * Start keeping track of changes to widget options, caching new values.
 
1445
         *
 
1446
         * @since 3.9.0
 
1447
         * @access protected
 
1448
         */
 
1449
        protected function start_capturing_option_updates() {
 
1450
                if ( $this->_is_capturing_option_updates ) {
 
1451
                        return;
 
1452
                }
 
1453
 
 
1454
                $this->_is_capturing_option_updates = true;
 
1455
 
 
1456
                add_filter( 'pre_update_option', array( $this, 'capture_filter_pre_update_option' ), 10, 3 );
 
1457
        }
 
1458
 
 
1459
        /**
 
1460
         * Pre-filter captured option values before updating.
 
1461
         *
 
1462
         * @since 3.9.0
 
1463
         * @access public
 
1464
         *
 
1465
         * @param mixed $new_value
 
1466
         * @param string $option_name
 
1467
         * @param mixed $old_value
 
1468
         * @return mixed
 
1469
         */
 
1470
        public function capture_filter_pre_update_option( $new_value, $option_name, $old_value ) {
 
1471
                if ( $this->is_option_capture_ignored( $option_name ) ) {
 
1472
                        return;
 
1473
                }
 
1474
 
 
1475
                if ( ! isset( $this->_captured_options[$option_name] ) ) {
 
1476
                        add_filter( "pre_option_{$option_name}", array( $this, 'capture_filter_pre_get_option' ) );
 
1477
                }
 
1478
 
 
1479
                $this->_captured_options[$option_name] = $new_value;
 
1480
 
 
1481
                return $old_value;
 
1482
        }
 
1483
 
 
1484
        /**
 
1485
         * Pre-filter captured option values before retrieving.
 
1486
         *
 
1487
         * @since 3.9.0
 
1488
         * @access public
 
1489
         *
 
1490
         * @param mixed $value Option
 
1491
         * @return mixed
 
1492
         */
 
1493
        public function capture_filter_pre_get_option( $value ) {
 
1494
                $option_name = preg_replace( '/^pre_option_/', '', current_filter() );
 
1495
 
 
1496
                if ( isset( $this->_captured_options[$option_name] ) ) {
 
1497
                        $value = $this->_captured_options[$option_name];
 
1498
 
 
1499
                        /** This filter is documented in wp-includes/option.php */
 
1500
                        $value = apply_filters( 'option_' . $option_name, $value );
 
1501
                }
 
1502
 
 
1503
                return $value;
 
1504
        }
 
1505
 
 
1506
        /**
 
1507
         * Undo any changes to the options since options capture began.
 
1508
         *
 
1509
         * @since 3.9.0
 
1510
         * @access protected
 
1511
         */
 
1512
        protected function stop_capturing_option_updates() {
 
1513
                if ( ! $this->_is_capturing_option_updates ) {
 
1514
                        return;
 
1515
                }
 
1516
 
 
1517
                remove_filter( 'pre_update_option', array( $this, 'capture_filter_pre_update_option' ), 10, 3 );
 
1518
 
 
1519
                foreach ( array_keys( $this->_captured_options ) as $option_name ) {
 
1520
                        remove_filter( "pre_option_{$option_name}", array( $this, 'capture_filter_pre_get_option' ) );
 
1521
                }
 
1522
 
 
1523
                $this->_captured_options = array();
 
1524
                $this->_is_capturing_option_updates = false;
 
1525
        }
 
1526
}