WordPress.org

Make WordPress Core

Ticket #38823: class-wp-customize-manager.php

File class-wp-customize-manager.php, 127.6 KB (added by odysseygate, 4 years ago)
Line 
1<?php
2/**
3 * WordPress Customize Manager classes
4 *
5 * @package WordPress
6 * @subpackage Customize
7 * @since 3.4.0
8 */
9
10/**
11 * Customize Manager class.
12 *
13 * Bootstraps the Customize experience on the server-side.
14 *
15 * Sets up the theme-switching process if a theme other than the active one is
16 * being previewed and customized.
17 *
18 * Serves as a factory for Customize Controls and Settings, and
19 * instantiates default Customize Controls and Settings.
20 *
21 * @since 3.4.0
22 */
23final class WP_Customize_Manager {
24        /**
25         * An instance of the theme being previewed.
26         *
27         * @since 3.4.0
28         * @access protected
29         * @var WP_Theme
30         */
31        protected $theme;
32
33        /**
34         * The directory name of the previously active theme (within the theme_root).
35         *
36         * @since 3.4.0
37         * @access protected
38         * @var string
39         */
40        protected $original_stylesheet;
41
42        /**
43         * Whether this is a Customizer pageload.
44         *
45         * @since 3.4.0
46         * @access protected
47         * @var bool
48         */
49        protected $previewing = false;
50
51        /**
52         * Methods and properties dealing with managing widgets in the Customizer.
53         *
54         * @since 3.9.0
55         * @access public
56         * @var WP_Customize_Widgets
57         */
58        public $widgets;
59
60        /**
61         * Methods and properties dealing with managing nav menus in the Customizer.
62         *
63         * @since 4.3.0
64         * @access public
65         * @var WP_Customize_Nav_Menus
66         */
67        public $nav_menus;
68
69        /**
70         * Methods and properties dealing with selective refresh in the Customizer preview.
71         *
72         * @since 4.5.0
73         * @access public
74         * @var WP_Customize_Selective_Refresh
75         */
76        public $selective_refresh;
77
78        /**
79         * Registered instances of WP_Customize_Setting.
80         *
81         * @since 3.4.0
82         * @access protected
83         * @var array
84         */
85        protected $settings = array();
86
87        /**
88         * Sorted top-level instances of WP_Customize_Panel and WP_Customize_Section.
89         *
90         * @since 4.0.0
91         * @access protected
92         * @var array
93         */
94        protected $containers = array();
95
96        /**
97         * Registered instances of WP_Customize_Panel.
98         *
99         * @since 4.0.0
100         * @access protected
101         * @var array
102         */
103        protected $panels = array();
104
105        /**
106         * List of core components.
107         *
108         * @since 4.5.0
109         * @access protected
110         * @var array
111         */
112        protected $components = array( 'widgets', 'nav_menus' );
113
114        /**
115         * Registered instances of WP_Customize_Section.
116         *
117         * @since 3.4.0
118         * @access protected
119         * @var array
120         */
121        protected $sections = array();
122
123        /**
124         * Registered instances of WP_Customize_Control.
125         *
126         * @since 3.4.0
127         * @access protected
128         * @var array
129         */
130        protected $controls = array();
131
132        /**
133         * Panel types that may be rendered from JS templates.
134         *
135         * @since 4.3.0
136         * @access protected
137         * @var array
138         */
139        protected $registered_panel_types = array();
140
141        /**
142         * Section types that may be rendered from JS templates.
143         *
144         * @since 4.3.0
145         * @access protected
146         * @var array
147         */
148        protected $registered_section_types = array();
149
150        /**
151         * Control types that may be rendered from JS templates.
152         *
153         * @since 4.1.0
154         * @access protected
155         * @var array
156         */
157        protected $registered_control_types = array();
158
159        /**
160         * Initial URL being previewed.
161         *
162         * @since 4.4.0
163         * @access protected
164         * @var string
165         */
166        protected $preview_url;
167
168        /**
169         * URL to link the user to when closing the Customizer.
170         *
171         * @since 4.4.0
172         * @access protected
173         * @var string
174         */
175        protected $return_url;
176
177        /**
178         * Mapping of 'panel', 'section', 'control' to the ID which should be autofocused.
179         *
180         * @since 4.4.0
181         * @access protected
182         * @var array
183         */
184        protected $autofocus = array();
185
186        /**
187         * Messenger channel.
188         *
189         * @since 4.7.0
190         * @access protected
191         * @var string
192         */
193        protected $messenger_channel;
194
195        /**
196         * Unsanitized values for Customize Settings parsed from $_POST['customized'].
197         *
198         * @var array
199         */
200        private $_post_values;
201
202        /**
203         * Changeset UUID.
204         *
205         * @since 4.7.0
206         * @access private
207         * @var string
208         */
209        private $_changeset_uuid;
210
211        /**
212         * Changeset post ID.
213         *
214         * @since 4.7.0
215         * @access private
216         * @var int|false
217         */
218        private $_changeset_post_id;
219
220        /**
221         * Changeset data loaded from a customize_changeset post.
222         *
223         * @since 4.7.0
224         * @access private
225         * @var array
226         */
227        private $_changeset_data;
228
229        /**
230         * Constructor.
231         *
232         * @since 3.4.0
233         * @since 4.7.0 Added $args param.
234         *
235         * @param array $args {
236         *     Args.
237         *
238         *     @type string $changeset_uuid    Changeset UUID, the post_name for the customize_changeset post containing the customized state. Defaults to new UUID.
239         *     @type string $theme             Theme to be previewed (for theme switch). Defaults to customize_theme or theme query params.
240         *     @type string $messenger_channel Messenger channel. Defaults to customize_messenger_channel query param.
241         * }
242         */
243        public function __construct( $args = array() ) {
244
245                $args = array_merge(
246                        array_fill_keys( array( 'changeset_uuid', 'theme', 'messenger_channel' ), null ),
247                        $args
248                );
249
250                // Note that the UUID format will be validated in the setup_theme() method.
251                if ( ! isset( $args['changeset_uuid'] ) ) {
252                        $args['changeset_uuid'] = wp_generate_uuid4();
253                }
254
255                // The theme and messenger_channel should be supplied via $args, but they are also looked at in the $_REQUEST global here for back-compat.
256                if ( ! isset( $args['theme'] ) ) {
257                        if ( isset( $_REQUEST['customize_theme'] ) ) {
258                                $args['theme'] = wp_unslash( $_REQUEST['customize_theme'] );
259                        } elseif ( isset( $_REQUEST['theme'] ) ) { // Deprecated.
260                                $args['theme'] = wp_unslash( $_REQUEST['theme'] );
261                        }
262                }
263                if ( ! isset( $args['messenger_channel'] ) && isset( $_REQUEST['customize_messenger_channel'] ) ) {
264                        $args['messenger_channel'] = sanitize_key( wp_unslash( $_REQUEST['customize_messenger_channel'] ) );
265                }
266
267                $this->original_stylesheet = get_stylesheet();
268                $this->theme = wp_get_theme( $args['theme'] );
269                $this->messenger_channel = $args['messenger_channel'];
270                $this->_changeset_uuid = $args['changeset_uuid'];
271
272                require_once( ABSPATH . WPINC . '/class-wp-customize-setting.php' );
273                require_once( ABSPATH . WPINC . '/class-wp-customize-panel.php' );
274                require_once( ABSPATH . WPINC . '/class-wp-customize-section.php' );
275                require_once( ABSPATH . WPINC . '/class-wp-customize-control.php' );
276
277                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-color-control.php' );
278                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-media-control.php' );
279                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-upload-control.php' );
280                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-image-control.php' );
281                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-background-image-control.php' );
282                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-background-position-control.php' );
283                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-cropped-image-control.php' );
284                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-site-icon-control.php' );
285                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-header-image-control.php' );
286                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-theme-control.php' );
287                require_once( ABSPATH . WPINC . '/customize/class-wp-widget-area-customize-control.php' );
288                require_once( ABSPATH . WPINC . '/customize/class-wp-widget-form-customize-control.php' );
289                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-control.php' );
290                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-item-control.php' );
291                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-location-control.php' );
292                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-name-control.php' );
293                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-auto-add-control.php' );
294                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-new-menu-control.php' );
295
296                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menus-panel.php' );
297
298                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-themes-section.php' );
299                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-sidebar-section.php' );
300                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-section.php' );
301                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-new-menu-section.php' );
302
303                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-custom-css-setting.php' );
304                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-filter-setting.php' );
305                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-header-image-setting.php' );
306                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-background-image-setting.php' );
307                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-item-setting.php' );
308                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-setting.php' );
309
310                /**
311                 * Filters the core Customizer components to load.
312                 *
313                 * This allows Core components to be excluded from being instantiated by
314                 * filtering them out of the array. Note that this filter generally runs
315                 * during the {@see 'plugins_loaded'} action, so it cannot be added
316                 * in a theme.
317                 *
318                 * @since 4.4.0
319                 *
320                 * @see WP_Customize_Manager::__construct()
321                 *
322                 * @param array                $components List of core components to load.
323                 * @param WP_Customize_Manager $this       WP_Customize_Manager instance.
324                 */
325                $components = apply_filters( 'customize_loaded_components', $this->components, $this );
326
327                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-selective-refresh.php' );
328                $this->selective_refresh = new WP_Customize_Selective_Refresh( $this );
329
330                if ( in_array( 'widgets', $components, true ) ) {
331                        require_once( ABSPATH . WPINC . '/class-wp-customize-widgets.php' );
332                        $this->widgets = new WP_Customize_Widgets( $this );
333                }
334
335                if ( in_array( 'nav_menus', $components, true ) ) {
336                        require_once( ABSPATH . WPINC . '/class-wp-customize-nav-menus.php' );
337                        $this->nav_menus = new WP_Customize_Nav_Menus( $this );
338                }
339
340                add_action( 'setup_theme', array( $this, 'setup_theme' ) );
341                add_action( 'wp_loaded',   array( $this, 'wp_loaded' ) );
342
343                // Do not spawn cron (especially the alternate cron) while running the Customizer.
344                remove_action( 'init', 'wp_cron' );
345
346                // Do not run update checks when rendering the controls.
347                remove_action( 'admin_init', '_maybe_update_core' );
348                remove_action( 'admin_init', '_maybe_update_plugins' );
349                remove_action( 'admin_init', '_maybe_update_themes' );
350
351                add_action( 'wp_ajax_customize_save',           array( $this, 'save' ) );
352                add_action( 'wp_ajax_customize_refresh_nonces', array( $this, 'refresh_nonces' ) );
353
354                add_action( 'customize_register',                 array( $this, 'register_controls' ) );
355                add_action( 'customize_register',                 array( $this, 'register_dynamic_settings' ), 11 ); // allow code to create settings first
356                add_action( 'customize_controls_init',            array( $this, 'prepare_controls' ) );
357                add_action( 'customize_controls_enqueue_scripts', array( $this, 'enqueue_control_scripts' ) );
358
359                // Render Panel, Section, and Control templates.
360                add_action( 'customize_controls_print_footer_scripts', array( $this, 'render_panel_templates' ), 1 );
361                add_action( 'customize_controls_print_footer_scripts', array( $this, 'render_section_templates' ), 1 );
362                add_action( 'customize_controls_print_footer_scripts', array( $this, 'render_control_templates' ), 1 );
363
364                // Export header video settings with the partial response.
365                add_filter( 'customize_render_partials_response', array( $this, 'export_header_video_settings' ), 10, 3 );
366
367                // Export the settings to JS via the _wpCustomizeSettings variable.
368                add_action( 'customize_controls_print_footer_scripts', array( $this, 'customize_pane_settings' ), 1000 );
369        }
370
371        /**
372         * Return true if it's an Ajax request.
373         *
374         * @since 3.4.0
375         * @since 4.2.0 Added `$action` param.
376         * @access public
377         *
378         * @param string|null $action Whether the supplied Ajax action is being run.
379         * @return bool True if it's an Ajax request, false otherwise.
380         */
381        public function doing_ajax( $action = null ) {
382                if ( ! wp_doing_ajax() ) {
383                        return false;
384                }
385
386                if ( ! $action ) {
387                        return true;
388                } else {
389                        /*
390                         * Note: we can't just use doing_action( "wp_ajax_{$action}" ) because we need
391                         * to check before admin-ajax.php gets to that point.
392                         */
393                        return isset( $_REQUEST['action'] ) && wp_unslash( $_REQUEST['action'] ) === $action;
394                }
395        }
396
397        /**
398         * Custom wp_die wrapper. Returns either the standard message for UI
399         * or the Ajax message.
400         *
401         * @since 3.4.0
402         *
403         * @param mixed $ajax_message Ajax return
404         * @param mixed $message UI message
405         */
406        protected function wp_die( $ajax_message, $message = null ) {
407                if ( $this->doing_ajax() ) {
408                        wp_die( $ajax_message );
409                }
410
411                if ( ! $message ) {
412                        $message = __( 'Cheatin&#8217; uh?' );
413                }
414
415                if ( $this->messenger_channel ) {
416                        ob_start();
417                        wp_enqueue_scripts();
418                        wp_print_scripts( array( 'customize-base' ) );
419
420                        $settings = array(
421                                'messengerArgs' => array(
422                                        'channel' => $this->messenger_channel,
423                                        'url' => wp_customize_url(),
424                                ),
425                                'error' => $ajax_message,
426                        );
427                        ?>
428                        <script>
429                        ( function( api, settings ) {
430                                var preview = new api.Messenger( settings.messengerArgs );
431                                preview.send( 'iframe-loading-error', settings.error );
432                        } )( wp.customize, <?php echo wp_json_encode( $settings ) ?> );
433                        </script>
434                        <?php
435                        $message .= ob_get_clean();
436                }
437
438                wp_die( $message );
439        }
440
441        /**
442         * Return the Ajax wp_die() handler if it's a customized request.
443         *
444         * @since 3.4.0
445         * @deprecated 4.7.0
446         *
447         * @return callable Die handler.
448         */
449        public function wp_die_handler() {
450                _deprecated_function( __METHOD__, '4.7.0' );
451
452                if ( $this->doing_ajax() || isset( $_POST['customized'] ) ) {
453                        return '_ajax_wp_die_handler';
454                }
455
456                return '_default_wp_die_handler';
457        }
458
459        /**
460         * Start preview and customize theme.
461         *
462         * Check if customize query variable exist. Init filters to filter the current theme.
463         *
464         * @since 3.4.0
465         */
466        public function setup_theme() {
467                global $pagenow;
468
469                // Check permissions for customize.php access since this method is called before customize.php can run any code,
470                if ( 'customize.php' === $pagenow && ! current_user_can( 'customize' ) ) {
471                        if ( ! is_user_logged_in() ) {
472                                auth_redirect();
473                        } else {
474                                wp_die(
475                                        '<h1>' . __( 'Cheatin&#8217; uh?' ) . '</h1>' .
476                                        '<p>' . __( 'Sorry, you are not allowed to customize this site.' ) . '</p>',
477                                        403
478                                );
479                        }
480                        return;
481                }
482
483                if ( ! preg_match( '/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/', $this->_changeset_uuid ) ) {
484                        $this->wp_die( -1, __( 'Invalid changeset UUID' ) );
485                }
486
487                /*
488                 * If unauthenticated then require a valid changeset UUID to load the preview.
489                 * In this way, the UUID serves as a secret key. If the messenger channel is present,
490                 * then send unauthenticated code to prompt re-auth.
491                 */
492                if ( ! current_user_can( 'customize' ) && ! $this->changeset_post_id() ) {
493                        $this->wp_die( $this->messenger_channel ? 0 : -1, __( 'Non-existent changeset UUID.' ) );
494                }
495
496                if ( ! headers_sent() ) {
497                        send_origin_headers();
498                }
499
500                // Hide the admin bar if we're embedded in the customizer iframe.
501                if ( $this->messenger_channel ) {
502                        show_admin_bar( false );
503                }
504
505                if ( $this->is_theme_active() ) {
506                        // Once the theme is loaded, we'll validate it.
507                        add_action( 'after_setup_theme', array( $this, 'after_setup_theme' ) );
508                } else {
509                        // If the requested theme is not the active theme and the user doesn't have the
510                        // switch_themes cap, bail.
511                        if ( ! current_user_can( 'switch_themes' ) ) {
512                                $this->wp_die( -1, __( 'Sorry, you are not allowed to edit theme options on this site.' ) );
513                        }
514
515                        // If the theme has errors while loading, bail.
516                        if ( $this->theme()->errors() ) {
517                                $this->wp_die( -1, $this->theme()->errors()->get_error_message() );
518                        }
519
520                        // If the theme isn't allowed per multisite settings, bail.
521                        if ( ! $this->theme()->is_allowed() ) {
522                                $this->wp_die( -1, __( 'The requested theme does not exist.' ) );
523                        }
524                }
525
526                /*
527                 * Import theme starter content for fresh installs when landing in the customizer.
528                 * Import starter content at after_setup_theme:100 so that any
529                 * add_theme_support( 'starter-content' ) calls will have been made.
530                 */
531                if ( get_option( 'fresh_site' ) && 'customize.php' === $pagenow ) {
532                        add_action( 'after_setup_theme', array( $this, 'import_theme_starter_content' ), 100 );
533                }
534
535                $this->start_previewing_theme();
536        }
537
538        /**
539         * Callback to validate a theme once it is loaded
540         *
541         * @since 3.4.0
542         */
543        public function after_setup_theme() {
544                $doing_ajax_or_is_customized = ( $this->doing_ajax() || isset( $_POST['customized'] ) );
545                if ( ! $doing_ajax_or_is_customized && ! validate_current_theme() ) {
546                        wp_redirect( 'themes.php?broken=true' );
547                        exit;
548                }
549        }
550
551        /**
552         * If the theme to be previewed isn't the active theme, add filter callbacks
553         * to swap it out at runtime.
554         *
555         * @since 3.4.0
556         */
557        public function start_previewing_theme() {
558                // Bail if we're already previewing.
559                if ( $this->is_preview() ) {
560                        return;
561                }
562
563                $this->previewing = true;
564
565                if ( ! $this->is_theme_active() ) {
566                        add_filter( 'template', array( $this, 'get_template' ) );
567                        add_filter( 'stylesheet', array( $this, 'get_stylesheet' ) );
568                        add_filter( 'pre_option_current_theme', array( $this, 'current_theme' ) );
569
570                        // @link: https://core.trac.wordpress.org/ticket/20027
571                        add_filter( 'pre_option_stylesheet', array( $this, 'get_stylesheet' ) );
572                        add_filter( 'pre_option_template', array( $this, 'get_template' ) );
573
574                        // Handle custom theme roots.
575                        add_filter( 'pre_option_stylesheet_root', array( $this, 'get_stylesheet_root' ) );
576                        add_filter( 'pre_option_template_root', array( $this, 'get_template_root' ) );
577                }
578
579                /**
580                 * Fires once the Customizer theme preview has started.
581                 *
582                 * @since 3.4.0
583                 *
584                 * @param WP_Customize_Manager $this WP_Customize_Manager instance.
585                 */
586                do_action( 'start_previewing_theme', $this );
587        }
588
589        /**
590         * Stop previewing the selected theme.
591         *
592         * Removes filters to change the current theme.
593         *
594         * @since 3.4.0
595         */
596        public function stop_previewing_theme() {
597                if ( ! $this->is_preview() ) {
598                        return;
599                }
600
601                $this->previewing = false;
602
603                if ( ! $this->is_theme_active() ) {
604                        remove_filter( 'template', array( $this, 'get_template' ) );
605                        remove_filter( 'stylesheet', array( $this, 'get_stylesheet' ) );
606                        remove_filter( 'pre_option_current_theme', array( $this, 'current_theme' ) );
607
608                        // @link: https://core.trac.wordpress.org/ticket/20027
609                        remove_filter( 'pre_option_stylesheet', array( $this, 'get_stylesheet' ) );
610                        remove_filter( 'pre_option_template', array( $this, 'get_template' ) );
611
612                        // Handle custom theme roots.
613                        remove_filter( 'pre_option_stylesheet_root', array( $this, 'get_stylesheet_root' ) );
614                        remove_filter( 'pre_option_template_root', array( $this, 'get_template_root' ) );
615                }
616
617                /**
618                 * Fires once the Customizer theme preview has stopped.
619                 *
620                 * @since 3.4.0
621                 *
622                 * @param WP_Customize_Manager $this WP_Customize_Manager instance.
623                 */
624                do_action( 'stop_previewing_theme', $this );
625        }
626
627        /**
628         * Get the changeset UUID.
629         *
630         * @since 4.7.0
631         * @access public
632         *
633         * @return string UUID.
634         */
635        public function changeset_uuid() {
636                return $this->_changeset_uuid;
637        }
638
639        /**
640         * Get the theme being customized.
641         *
642         * @since 3.4.0
643         *
644         * @return WP_Theme
645         */
646        public function theme() {
647                if ( ! $this->theme ) {
648                        $this->theme = wp_get_theme();
649                }
650                return $this->theme;
651        }
652
653        /**
654         * Get the registered settings.
655         *
656         * @since 3.4.0
657         *
658         * @return array
659         */
660        public function settings() {
661                return $this->settings;
662        }
663
664        /**
665         * Get the registered controls.
666         *
667         * @since 3.4.0
668         *
669         * @return array
670         */
671        public function controls() {
672                return $this->controls;
673        }
674
675        /**
676         * Get the registered containers.
677         *
678         * @since 4.0.0
679         *
680         * @return array
681         */
682        public function containers() {
683                return $this->containers;
684        }
685
686        /**
687         * Get the registered sections.
688         *
689         * @since 3.4.0
690         *
691         * @return array
692         */
693        public function sections() {
694                return $this->sections;
695        }
696
697        /**
698         * Get the registered panels.
699         *
700         * @since 4.0.0
701         * @access public
702         *
703         * @return array Panels.
704         */
705        public function panels() {
706                return $this->panels;
707        }
708
709        /**
710         * Checks if the current theme is active.
711         *
712         * @since 3.4.0
713         *
714         * @return bool
715         */
716        public function is_theme_active() {
717                return $this->get_stylesheet() == $this->original_stylesheet;
718        }
719
720        /**
721         * Register styles/scripts and initialize the preview of each setting
722         *
723         * @since 3.4.0
724         */
725        public function wp_loaded() {
726
727                /**
728                 * Fires once WordPress has loaded, allowing scripts and styles to be initialized.
729                 *
730                 * @since 3.4.0
731                 *
732                 * @param WP_Customize_Manager $this WP_Customize_Manager instance.
733                 */
734                do_action( 'customize_register', $this );
735
736                /*
737                 * Note that settings must be previewed here even outside the customizer preview
738                 * and also in the customizer pane itself. This is to enable loading an existing
739                 * changeset into the customizer. Previewing the settings only has to be prevented
740                 * in the case of a customize_save action because then update_option()
741                 * may short-circuit because it will detect that there are no changes to
742                 * make.
743                 */
744                if ( ! $this->doing_ajax( 'customize_save' ) ) {
745                        foreach ( $this->settings as $setting ) {
746                                $setting->preview();
747                        }
748                }
749
750                if ( $this->is_preview() && ! is_admin() ) {
751                        $this->customize_preview_init();
752                }
753        }
754
755        /**
756         * Prevents Ajax requests from following redirects when previewing a theme
757         * by issuing a 200 response instead of a 30x.
758         *
759         * Instead, the JS will sniff out the location header.
760         *
761         * @since 3.4.0
762         * @deprecated 4.7.0
763         *
764         * @param int $status Status.
765         * @return int
766         */
767        public function wp_redirect_status( $status ) {
768                _deprecated_function( __FUNCTION__, '4.7.0' );
769
770                if ( $this->is_preview() && ! is_admin() ) {
771                        return 200;
772                }
773
774                return $status;
775        }
776
777        /**
778         * Find the changeset post ID for a given changeset UUID.
779         *
780         * @since 4.7.0
781         * @access public
782         *
783         * @param string $uuid Changeset UUID.
784         * @return int|null Returns post ID on success and null on failure.
785         */
786        public function find_changeset_post_id( $uuid ) {
787                $cache_group = 'customize_changeset_post';
788                $changeset_post_id = wp_cache_get( $uuid, $cache_group );
789                if ( $changeset_post_id && 'customize_changeset' === get_post_type( $changeset_post_id ) ) {
790                        return $changeset_post_id;
791                }
792
793                $changeset_post_query = new WP_Query( array(
794                        'post_type' => 'customize_changeset',
795                        'post_status' => get_post_stati(),
796                        'name' => $uuid,
797                        'number' => 1,
798                        'no_found_rows' => true,
799                        'cache_results' => true,
800                        'update_post_meta_cache' => false,
801                        'update_term_meta_cache' => false,
802                ) );
803                if ( ! empty( $changeset_post_query->posts ) ) {
804                        // Note: 'fields'=>'ids' is not being used in order to cache the post object as it will be needed.
805                        $changeset_post_id = $changeset_post_query->posts[0]->ID;
806                        wp_cache_set( $this->_changeset_uuid, $changeset_post_id, $cache_group );
807                        return $changeset_post_id;
808                }
809
810                return null;
811        }
812
813        /**
814         * Get the changeset post id for the loaded changeset.
815         *
816         * @since 4.7.0
817         * @access public
818         *
819         * @return int|null Post ID on success or null if there is no post yet saved.
820         */
821        public function changeset_post_id() {
822                if ( ! isset( $this->_changeset_post_id ) ) {
823                        $post_id = $this->find_changeset_post_id( $this->_changeset_uuid );
824                        if ( ! $post_id ) {
825                                $post_id = false;
826                        }
827                        $this->_changeset_post_id = $post_id;
828                }
829                if ( false === $this->_changeset_post_id ) {
830                        return null;
831                }
832                return $this->_changeset_post_id;
833        }
834
835        /**
836         * Get the data stored in a changeset post.
837         *
838         * @since 4.7.0
839         * @access protected
840         *
841         * @param int $post_id Changeset post ID.
842         * @return array|WP_Error Changeset data or WP_Error on error.
843         */
844        protected function get_changeset_post_data( $post_id ) {
845                if ( ! $post_id ) {
846                        return new WP_Error( 'empty_post_id' );
847                }
848                $changeset_post = get_post( $post_id );
849                if ( ! $changeset_post ) {
850                        return new WP_Error( 'missing_post' );
851                }
852                if ( 'customize_changeset' !== $changeset_post->post_type ) {
853                        return new WP_Error( 'wrong_post_type' );
854                }
855                $changeset_data = json_decode( $changeset_post->post_content, true );
856                if ( function_exists( 'json_last_error' ) && json_last_error() ) {
857                        return new WP_Error( 'json_parse_error', '', json_last_error() );
858                }
859                if ( ! is_array( $changeset_data ) ) {
860                        return new WP_Error( 'expected_array' );
861                }
862                return $changeset_data;
863        }
864
865        /**
866         * Get changeset data.
867         *
868         * @since 4.7.0
869         * @access public
870         *
871         * @return array Changeset data.
872         */
873        public function changeset_data() {
874                if ( isset( $this->_changeset_data ) ) {
875                        return $this->_changeset_data;
876                }
877                $changeset_post_id = $this->changeset_post_id();
878                if ( ! $changeset_post_id ) {
879                        $this->_changeset_data = array();
880                } else {
881                        $data = $this->get_changeset_post_data( $changeset_post_id );
882                        if ( ! is_wp_error( $data ) ) {
883                                $this->_changeset_data = $data;
884                        } else {
885                                $this->_changeset_data = array();
886                        }
887                }
888                return $this->_changeset_data;
889        }
890
891        /**
892         * Starter content setting IDs.
893         *
894         * @since 4.7.0
895         * @access private
896         * @var array
897         */
898        protected $starter_content_settings_ids = array();
899
900        /**
901         * Import theme starter content into the customized state.
902         *
903         * @since 4.7.0
904         * @access public
905         *
906         * @param array $starter_content Starter content. Defaults to `get_theme_starter_content()`.
907         */
908        function import_theme_starter_content( $starter_content = array() ) {
909                if ( empty( $starter_content ) ) {
910                        $starter_content = get_theme_starter_content();
911                }
912
913                $changeset_data = array();
914                if ( $this->changeset_post_id() ) {
915                        $changeset_data = $this->get_changeset_post_data( $this->changeset_post_id() );
916                }
917
918                $sidebars_widgets = isset( $starter_content['widgets'] ) && ! empty( $this->widgets ) ? $starter_content['widgets'] : array();
919                $posts = isset( $starter_content['posts'] ) && ! empty( $this->nav_menus ) ? $starter_content['posts'] : array();
920                $options = isset( $starter_content['options'] ) ? $starter_content['options'] : array();
921                $nav_menus = isset( $starter_content['nav_menus'] ) && ! empty( $this->nav_menus ) ? $starter_content['nav_menus'] : array();
922                $theme_mods = isset( $starter_content['theme_mods'] ) ? $starter_content['theme_mods'] : array();
923
924                // Widgets.
925                $max_widget_numbers = array();
926                foreach ( $sidebars_widgets as $sidebar_id => $widgets ) {
927                        $sidebar_widget_ids = array();
928                        foreach ( $widgets as $widget ) {
929                                list( $id_base, $instance ) = $widget;
930
931                                if ( ! isset( $max_widget_numbers[ $id_base ] ) ) {
932
933                                        // When $settings is an array-like object, get an intrinsic array for use with array_keys().
934                                        $settings = get_option( "widget_{$id_base}", array() );
935                                        if ( $settings instanceof ArrayObject || $settings instanceof ArrayIterator ) {
936                                                $settings = $settings->getArrayCopy();
937                                        }
938
939                                        // Find the max widget number for this type.
940                                        $widget_numbers = array_keys( $settings );
941                                        if ( count( $widget_numbers ) > 0 ) {
942                                                $widget_numbers[] = 1;
943                                                $max_widget_numbers[ $id_base ] = call_user_func_array( 'max', $widget_numbers );
944                                        } else {
945                                                $max_widget_numbers[ $id_base ] = 1;
946                                        }
947                                }
948                                $max_widget_numbers[ $id_base ] += 1;
949
950                                $widget_id = sprintf( '%s-%d', $id_base, $max_widget_numbers[ $id_base ] );
951                                $setting_id = sprintf( 'widget_%s[%d]', $id_base, $max_widget_numbers[ $id_base ] );
952
953                                $setting_value = $this->widgets->sanitize_widget_js_instance( $instance );
954                                if ( empty( $changeset_data[ $setting_id ] ) || ! empty( $changeset_data[ $setting_id ]['starter_content'] ) ) {
955                                        $this->set_post_value( $setting_id, $setting_value );
956                                        $this->starter_content_settings_ids[] = $setting_id;
957                                }
958                                $sidebar_widget_ids[] = $widget_id;
959                        }
960
961                        $setting_id = sprintf( 'sidebars_widgets[%s]', $sidebar_id );
962                        if ( empty( $changeset_data[ $setting_id ] ) || ! empty( $changeset_data[ $setting_id ]['starter_content'] ) ) {
963                                $this->set_post_value( $setting_id, $sidebar_widget_ids );
964                                $this->starter_content_settings_ids[] = $setting_id;
965                        }
966                }
967
968                // Posts & pages.
969                if ( ! empty( $posts ) ) {
970                        $nav_menus_created_posts = array();
971                        if ( ! empty( $changeset_data['nav_menus_created_posts']['value'] ) ) {
972                                $nav_menus_created_posts = $changeset_data['nav_menus_created_posts']['value'];
973                        }
974
975                        $existing_posts = array();
976                        if ( ! empty( $nav_menus_created_posts ) ) {
977                                $existing_posts_query = new WP_Query( array(
978                                        'post__in' => $nav_menus_created_posts,
979                                        'post_status' => 'auto-draft',
980                                        'post_type' => 'any',
981                                        'number' => -1,
982                                ) );
983                                foreach ( $existing_posts_query->posts as $existing_post ) {
984                                        $existing_posts[ $existing_post->post_type . ':' . $existing_post->post_name ] = $existing_post;
985                                }
986                        }
987
988                        foreach ( array_keys( $posts ) as $post_symbol ) {
989                                if ( empty( $posts[ $post_symbol ]['post_type'] ) ) {
990                                        continue;
991                                }
992                                $post_type = $posts[ $post_symbol ]['post_type'];
993                                if ( ! empty( $posts[ $post_symbol ]['post_name'] ) ) {
994                                        $post_name = $posts[ $post_symbol ]['post_name'];
995                                } elseif ( ! empty( $posts[ $post_symbol ]['post_title'] ) ) {
996                                        $post_name = sanitize_title( $posts[ $post_symbol ]['post_title'] );
997                                } else {
998                                        continue;
999                                }
1000
1001                                // Use existing auto-draft post if one already exists with the same type and name.
1002                                if ( isset( $existing_posts[ $post_type . ':' . $post_name ] ) ) {
1003                                        $posts[ $post_symbol ]['ID'] = $existing_posts[ $post_type . ':' . $post_name ]->ID;
1004                                        continue;
1005                                }
1006
1007                                $r = $this->nav_menus->insert_auto_draft_post( $posts[ $post_symbol ] );
1008                                if ( $r instanceof WP_Post ) {
1009                                        $posts[ $post_symbol ]['ID'] = $r->ID;
1010                                }
1011                        }
1012
1013                        // The nav_menus_created_posts setting is why nav_menus component is dependency for adding posts.
1014                        $setting_id = 'nav_menus_created_posts';
1015                        if ( empty( $changeset_data[ $setting_id ] ) || ! empty( $changeset_data[ $setting_id ]['starter_content'] ) ) {
1016                                $nav_menus_created_posts = array_unique( array_merge( $nav_menus_created_posts, wp_list_pluck( $posts, 'ID' ) ) );
1017                                $this->set_post_value( $setting_id, array_values( $nav_menus_created_posts ) );
1018                                $this->starter_content_settings_ids[] = $setting_id;
1019                        }
1020                }
1021
1022                // Nav menus.
1023                $placeholder_id = -1;
1024                $reused_nav_menu_setting_ids = array();
1025                foreach ( $nav_menus as $nav_menu_location => $nav_menu ) {
1026
1027                        $nav_menu_term_id = null;
1028                        $nav_menu_setting_id = null;
1029                        $matches = array();
1030
1031                        // Look for an existing placeholder menu with starter content to re-use.
1032                        foreach ( $changeset_data as $setting_id => $setting_params ) {
1033                                $can_reuse = (
1034                                        ! empty( $setting_params['starter_content'] )
1035                                        &&
1036                                        ! in_array( $setting_id, $reused_nav_menu_setting_ids, true )
1037                                        &&
1038                                        preg_match( '#^nav_menu\[(?P<nav_menu_id>-?\d+)\]$#', $setting_id, $matches )
1039                                );
1040                                if ( $can_reuse ) {
1041                                        $nav_menu_term_id = intval( $matches['nav_menu_id'] );
1042                                        $nav_menu_setting_id = $setting_id;
1043                                        $reused_nav_menu_setting_ids[] = $setting_id;
1044                                        break;
1045                                }
1046                        }
1047
1048                        if ( ! $nav_menu_term_id ) {
1049                                while ( isset( $changeset_data[ sprintf( 'nav_menu[%d]', $placeholder_id ) ] ) ) {
1050                                        $placeholder_id--;
1051                                }
1052                                $nav_menu_term_id = $placeholder_id;
1053                                $nav_menu_setting_id = sprintf( 'nav_menu[%d]', $placeholder_id );
1054                        }
1055
1056                        $this->set_post_value( $nav_menu_setting_id, array(
1057                                'name' => isset( $nav_menu['name'] ) ? $nav_menu['name'] : $nav_menu_location,
1058                        ) );
1059                        $this->starter_content_settings_ids[] = $nav_menu_setting_id;
1060
1061                        // @todo Add support for menu_item_parent.
1062                        $position = 0;
1063                        foreach ( $nav_menu['items'] as $nav_menu_item ) {
1064                                $nav_menu_item_setting_id = sprintf( 'nav_menu_item[%d]', $placeholder_id-- );
1065                                if ( ! isset( $nav_menu_item['position'] ) ) {
1066                                        $nav_menu_item['position'] = $position++;
1067                                }
1068                                $nav_menu_item['nav_menu_term_id'] = $nav_menu_term_id;
1069
1070                                if ( isset( $nav_menu_item['object_id'] ) ) {
1071                                        if ( 'post_type' === $nav_menu_item['type'] && preg_match( '/^{{(?P<symbol>.+)}}$/', $nav_menu_item['object_id'], $matches ) && isset( $posts[ $matches['symbol'] ] ) ) {
1072                                                $nav_menu_item['object_id'] = $posts[ $matches['symbol'] ]['ID'];
1073                                                if ( empty( $nav_menu_item['title'] ) ) {
1074                                                        $original_object = get_post( $nav_menu_item['object_id'] );
1075                                                        $nav_menu_item['title'] = $original_object->post_title;
1076                                                }
1077                                        } else {
1078                                                continue;
1079                                        }
1080                                } else {
1081                                        $nav_menu_item['object_id'] = 0;
1082                                }
1083
1084                                if ( empty( $changeset_data[ $nav_menu_item_setting_id ] ) || ! empty( $changeset_data[ $nav_menu_item_setting_id ]['starter_content'] ) ) {
1085                                        $this->set_post_value( $nav_menu_item_setting_id, $nav_menu_item );
1086                                        $this->starter_content_settings_ids[] = $nav_menu_item_setting_id;
1087                                }
1088                        }
1089
1090                        $setting_id = sprintf( 'nav_menu_locations[%s]', $nav_menu_location );
1091                        if ( empty( $changeset_data[ $setting_id ] ) || ! empty( $changeset_data[ $setting_id ]['starter_content'] ) ) {
1092                                $this->set_post_value( $setting_id, $nav_menu_term_id );
1093                                $this->starter_content_settings_ids[] = $setting_id;
1094                        }
1095                }
1096
1097                // Options.
1098                foreach ( $options as $name => $value ) {
1099                        if ( preg_match( '/^{{(?P<symbol>.+)}}$/', $value, $matches ) && isset( $posts[ $matches['symbol'] ] ) ) {
1100                                $value = $posts[ $matches['symbol'] ]['ID'];
1101                        }
1102
1103                        if ( empty( $changeset_data[ $name ] ) || ! empty( $changeset_data[ $name ]['starter_content'] ) ) {
1104                                $this->set_post_value( $name, $value );
1105                                $this->starter_content_settings_ids[] = $name;
1106                        }
1107                }
1108
1109                // Theme mods.
1110                foreach ( $theme_mods as $name => $value ) {
1111                        if ( preg_match( '/^{{(?P<symbol>.+)}}$/', $value, $matches ) && isset( $posts[ $matches['symbol'] ] ) ) {
1112                                $value = $posts[ $matches['symbol'] ]['ID'];
1113                        }
1114
1115                        if ( empty( $changeset_data[ $name ] ) || ! empty( $changeset_data[ $name ]['starter_content'] ) ) {
1116                                $this->set_post_value( $name, $value );
1117                                $this->starter_content_settings_ids[] = $name;
1118                        }
1119                }
1120
1121                if ( ! empty( $this->starter_content_settings_ids ) ) {
1122                        if ( did_action( 'customize_register' ) ) {
1123                                $this->_save_starter_content_changeset();
1124                        } else {
1125                                add_action( 'customize_register', array( $this, '_save_starter_content_changeset' ), 1000 );
1126                        }
1127                }
1128        }
1129
1130        /**
1131         * Save starter content changeset.
1132         *
1133         * @since 4.7.0
1134         * @access private
1135         */
1136        public function _save_starter_content_changeset() {
1137
1138                if ( empty( $this->starter_content_settings_ids ) ) {
1139                        return;
1140                }
1141
1142                $this->save_changeset_post( array(
1143                        'data' => array_fill_keys( $this->starter_content_settings_ids, array( 'starter_content' => true ) ),
1144                        'starter_content' => true,
1145                ) );
1146        }
1147
1148        /**
1149         * Get dirty pre-sanitized setting values in the current customized state.
1150         *
1151         * The returned array consists of a merge of three sources:
1152         * 1. If the theme is not currently active, then the base array is any stashed
1153         *    theme mods that were modified previously but never published.
1154         * 2. The values from the current changeset, if it exists.
1155         * 3. If the user can customize, the values parsed from the incoming
1156         *    `$_POST['customized']` JSON data.
1157         * 4. Any programmatically-set post values via `WP_Customize_Manager::set_post_value()`.
1158         *
1159         * The name "unsanitized_post_values" is a carry-over from when the customized
1160         * state was exclusively sourced from `$_POST['customized']`. Nevertheless,
1161         * the value returned will come from the current changeset post and from the
1162         * incoming post data.
1163         *
1164         * @since 4.1.1
1165         * @since 4.7.0 Added $args param and merging with changeset values and stashed theme mods.
1166         *
1167         * @param array $args {
1168         *     Args.
1169         *
1170         *     @type bool $exclude_changeset Whether the changeset values should also be excluded. Defaults to false.
1171         *     @type bool $exclude_post_data Whether the post input values should also be excluded. Defaults to false when lacking the customize capability.
1172         * }
1173         * @return array
1174         */
1175        public function unsanitized_post_values( $args = array() ) {
1176                $args = array_merge(
1177                        array(
1178                                'exclude_changeset' => false,
1179                                'exclude_post_data' => ! current_user_can( 'customize' ),
1180                        ),
1181                        $args
1182                );
1183
1184                $values = array();
1185
1186                // Let default values be from the stashed theme mods if doing a theme switch and if no changeset is present.
1187                if ( ! $this->is_theme_active() ) {
1188                        $stashed_theme_mods = get_option( 'customize_stashed_theme_mods' );
1189                        $stylesheet = $this->get_stylesheet();
1190                        if ( isset( $stashed_theme_mods[ $stylesheet ] ) ) {
1191                                $values = array_merge( $values, wp_list_pluck( $stashed_theme_mods[ $stylesheet ], 'value' ) );
1192                        }
1193                }
1194
1195                if ( ! $args['exclude_changeset'] ) {
1196                        foreach ( $this->changeset_data() as $setting_id => $setting_params ) {
1197                                if ( ! array_key_exists( 'value', $setting_params ) ) {
1198                                        continue;
1199                                }
1200                                if ( isset( $setting_params['type'] ) && 'theme_mod' === $setting_params['type'] ) {
1201
1202                                        // Ensure that theme mods values are only used if they were saved under the current theme.
1203                                        $namespace_pattern = '/^(?P<stylesheet>.+?)::(?P<setting_id>.+)$/';
1204                                        if ( preg_match( $namespace_pattern, $setting_id, $matches ) && $this->get_stylesheet() === $matches['stylesheet'] ) {
1205                                                $values[ $matches['setting_id'] ] = $setting_params['value'];
1206                                        }
1207                                } else {
1208                                        $values[ $setting_id ] = $setting_params['value'];
1209                                }
1210                        }
1211                }
1212
1213                if ( ! $args['exclude_post_data'] ) {
1214                        if ( ! isset( $this->_post_values ) ) {
1215                                if ( isset( $_POST['customized'] ) ) {
1216                                        $post_values = json_decode( wp_unslash( $_POST['customized'] ), true );
1217                                } else {
1218                                        $post_values = array();
1219                                }
1220                                if ( is_array( $post_values ) ) {
1221                                        $this->_post_values = $post_values;
1222                                } else {
1223                                        $this->_post_values = array();
1224                                }
1225                        }
1226                        $values = array_merge( $values, $this->_post_values );
1227                }
1228                return $values;
1229        }
1230
1231        /**
1232         * Returns the sanitized value for a given setting from the current customized state.
1233         *
1234         * The name "post_value" is a carry-over from when the customized state was exclusively
1235         * sourced from `$_POST['customized']`. Nevertheless, the value returned will come
1236         * from the current changeset post and from the incoming post data.
1237         *
1238         * @since 3.4.0
1239         * @since 4.1.1 Introduced the `$default` parameter.
1240         * @since 4.6.0 `$default` is now returned early when the setting post value is invalid.
1241         * @access public
1242         *
1243         * @see WP_REST_Server::dispatch()
1244         * @see WP_Rest_Request::sanitize_params()
1245         * @see WP_Rest_Request::has_valid_params()
1246         *
1247         * @param WP_Customize_Setting $setting A WP_Customize_Setting derived object.
1248         * @param mixed                $default Value returned $setting has no post value (added in 4.2.0)
1249         *                                      or the post value is invalid (added in 4.6.0).
1250         * @return string|mixed $post_value Sanitized value or the $default provided.
1251         */
1252        public function post_value( $setting, $default = null ) {
1253                $post_values = $this->unsanitized_post_values();
1254                if ( ! array_key_exists( $setting->id, $post_values ) ) {
1255                        return $default;
1256                }
1257                $value = $post_values[ $setting->id ];
1258                $valid = $setting->validate( $value );
1259                if ( is_wp_error( $valid ) ) {
1260                        return $default;
1261                }
1262                $value = $setting->sanitize( $value );
1263                if ( is_null( $value ) || is_wp_error( $value ) ) {
1264                        return $default;
1265                }
1266                return $value;
1267        }
1268
1269        /**
1270         * Override a setting's value in the current customized state.
1271         *
1272         * The name "post_value" is a carry-over from when the customized state was
1273         * exclusively sourced from `$_POST['customized']`.
1274         *
1275         * @since 4.2.0
1276         * @access public
1277         *
1278         * @param string $setting_id ID for the WP_Customize_Setting instance.
1279         * @param mixed  $value      Post value.
1280         */
1281        public function set_post_value( $setting_id, $value ) {
1282                $this->unsanitized_post_values(); // Populate _post_values from $_POST['customized'].
1283                $this->_post_values[ $setting_id ] = $value;
1284
1285                /**
1286                 * Announce when a specific setting's unsanitized post value has been set.
1287                 *
1288                 * Fires when the WP_Customize_Manager::set_post_value() method is called.
1289                 *
1290                 * The dynamic portion of the hook name, `$setting_id`, refers to the setting ID.
1291                 *
1292                 * @since 4.4.0
1293                 *
1294                 * @param mixed                $value Unsanitized setting post value.
1295                 * @param WP_Customize_Manager $this  WP_Customize_Manager instance.
1296                 */
1297                do_action( "customize_post_value_set_{$setting_id}", $value, $this );
1298
1299                /**
1300                 * Announce when any setting's unsanitized post value has been set.
1301                 *
1302                 * Fires when the WP_Customize_Manager::set_post_value() method is called.
1303                 *
1304                 * This is useful for `WP_Customize_Setting` instances to watch
1305                 * in order to update a cached previewed value.
1306                 *
1307                 * @since 4.4.0
1308                 *
1309                 * @param string               $setting_id Setting ID.
1310                 * @param mixed                $value      Unsanitized setting post value.
1311                 * @param WP_Customize_Manager $this       WP_Customize_Manager instance.
1312                 */
1313                do_action( 'customize_post_value_set', $setting_id, $value, $this );
1314        }
1315
1316        /**
1317         * Print JavaScript settings.
1318         *
1319         * @since 3.4.0
1320         */
1321        public function customize_preview_init() {
1322
1323                /*
1324                 * Now that Customizer previews are loaded into iframes via GET requests
1325                 * and natural URLs with transaction UUIDs added, we need to ensure that
1326                 * the responses are never cached by proxies. In practice, this will not
1327                 * be needed if the user is logged-in anyway. But if anonymous access is
1328                 * allowed then the auth cookies would not be sent and WordPress would
1329                 * not send no-cache headers by default.
1330                 */
1331                if ( ! headers_sent() ) {
1332                        nocache_headers();
1333                        header( 'X-Robots: noindex, nofollow, noarchive' );
1334                }
1335                add_action( 'wp_head', 'wp_no_robots' );
1336                add_filter( 'wp_headers', array( $this, 'filter_iframe_security_headers' ) );
1337
1338                /*
1339                 * If preview is being served inside the customizer preview iframe, and
1340                 * if the user doesn't have customize capability, then it is assumed
1341                 * that the user's session has expired and they need to re-authenticate.
1342                 */
1343                if ( $this->messenger_channel && ! current_user_can( 'customize' ) ) {
1344                        $this->wp_die( -1, __( 'Unauthorized. You may remove the customize_messenger_channel param to preview as frontend.' ) );
1345                        return;
1346                }
1347
1348                $this->prepare_controls();
1349
1350                add_filter( 'wp_redirect', array( $this, 'add_state_query_params' ) );
1351
1352                wp_enqueue_script( 'customize-preview' );
1353                add_action( 'wp_head', array( $this, 'customize_preview_loading_style' ) );
1354                add_action( 'wp_footer', array( $this, 'customize_preview_settings' ), 20 );
1355                add_filter( 'get_edit_post_link', '__return_empty_string' );
1356
1357                /**
1358                 * Fires once the Customizer preview has initialized and JavaScript
1359                 * settings have been printed.
1360                 *
1361                 * @since 3.4.0
1362                 *
1363                 * @param WP_Customize_Manager $this WP_Customize_Manager instance.
1364                 */
1365                do_action( 'customize_preview_init', $this );
1366        }
1367
1368        /**
1369         * Filter the X-Frame-Options and Content-Security-Policy headers to ensure frontend can load in customizer.
1370         *
1371         * @since 4.7.0
1372         * @access public
1373         *
1374         * @param array $headers Headers.
1375         * @return array Headers.
1376         */
1377        public function filter_iframe_security_headers( $headers ) {
1378                $customize_url = admin_url( 'customize.php' );
1379                $headers['X-Frame-Options'] = 'ALLOW-FROM ' . $customize_url;
1380                $headers['Content-Security-Policy'] = 'frame-ancestors ' . preg_replace( '#^(\w+://[^/]+).+?$#', '$1', $customize_url );
1381                return $headers;
1382        }
1383
1384        /**
1385         * Add customize state query params to a given URL if preview is allowed.
1386         *
1387         * @since 4.7.0
1388         * @access public
1389         * @see wp_redirect()
1390         * @see WP_Customize_Manager::get_allowed_url()
1391         *
1392         * @param string $url URL.
1393         * @return string URL.
1394         */
1395        public function add_state_query_params( $url ) {
1396                $parsed_original_url = wp_parse_url( $url );
1397                $is_allowed = false;
1398                foreach ( $this->get_allowed_urls() as $allowed_url ) {
1399                        $parsed_allowed_url = wp_parse_url( $allowed_url );
1400                        $is_allowed = (
1401                                $parsed_allowed_url['scheme'] === $parsed_original_url['scheme']
1402                                &&
1403                                $parsed_allowed_url['host'] === $parsed_original_url['host']
1404                                &&
1405                                0 === strpos( $parsed_original_url['path'], $parsed_allowed_url['path'] )
1406                        );
1407                        if ( $is_allowed ) {
1408                                break;
1409                        }
1410                }
1411
1412                if ( $is_allowed ) {
1413                        $query_params = array(
1414                                'customize_changeset_uuid' => $this->changeset_uuid(),
1415                        );
1416                        if ( ! $this->is_theme_active() ) {
1417                                $query_params['customize_theme'] = $this->get_stylesheet();
1418                        }
1419                        if ( $this->messenger_channel ) {
1420                                $query_params['customize_messenger_channel'] = $this->messenger_channel;
1421                        }
1422                        $url = add_query_arg( $query_params, $url );
1423                }
1424
1425                return $url;
1426        }
1427
1428        /**
1429         * Prevent sending a 404 status when returning the response for the customize
1430         * preview, since it causes the jQuery Ajax to fail. Send 200 instead.
1431         *
1432         * @since 4.0.0
1433         * @deprecated 4.7.0
1434         * @access public
1435         */
1436        public function customize_preview_override_404_status() {
1437                _deprecated_function( __METHOD__, '4.7.0' );
1438        }
1439
1440        /**
1441         * Print base element for preview frame.
1442         *
1443         * @since 3.4.0
1444         * @deprecated 4.7.0
1445         */
1446        public function customize_preview_base() {
1447                _deprecated_function( __METHOD__, '4.7.0' );
1448        }
1449
1450        /**
1451         * Print a workaround to handle HTML5 tags in IE < 9.
1452         *
1453         * @since 3.4.0
1454         * @deprecated 4.7.0 Customizer no longer supports IE8, so all supported browsers recognize HTML5.
1455         */
1456        public function customize_preview_html5() {
1457                _deprecated_function( __FUNCTION__, '4.7.0' );
1458        }
1459
1460        /**
1461         * Print CSS for loading indicators for the Customizer preview.
1462         *
1463         * @since 4.2.0
1464         * @access public
1465         */
1466        public function customize_preview_loading_style() {
1467                ?><style>
1468                        body.wp-customizer-unloading {
1469                                opacity: 0.25;
1470                                cursor: progress !important;
1471                                -webkit-transition: opacity 0.5s;
1472                                transition: opacity 0.5s;
1473                        }
1474                        body.wp-customizer-unloading * {
1475                                pointer-events: none !important;
1476                        }
1477                        form.customize-unpreviewable,
1478                        form.customize-unpreviewable input,
1479                        form.customize-unpreviewable select,
1480                        form.customize-unpreviewable button,
1481                        a.customize-unpreviewable,
1482                        area.customize-unpreviewable {
1483                                cursor: not-allowed !important;
1484                        }
1485                </style><?php
1486        }
1487
1488        /**
1489         * Print JavaScript settings for preview frame.
1490         *
1491         * @since 3.4.0
1492         */
1493        public function customize_preview_settings() {
1494                $post_values = $this->unsanitized_post_values( array( 'exclude_changeset' => true ) );
1495                $setting_validities = $this->validate_setting_values( $post_values );
1496                $exported_setting_validities = array_map( array( $this, 'prepare_setting_validity_for_js' ), $setting_validities );
1497
1498                // Note that the REQUEST_URI is not passed into home_url() since this breaks subdirectory installs.
1499                $self_url = empty( $_SERVER['REQUEST_URI'] ) ? home_url( '/' ) : esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) );
1500                $state_query_params = array(
1501                        'customize_theme',
1502                        'customize_changeset_uuid',
1503                        'customize_messenger_channel',
1504                );
1505                $self_url = remove_query_arg( $state_query_params, $self_url );
1506
1507                $allowed_urls = $this->get_allowed_urls();
1508                $allowed_hosts = array();
1509                foreach ( $allowed_urls as $allowed_url ) {
1510                        $parsed = wp_parse_url( $allowed_url );
1511                        if ( empty( $parsed['host'] ) ) {
1512                                continue;
1513                        }
1514                        $host = $parsed['host'];
1515                        if ( ! empty( $parsed['port'] ) ) {
1516                                $host .= ':' . $parsed['port'];
1517                        }
1518                        $allowed_hosts[] = $host;
1519                }
1520                $settings = array(
1521                        'changeset' => array(
1522                                'uuid' => $this->_changeset_uuid,
1523                        ),
1524                        'timeouts' => array(
1525                                'selectiveRefresh' => 250,
1526                                'keepAliveSend' => 1000,
1527                        ),
1528                        'theme' => array(
1529                                'stylesheet' => $this->get_stylesheet(),
1530                                'active'     => $this->is_theme_active(),
1531                        ),
1532                        'url' => array(
1533                                'self' => $self_url,
1534                                'allowed' => array_map( 'esc_url_raw', $this->get_allowed_urls() ),
1535                                'allowedHosts' => array_unique( $allowed_hosts ),
1536                                'isCrossDomain' => $this->is_cross_domain(),
1537                        ),
1538                        'channel' => $this->messenger_channel,
1539                        'activePanels' => array(),
1540                        'activeSections' => array(),
1541                        'activeControls' => array(),
1542                        'settingValidities' => $exported_setting_validities,
1543                        'nonce' => current_user_can( 'customize' ) ? $this->get_nonces() : array(),
1544                        'l10n' => array(
1545                                'shiftClickToEdit' => __( 'Shift-click to edit this element.' ),
1546                                'linkUnpreviewable' => __( 'This link is not live-previewable.' ),
1547                                'formUnpreviewable' => __( 'This form is not live-previewable.' ),
1548                        ),
1549                        '_dirty' => array_keys( $post_values ),
1550                );
1551
1552                foreach ( $this->panels as $panel_id => $panel ) {
1553                        if ( $panel->check_capabilities() ) {
1554                                $settings['activePanels'][ $panel_id ] = $panel->active();
1555                                foreach ( $panel->sections as $section_id => $section ) {
1556                                        if ( $section->check_capabilities() ) {
1557                                                $settings['activeSections'][ $section_id ] = $section->active();
1558                                        }
1559                                }
1560                        }
1561                }
1562                foreach ( $this->sections as $id => $section ) {
1563                        if ( $section->check_capabilities() ) {
1564                                $settings['activeSections'][ $id ] = $section->active();
1565                        }
1566                }
1567                foreach ( $this->controls as $id => $control ) {
1568                        if ( $control->check_capabilities() ) {
1569                                $settings['activeControls'][ $id ] = $control->active();
1570                        }
1571                }
1572
1573                ?>
1574                <script type="text/javascript">
1575                        var _wpCustomizeSettings = <?php echo wp_json_encode( $settings ); ?>;
1576                        _wpCustomizeSettings.values = {};
1577                        (function( v ) {
1578                                <?php
1579                                /*
1580                                 * Serialize settings separately from the initial _wpCustomizeSettings
1581                                 * serialization in order to avoid a peak memory usage spike.
1582                                 * @todo We may not even need to export the values at all since the pane syncs them anyway.
1583                                 */
1584                                foreach ( $this->settings as $id => $setting ) {
1585                                        if ( $setting->check_capabilities() ) {
1586                                                printf(
1587                                                        "v[%s] = %s;\n",
1588                                                        wp_json_encode( $id ),
1589                                                        wp_json_encode( $setting->js_value() )
1590                                                );
1591                                        }
1592                                }
1593                                ?>
1594                        })( _wpCustomizeSettings.values );
1595                </script>
1596                <?php
1597        }
1598
1599        /**
1600         * Prints a signature so we can ensure the Customizer was properly executed.
1601         *
1602         * @since 3.4.0
1603         * @deprecated 4.7.0
1604         */
1605        public function customize_preview_signature() {
1606                _deprecated_function( __METHOD__, '4.7.0' );
1607        }
1608
1609        /**
1610         * Removes the signature in case we experience a case where the Customizer was not properly executed.
1611         *
1612         * @since 3.4.0
1613         * @deprecated 4.7.0
1614         *
1615         * @param mixed $return Value passed through for {@see 'wp_die_handler'} filter.
1616         * @return mixed Value passed through for {@see 'wp_die_handler'} filter.
1617         */
1618        public function remove_preview_signature( $return = null ) {
1619                _deprecated_function( __METHOD__, '4.7.0' );
1620
1621                return $return;
1622        }
1623
1624        /**
1625         * Is it a theme preview?
1626         *
1627         * @since 3.4.0
1628         *
1629         * @return bool True if it's a preview, false if not.
1630         */
1631        public function is_preview() {
1632                return (bool) $this->previewing;
1633        }
1634
1635        /**
1636         * Retrieve the template name of the previewed theme.
1637         *
1638         * @since 3.4.0
1639         *
1640         * @return string Template name.
1641         */
1642        public function get_template() {
1643                return $this->theme()->get_template();
1644        }
1645
1646        /**
1647         * Retrieve the stylesheet name of the previewed theme.
1648         *
1649         * @since 3.4.0
1650         *
1651         * @return string Stylesheet name.
1652         */
1653        public function get_stylesheet() {
1654                return $this->theme()->get_stylesheet();
1655        }
1656
1657        /**
1658         * Retrieve the template root of the previewed theme.
1659         *
1660         * @since 3.4.0
1661         *
1662         * @return string Theme root.
1663         */
1664        public function get_template_root() {
1665                return get_raw_theme_root( $this->get_template(), true );
1666        }
1667
1668        /**
1669         * Retrieve the stylesheet root of the previewed theme.
1670         *
1671         * @since 3.4.0
1672         *
1673         * @return string Theme root.
1674         */
1675        public function get_stylesheet_root() {
1676                return get_raw_theme_root( $this->get_stylesheet(), true );
1677        }
1678
1679        /**
1680         * Filters the current theme and return the name of the previewed theme.
1681         *
1682         * @since 3.4.0
1683         *
1684         * @param $current_theme {@internal Parameter is not used}
1685         * @return string Theme name.
1686         */
1687        public function current_theme( $current_theme ) {
1688                return $this->theme()->display('Name');
1689        }
1690
1691        /**
1692         * Validates setting values.
1693         *
1694         * Validation is skipped for unregistered settings or for values that are
1695         * already null since they will be skipped anyway. Sanitization is applied
1696         * to values that pass validation, and values that become null or `WP_Error`
1697         * after sanitizing are marked invalid.
1698         *
1699         * @since 4.6.0
1700         * @access public
1701         *
1702         * @see WP_REST_Request::has_valid_params()
1703         * @see WP_Customize_Setting::validate()
1704         *
1705         * @param array $setting_values Mapping of setting IDs to values to validate and sanitize.
1706         * @param array $options {
1707         *     Options.
1708         *
1709         *     @type bool $validate_existence  Whether a setting's existence will be checked.
1710         *     @type bool $validate_capability Whether the setting capability will be checked.
1711         * }
1712         * @return array Mapping of setting IDs to return value of validate method calls, either `true` or `WP_Error`.
1713         */
1714        public function validate_setting_values( $setting_values, $options = array() ) {
1715                $options = wp_parse_args( $options, array(
1716                        'validate_capability' => false,
1717                        'validate_existence' => false,
1718                ) );
1719
1720                $validities = array();
1721                foreach ( $setting_values as $setting_id => $unsanitized_value ) {
1722                        $setting = $this->get_setting( $setting_id );
1723                        if ( ! $setting ) {
1724                                if ( $options['validate_existence'] ) {
1725                                        $validities[ $setting_id ] = new WP_Error( 'unrecognized', __( 'Setting does not exist or is unrecognized.' ) );
1726                                }
1727                                continue;
1728                        }
1729                        if ( is_null( $unsanitized_value ) ) {
1730                                continue;
1731                        }
1732                        if ( $options['validate_capability'] && ! current_user_can( $setting->capability ) ) {
1733                                $validity = new WP_Error( 'unauthorized', __( 'Unauthorized to modify setting due to capability.' ) );
1734                        } else {
1735                                $validity = $setting->validate( $unsanitized_value );
1736                        }
1737                        if ( ! is_wp_error( $validity ) ) {
1738                                /** This filter is documented in wp-includes/class-wp-customize-setting.php */
1739                                $late_validity = apply_filters( "customize_validate_{$setting->id}", new WP_Error(), $unsanitized_value, $setting );
1740                                if ( ! empty( $late_validity->errors ) ) {
1741                                        $validity = $late_validity;
1742                                }
1743                        }
1744                        if ( ! is_wp_error( $validity ) ) {
1745                                $value = $setting->sanitize( $unsanitized_value );
1746                                if ( is_null( $value ) ) {
1747                                        $validity = false;
1748                                } elseif ( is_wp_error( $value ) ) {
1749                                        $validity = $value;
1750                                }
1751                        }
1752                        if ( false === $validity ) {
1753                                $validity = new WP_Error( 'invalid_value', __( 'Invalid value.' ) );
1754                        }
1755                        $validities[ $setting_id ] = $validity;
1756                }
1757                return $validities;
1758        }
1759
1760        /**
1761         * Prepares setting validity for exporting to the client (JS).
1762         *
1763         * Converts `WP_Error` instance into array suitable for passing into the
1764         * `wp.customize.Notification` JS model.
1765         *
1766         * @since 4.6.0
1767         * @access public
1768         *
1769         * @param true|WP_Error $validity Setting validity.
1770         * @return true|array If `$validity` was a WP_Error, the error codes will be array-mapped
1771         *                    to their respective `message` and `data` to pass into the
1772         *                    `wp.customize.Notification` JS model.
1773         */
1774        public function prepare_setting_validity_for_js( $validity ) {
1775                if ( is_wp_error( $validity ) ) {
1776                        $notification = array();
1777                        foreach ( $validity->errors as $error_code => $error_messages ) {
1778                                $notification[ $error_code ] = array(
1779                                        'message' => join( ' ', $error_messages ),
1780                                        'data' => $validity->get_error_data( $error_code ),
1781                                );
1782                        }
1783                        return $notification;
1784                } else {
1785                        return true;
1786                }
1787        }
1788
1789        /**
1790         * Handle customize_save WP Ajax request to save/update a changeset.
1791         *
1792         * @since 3.4.0
1793         * @since 4.7.0 The semantics of this method have changed to update a changeset, optionally to also change the status and other attributes.
1794         */
1795        public function save() {
1796                if ( ! is_user_logged_in() ) {
1797                        wp_send_json_error( 'unauthenticated' );
1798                }
1799
1800                if ( ! $this->is_preview() ) {
1801                        wp_send_json_error( 'not_preview' );
1802                }
1803
1804                $action = 'save-customize_' . $this->get_stylesheet();
1805                if ( ! check_ajax_referer( $action, 'nonce', false ) ) {
1806                        wp_send_json_error( 'invalid_nonce' );
1807                }
1808
1809                $changeset_post_id = $this->changeset_post_id();
1810                if ( $changeset_post_id && in_array( get_post_status( $changeset_post_id ), array( 'publish', 'trash' ) ) ) {
1811                        wp_send_json_error( 'changeset_already_published' );
1812                }
1813
1814                if ( empty( $changeset_post_id ) ) {
1815                        if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->create_posts ) ) {
1816                                wp_send_json_error( 'cannot_create_changeset_post' );
1817                        }
1818                } else {
1819                        if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->edit_post, $changeset_post_id ) ) {
1820                                wp_send_json_error( 'cannot_edit_changeset_post' );
1821                        }
1822                }
1823
1824                if ( ! empty( $_POST['customize_changeset_data'] ) ) {
1825                        $input_changeset_data = json_decode( wp_unslash( $_POST['customize_changeset_data'] ), true );
1826                        if ( ! is_array( $input_changeset_data ) ) {
1827                                wp_send_json_error( 'invalid_customize_changeset_data' );
1828                        }
1829                } else {
1830                        $input_changeset_data = array();
1831                }
1832
1833                // Validate title.
1834                $changeset_title = null;
1835                if ( isset( $_POST['customize_changeset_title'] ) ) {
1836                        $changeset_title = sanitize_text_field( wp_unslash( $_POST['customize_changeset_title'] ) );
1837                }
1838
1839                // Validate changeset status param.
1840                $is_publish = null;
1841                $changeset_status = null;
1842                if ( isset( $_POST['customize_changeset_status'] ) ) {
1843                        $changeset_status = wp_unslash( $_POST['customize_changeset_status'] );
1844                        if ( ! get_post_status_object( $changeset_status ) || ! in_array( $changeset_status, array( 'draft', 'pending', 'publish', 'future' ), true ) ) {
1845                                wp_send_json_error( 'bad_customize_changeset_status', 400 );
1846                        }
1847                        $is_publish = ( 'publish' === $changeset_status || 'future' === $changeset_status );
1848                        if ( $is_publish ) {
1849                                if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->publish_posts ) ) {
1850                                        wp_send_json_error( 'changeset_publish_unauthorized', 403 );
1851                                }
1852                                if ( false === has_action( 'transition_post_status', '_wp_customize_publish_changeset' ) ) {
1853                                        wp_send_json_error( 'missing_publish_callback', 500 );
1854                                }
1855                        }
1856                }
1857
1858                /*
1859                 * Validate changeset date param. Date is assumed to be in local time for
1860                 * the WP if in MySQL format (YYYY-MM-DD HH:MM:SS). Otherwise, the date
1861                 * is parsed with strtotime() so that ISO date format may be supplied
1862                 * or a string like "+10 minutes".
1863                 */
1864                $changeset_date_gmt = null;
1865                if ( isset( $_POST['customize_changeset_date'] ) ) {
1866                        $changeset_date = wp_unslash( $_POST['customize_changeset_date'] );
1867                        if ( preg_match( '/^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d$/', $changeset_date ) ) {
1868                                $mm = substr( $changeset_date, 5, 2 );
1869                                $jj = substr( $changeset_date, 8, 2 );
1870                                $aa = substr( $changeset_date, 0, 4 );
1871                                $valid_date = wp_checkdate( $mm, $jj, $aa, $changeset_date );
1872                                if ( ! $valid_date ) {
1873                                        wp_send_json_error( 'bad_customize_changeset_date', 400 );
1874                                }
1875                                $changeset_date_gmt = get_gmt_from_date( $changeset_date );
1876                        } else {
1877                                $timestamp = strtotime( $changeset_date );
1878                                if ( ! $timestamp ) {
1879                                        wp_send_json_error( 'bad_customize_changeset_date', 400 );
1880                                }
1881                                $changeset_date_gmt = gmdate( 'Y-m-d H:i:s', $timestamp );
1882                        }
1883                        $now = gmdate( 'Y-m-d H:i:59' );
1884
1885                        $is_future_dated = ( mysql2date( 'U', $changeset_date_gmt, false ) > mysql2date( 'U', $now, false ) );
1886                        if ( ! $is_future_dated ) {
1887                                wp_send_json_error( 'not_future_date', 400 ); // Only future dates are allowed.
1888                        }
1889
1890                        if ( ! $this->is_theme_active() && ( 'future' === $changeset_status || $is_future_dated ) ) {
1891                                wp_send_json_error( 'cannot_schedule_theme_switches', 400 ); // This should be allowed in the future, when theme is a regular setting.
1892                        }
1893                        $will_remain_auto_draft = ( ! $changeset_status && ( ! $changeset_post_id || 'auto-draft' === get_post_status( $changeset_post_id ) ) );
1894                        if ( $changeset_date && $will_remain_auto_draft ) {
1895                                wp_send_json_error( 'cannot_supply_date_for_auto_draft_changeset', 400 );
1896                        }
1897                }
1898
1899                $r = $this->save_changeset_post( array(
1900                        'status' => $changeset_status,
1901                        'title' => $changeset_title,
1902                        'date_gmt' => $changeset_date_gmt,
1903                        'data' => $input_changeset_data,
1904                ) );
1905                if ( is_wp_error( $r ) ) {
1906                        $response = $r->get_error_data();
1907                } else {
1908                        $response = $r;
1909
1910                        // Note that if the changeset status was publish, then it will get set to trash if revisions are not supported.
1911                        $response['changeset_status'] = get_post_status( $this->changeset_post_id() );
1912                        if ( $is_publish && 'trash' === $response['changeset_status'] ) {
1913                                $response['changeset_status'] = 'publish';
1914                        }
1915
1916                        if ( 'publish' === $response['changeset_status'] ) {
1917                                $response['next_changeset_uuid'] = wp_generate_uuid4();
1918                        }
1919                }
1920
1921                if ( isset( $response['setting_validities'] ) ) {
1922                        $response['setting_validities'] = array_map( array( $this, 'prepare_setting_validity_for_js' ), $response['setting_validities'] );
1923                }
1924
1925                /**
1926                 * Filters response data for a successful customize_save Ajax request.
1927                 *
1928                 * This filter does not apply if there was a nonce or authentication failure.
1929                 *
1930                 * @since 4.2.0
1931                 *
1932                 * @param array                $response Additional information passed back to the 'saved'
1933                 *                                       event on `wp.customize`.
1934                 * @param WP_Customize_Manager $this     WP_Customize_Manager instance.
1935                 */
1936                $response = apply_filters( 'customize_save_response', $response, $this );
1937
1938                if ( is_wp_error( $r ) ) {
1939                        wp_send_json_error( $response );
1940                } else {
1941                        wp_send_json_success( $response );
1942                }
1943        }
1944
1945        /**
1946         * Save the post for the loaded changeset.
1947         *
1948         * @since 4.7.0
1949         * @access public
1950         *
1951         * @param array $args {
1952         *     Args for changeset post.
1953         *
1954         *     @type array  $data            Optional additional changeset data. Values will be merged on top of any existing post values.
1955         *     @type string $status          Post status. Optional. If supplied, the save will be transactional and a post revision will be allowed.
1956         *     @type string $title           Post title. Optional.
1957         *     @type string $date_gmt        Date in GMT. Optional.
1958         *     @type int    $user_id         ID for user who is saving the changeset. Optional, defaults to the current user ID.
1959         *     @type bool   $starter_content Whether the data is starter content. If false (default), then $starter_content will be cleared for any $data being saved.
1960         * }
1961         *
1962         * @return array|WP_Error Returns array on success and WP_Error with array data on error.
1963         */
1964        function save_changeset_post( $args = array() ) {
1965
1966                $args = array_merge(
1967                        array(
1968                                'status' => null,
1969                                'title' => null,
1970                                'data' => array(),
1971                                'date_gmt' => null,
1972                                'user_id' => get_current_user_id(),
1973                                'starter_content' => false,
1974                        ),
1975                        $args
1976                );
1977
1978                $changeset_post_id = $this->changeset_post_id();
1979                $existing_changeset_data = array();
1980                if ( $changeset_post_id ) {
1981                        $existing_changeset_data = $this->get_changeset_post_data( $changeset_post_id );
1982                }
1983
1984                // The request was made via wp.customize.previewer.save().
1985                $update_transactionally = (bool) $args['status'];
1986                $allow_revision = (bool) $args['status'];
1987
1988                // Amend post values with any supplied data.
1989                foreach ( $args['data'] as $setting_id => $setting_params ) {
1990                        if ( array_key_exists( 'value', $setting_params ) ) {
1991                                $this->set_post_value( $setting_id, $setting_params['value'] ); // Add to post values so that they can be validated and sanitized.
1992                        }
1993                }
1994
1995                // Note that in addition to post data, this will include any stashed theme mods.
1996                $post_values = $this->unsanitized_post_values( array(
1997                        'exclude_changeset' => true,
1998                        'exclude_post_data' => false,
1999                ) );
2000                $this->add_dynamic_settings( array_keys( $post_values ) ); // Ensure settings get created even if they lack an input value.
2001
2002                /*
2003                 * Get list of IDs for settings that have values different from what is currently
2004                 * saved in the changeset. By skipping any values that are already the same, the
2005                 * subset of changed settings can be passed into validate_setting_values to prevent
2006                 * an underprivileged modifying a single setting for which they have the capability
2007                 * from being blocked from saving. This also prevents a user from touching of the
2008                 * previous saved settings and overriding the associated user_id if they made no change.
2009                 */
2010                $changed_setting_ids = array();
2011                foreach ( $post_values as $setting_id => $setting_value ) {
2012                        $setting = $this->get_setting( $setting_id );
2013
2014                        if ( $setting && 'theme_mod' === $setting->type ) {
2015                                $prefixed_setting_id = $this->get_stylesheet() . '::' . $setting->id;
2016                        } else {
2017                                $prefixed_setting_id = $setting_id;
2018                        }
2019
2020                        $is_value_changed = (
2021                                ! isset( $existing_changeset_data[ $prefixed_setting_id ] )
2022                                ||
2023                                ! array_key_exists( 'value', $existing_changeset_data[ $prefixed_setting_id ] )
2024                                ||
2025                                $existing_changeset_data[ $prefixed_setting_id ]['value'] !== $setting_value
2026                        );
2027                        if ( $is_value_changed ) {
2028                                $changed_setting_ids[] = $setting_id;
2029                        }
2030                }
2031                $post_values = wp_array_slice_assoc( $post_values, $changed_setting_ids );
2032
2033                /**
2034                 * Fires before save validation happens.
2035                 *
2036                 * Plugins can add just-in-time {@see 'customize_validate_{$this->ID}'} filters
2037                 * at this point to catch any settings registered after `customize_register`.
2038                 * The dynamic portion of the hook name, `$this->ID` refers to the setting ID.
2039                 *
2040                 * @since 4.6.0
2041                 *
2042                 * @param WP_Customize_Manager $this WP_Customize_Manager instance.
2043                 */
2044                do_action( 'customize_save_validation_before', $this );
2045
2046                // Validate settings.
2047                $setting_validities = $this->validate_setting_values( $post_values, array(
2048                        'validate_capability' => true,
2049                        'validate_existence' => true,
2050                ) );
2051                $invalid_setting_count = count( array_filter( $setting_validities, 'is_wp_error' ) );
2052
2053                /*
2054                 * Short-circuit if there are invalid settings the update is transactional.
2055                 * A changeset update is transactional when a status is supplied in the request.
2056                 */
2057                if ( $update_transactionally && $invalid_setting_count > 0 ) {
2058                        $response = array(
2059                                'setting_validities' => $setting_validities,
2060                                'message' => sprintf( _n( 'There is %s invalid setting.', 'There are %s invalid settings.', $invalid_setting_count ), number_format_i18n( $invalid_setting_count ) ),
2061                        );
2062                        return new WP_Error( 'transaction_fail', '', $response );
2063                }
2064
2065                $response = array(
2066                        'setting_validities' => $setting_validities,
2067                );
2068
2069                // Obtain/merge data for changeset.
2070                $original_changeset_data = $this->get_changeset_post_data( $changeset_post_id );
2071                $data = $original_changeset_data;
2072                if ( is_wp_error( $data ) ) {
2073                        $data = array();
2074                }
2075
2076                // Ensure that all post values are included in the changeset data.
2077                foreach ( $post_values as $setting_id => $post_value ) {
2078                        if ( ! isset( $args['data'][ $setting_id ] ) ) {
2079                                $args['data'][ $setting_id ] = array();
2080                        }
2081                        if ( ! isset( $args['data'][ $setting_id ]['value'] ) ) {
2082                                $args['data'][ $setting_id ]['value'] = $post_value;
2083                        }
2084                }
2085
2086                foreach ( $args['data'] as $setting_id => $setting_params ) {
2087                        $setting = $this->get_setting( $setting_id );
2088                        if ( ! $setting || ! $setting->check_capabilities() ) {
2089                                continue;
2090                        }
2091
2092                        // Skip updating changeset for invalid setting values.
2093                        if ( isset( $setting_validities[ $setting_id ] ) && is_wp_error( $setting_validities[ $setting_id ] ) ) {
2094                                continue;
2095                        }
2096
2097                        $changeset_setting_id = $setting_id;
2098                        if ( 'theme_mod' === $setting->type ) {
2099                                $changeset_setting_id = sprintf( '%s::%s', $this->get_stylesheet(), $setting_id );
2100                        }
2101
2102                        if ( null === $setting_params ) {
2103                                // Remove setting from changeset entirely.
2104                                unset( $data[ $changeset_setting_id ] );
2105                        } else {
2106                                // Merge any additional setting params that have been supplied with the existing params.
2107                                if ( ! isset( $data[ $changeset_setting_id ] ) ) {
2108                                        $data[ $changeset_setting_id ] = array();
2109                                }
2110
2111                                $data[ $changeset_setting_id ] = array_merge(
2112                                        $data[ $changeset_setting_id ],
2113                                        $setting_params,
2114                                        array(
2115                                                'type' => $setting->type,
2116                                                'user_id' => $args['user_id'],
2117                                        )
2118                                );
2119
2120                                // Clear starter_content flag in data if changeset is not explicitly being updated for starter content.
2121                                if ( empty( $args['starter_content'] ) ) {
2122                                        unset( $data[ $changeset_setting_id ]['starter_content'] );
2123                                }
2124                        }
2125                }
2126
2127                $filter_context = array(
2128                        'uuid' => $this->changeset_uuid(),
2129                        'title' => $args['title'],
2130                        'status' => $args['status'],
2131                        'date_gmt' => $args['date_gmt'],
2132                        'post_id' => $changeset_post_id,
2133                        'previous_data' => is_wp_error( $original_changeset_data ) ? array() : $original_changeset_data,
2134                        'manager' => $this,
2135                );
2136
2137                /**
2138                 * Filters the settings' data that will be persisted into the changeset.
2139                 *
2140                 * Plugins may amend additional data (such as additional meta for settings) into the changeset with this filter.
2141                 *
2142                 * @since 4.7.0
2143                 *
2144                 * @param array $data Updated changeset data, mapping setting IDs to arrays containing a $value item and optionally other metadata.
2145                 * @param array $context {
2146                 *     Filter context.
2147                 *
2148                 *     @type string               $uuid          Changeset UUID.
2149                 *     @type string               $title         Requested title for the changeset post.
2150                 *     @type string               $status        Requested status for the changeset post.
2151                 *     @type string               $date_gmt      Requested date for the changeset post in MySQL format and GMT timezone.
2152                 *     @type int|false            $post_id       Post ID for the changeset, or false if it doesn't exist yet.
2153                 *     @type array                $previous_data Previous data contained in the changeset.
2154                 *     @type WP_Customize_Manager $manager       Manager instance.
2155                 * }
2156                 */
2157                $data = apply_filters( 'customize_changeset_save_data', $data, $filter_context );
2158
2159                // Switch theme if publishing changes now.
2160                if ( 'publish' === $args['status'] && ! $this->is_theme_active() ) {
2161                        // Temporarily stop previewing the theme to allow switch_themes() to operate properly.
2162                        $this->stop_previewing_theme();
2163                        switch_theme( $this->get_stylesheet() );
2164                        update_option( 'theme_switched_via_customizer', true );
2165                        $this->start_previewing_theme();
2166                }
2167
2168                // Gather the data for wp_insert_post()/wp_update_post().
2169                $json_options = 0;
2170                if ( defined( 'JSON_UNESCAPED_SLASHES' ) ) {
2171                        $json_options |= JSON_UNESCAPED_SLASHES; // Introduced in PHP 5.4. This is only to improve readability as slashes needn't be escaped in storage.
2172                }
2173                $json_options |= JSON_PRETTY_PRINT; // Also introduced in PHP 5.4, but WP defines constant for back compat. See WP Trac #30139.
2174                $post_array = array(
2175                        'post_content' => wp_json_encode( $data, $json_options ),
2176                );
2177                if ( $args['title'] ) {
2178                        $post_array['post_title'] = $args['title'];
2179                }
2180                if ( $changeset_post_id ) {
2181                        $post_array['ID'] = $changeset_post_id;
2182                } else {
2183                        $post_array['post_type'] = 'customize_changeset';
2184                        $post_array['post_name'] = $this->changeset_uuid();
2185                        $post_array['post_status'] = 'auto-draft';
2186                }
2187                if ( $args['status'] ) {
2188                        $post_array['post_status'] = $args['status'];
2189                }
2190                if ( $args['date_gmt'] ) {
2191                        $post_array['post_date_gmt'] = $args['date_gmt'];
2192                        $post_array['post_date'] = get_date_from_gmt( $args['date_gmt'] );
2193                }
2194
2195                $this->store_changeset_revision = $allow_revision;
2196                add_filter( 'wp_save_post_revision_post_has_changed', array( $this, '_filter_revision_post_has_changed' ), 5, 3 );
2197
2198                // Update the changeset post. The publish_customize_changeset action will cause the settings in the changeset to be saved via WP_Customize_Setting::save().
2199                $has_kses = ( false !== has_filter( 'content_save_pre', 'wp_filter_post_kses' ) );
2200                if ( $has_kses ) {
2201                        kses_remove_filters(); // Prevent KSES from corrupting JSON in post_content.
2202                }
2203
2204                // Note that updating a post with publish status will trigger WP_Customize_Manager::publish_changeset_values().
2205                if ( $changeset_post_id ) {
2206                        $post_array['edit_date'] = true; // Prevent date clearing.
2207                        $r = wp_update_post( wp_slash( $post_array ), true );
2208                } else {
2209                        $r = wp_insert_post( wp_slash( $post_array ), true );
2210                        if ( ! is_wp_error( $r ) ) {
2211                                $this->_changeset_post_id = $r; // Update cached post ID for the loaded changeset.
2212                        }
2213                }
2214                if ( $has_kses ) {
2215                        kses_init_filters();
2216                }
2217                $this->_changeset_data = null; // Reset so WP_Customize_Manager::changeset_data() will re-populate with updated contents.
2218
2219                remove_filter( 'wp_save_post_revision_post_has_changed', array( $this, '_filter_revision_post_has_changed' ) );
2220
2221                if ( is_wp_error( $r ) ) {
2222                        $response['changeset_post_save_failure'] = $r->get_error_code();
2223                        return new WP_Error( 'changeset_post_save_failure', '', $response );
2224                }
2225
2226                return $response;
2227        }
2228
2229        /**
2230         * Whether a changeset revision should be made.
2231         *
2232         * @since 4.7.0
2233         * @access private
2234         * @var bool
2235         */
2236        protected $store_changeset_revision;
2237
2238        /**
2239         * Filters whether a changeset has changed to create a new revision.
2240         *
2241         * Note that this will not be called while a changeset post remains in auto-draft status.
2242         *
2243         * @since 4.7.0
2244         * @access private
2245         *
2246         * @param bool    $post_has_changed Whether the post has changed.
2247         * @param WP_Post $last_revision    The last revision post object.
2248         * @param WP_Post $post             The post object.
2249         *
2250         * @return bool Whether a revision should be made.
2251         */
2252        public function _filter_revision_post_has_changed( $post_has_changed, $last_revision, $post ) {
2253                unset( $last_revision );
2254                if ( 'customize_changeset' === $post->post_type ) {
2255                        $post_has_changed = $this->store_changeset_revision;
2256                }
2257                return $post_has_changed;
2258        }
2259
2260        /**
2261         * Publish changeset values.
2262         *
2263         * This will the values contained in a changeset, even changesets that do not
2264         * correspond to current manager instance. This is called by
2265         * `_wp_customize_publish_changeset()` when a customize_changeset post is
2266         * transitioned to the `publish` status. As such, this method should not be
2267         * called directly and instead `wp_publish_post()` should be used.
2268         *
2269         * Please note that if the settings in the changeset are for a non-activated
2270         * theme, the theme must first be switched to (via `switch_theme()`) before
2271         * invoking this method.
2272         *
2273         * @since 4.7.0
2274         * @access private
2275         * @see _wp_customize_publish_changeset()
2276         *
2277         * @param int $changeset_post_id ID for customize_changeset post. Defaults to the changeset for the current manager instance.
2278         * @return true|WP_Error True or error info.
2279         */
2280        public function _publish_changeset_values( $changeset_post_id ) {
2281                $publishing_changeset_data = $this->get_changeset_post_data( $changeset_post_id );
2282                if ( is_wp_error( $publishing_changeset_data ) ) {
2283                        return $publishing_changeset_data;
2284                }
2285
2286                $changeset_post = get_post( $changeset_post_id );
2287
2288                /*
2289                 * Temporarily override the changeset context so that it will be read
2290                 * in calls to unsanitized_post_values() and so that it will be available
2291                 * on the $wp_customize object passed to hooks during the save logic.
2292                 */
2293                $previous_changeset_post_id = $this->_changeset_post_id;
2294                $this->_changeset_post_id   = $changeset_post_id;
2295                $previous_changeset_uuid    = $this->_changeset_uuid;
2296                $this->_changeset_uuid      = $changeset_post->post_name;
2297                $previous_changeset_data    = $this->_changeset_data;
2298                $this->_changeset_data      = $publishing_changeset_data;
2299
2300                // Parse changeset data to identify theme mod settings and user IDs associated with settings to be saved.
2301                $setting_user_ids = array();
2302                $theme_mod_settings = array();
2303                $namespace_pattern = '/^(?P<stylesheet>.+?)::(?P<setting_id>.+)$/';
2304                $matches = array();
2305                foreach ( $this->_changeset_data as $raw_setting_id => $setting_params ) {
2306                        $actual_setting_id = null;
2307                        $is_theme_mod_setting = (
2308                                isset( $setting_params['value'] )
2309                                &&
2310                                isset( $setting_params['type'] )
2311                                &&
2312                                'theme_mod' === $setting_params['type']
2313                                &&
2314                                preg_match( $namespace_pattern, $raw_setting_id, $matches )
2315                        );
2316                        if ( $is_theme_mod_setting ) {
2317                                if ( ! isset( $theme_mod_settings[ $matches['stylesheet'] ] ) ) {
2318                                        $theme_mod_settings[ $matches['stylesheet'] ] = array();
2319                                }
2320                                $theme_mod_settings[ $matches['stylesheet'] ][ $matches['setting_id'] ] = $setting_params;
2321
2322                                if ( $this->get_stylesheet() === $matches['stylesheet'] ) {
2323                                        $actual_setting_id = $matches['setting_id'];
2324                                }
2325                        } else {
2326                                $actual_setting_id = $raw_setting_id;
2327                        }
2328
2329                        // Keep track of the user IDs for settings actually for this theme.
2330                        if ( $actual_setting_id && isset( $setting_params['user_id'] ) ) {
2331                                $setting_user_ids[ $actual_setting_id ] = $setting_params['user_id'];
2332                        }
2333                }
2334
2335                $changeset_setting_values = $this->unsanitized_post_values( array(
2336                        'exclude_post_data' => true,
2337                        'exclude_changeset' => false,
2338                ) );
2339                $changeset_setting_ids = array_keys( $changeset_setting_values );
2340                $this->add_dynamic_settings( $changeset_setting_ids );
2341
2342                /**
2343                 * Fires once the theme has switched in the Customizer, but before settings
2344                 * have been saved.
2345                 *
2346                 * @since 3.4.0
2347                 *
2348                 * @param WP_Customize_Manager $manager WP_Customize_Manager instance.
2349                 */
2350                do_action( 'customize_save', $this );
2351
2352                /*
2353                 * Ensure that all settings will allow themselves to be saved. Note that
2354                 * this is safe because the setting would have checked the capability
2355                 * when the setting value was written into the changeset. So this is why
2356                 * an additional capability check is not required here.
2357                 */
2358                $original_setting_capabilities = array();
2359                foreach ( $changeset_setting_ids as $setting_id ) {
2360                        $setting = $this->get_setting( $setting_id );
2361                        if ( $setting && ! isset( $setting_user_ids[ $setting_id ] ) ) {
2362                                $original_setting_capabilities[ $setting->id ] = $setting->capability;
2363                                $setting->capability = 'exist';
2364                        }
2365                }
2366
2367                $original_user_id = get_current_user_id();
2368                foreach ( $changeset_setting_ids as $setting_id ) {
2369                        $setting = $this->get_setting( $setting_id );
2370                        if ( $setting ) {
2371                                /*
2372                                 * Set the current user to match the user who saved the value into
2373                                 * the changeset so that any filters that apply during the save
2374                                 * process will respect the original user's capabilities. This
2375                                 * will ensure, for example, that KSES won't strip unsafe HTML
2376                                 * when a scheduled changeset publishes via WP Cron.
2377                                 */
2378                                if ( isset( $setting_user_ids[ $setting_id ] ) ) {
2379                                        wp_set_current_user( $setting_user_ids[ $setting_id ] );
2380                                } else {
2381                                        wp_set_current_user( $original_user_id );
2382                                }
2383
2384                                $setting->save();
2385                        }
2386                }
2387                wp_set_current_user( $original_user_id );
2388
2389                // Update the stashed theme mod settings, removing the active theme's stashed settings, if activated.
2390                if ( did_action( 'switch_theme' ) ) {
2391                        $other_theme_mod_settings = $theme_mod_settings;
2392                        unset( $other_theme_mod_settings[ $this->get_stylesheet() ] );
2393                        $this->update_stashed_theme_mod_settings( $other_theme_mod_settings );
2394                }
2395
2396                /**
2397                 * Fires after Customize settings have been saved.
2398                 *
2399                 * @since 3.6.0
2400                 *
2401                 * @param WP_Customize_Manager $manager WP_Customize_Manager instance.
2402                 */
2403                do_action( 'customize_save_after', $this );
2404
2405                // Restore original capabilities.
2406                foreach ( $original_setting_capabilities as $setting_id => $capability ) {
2407                        $setting = $this->get_setting( $setting_id );
2408                        if ( $setting ) {
2409                                $setting->capability = $capability;
2410                        }
2411                }
2412
2413                // Restore original changeset data.
2414                $this->_changeset_data    = $previous_changeset_data;
2415                $this->_changeset_post_id = $previous_changeset_post_id;
2416                $this->_changeset_uuid    = $previous_changeset_uuid;
2417
2418                return true;
2419        }
2420
2421        /**
2422         * Update stashed theme mod settings.
2423         *
2424         * @since 4.7.0
2425         * @access private
2426         *
2427         * @param array $inactive_theme_mod_settings Mapping of stylesheet to arrays of theme mod settings.
2428         * @return array|false Returns array of updated stashed theme mods or false if the update failed or there were no changes.
2429         */
2430        protected function update_stashed_theme_mod_settings( $inactive_theme_mod_settings ) {
2431                $stashed_theme_mod_settings = get_option( 'customize_stashed_theme_mods' );
2432                if ( empty( $stashed_theme_mod_settings ) ) {
2433                        $stashed_theme_mod_settings = array();
2434                }
2435
2436                // Delete any stashed theme mods for the active theme since since they would have been loaded and saved upon activation.
2437                unset( $stashed_theme_mod_settings[ $this->get_stylesheet() ] );
2438
2439                // Merge inactive theme mods with the stashed theme mod settings.
2440                foreach ( $inactive_theme_mod_settings as $stylesheet => $theme_mod_settings ) {
2441                        if ( ! isset( $stashed_theme_mod_settings[ $stylesheet ] ) ) {
2442                                $stashed_theme_mod_settings[ $stylesheet ] = array();
2443                        }
2444
2445                        $stashed_theme_mod_settings[ $stylesheet ] = array_merge(
2446                                $stashed_theme_mod_settings[ $stylesheet ],
2447                                $theme_mod_settings
2448                        );
2449                }
2450
2451                $autoload = false;
2452                $result = update_option( 'customize_stashed_theme_mods', $stashed_theme_mod_settings, $autoload );
2453                if ( ! $result ) {
2454                        return false;
2455                }
2456                return $stashed_theme_mod_settings;
2457        }
2458
2459        /**
2460         * Refresh nonces for the current preview.
2461         *
2462         * @since 4.2.0
2463         */
2464        public function refresh_nonces() {
2465                if ( ! $this->is_preview() ) {
2466                        wp_send_json_error( 'not_preview' );
2467                }
2468
2469                wp_send_json_success( $this->get_nonces() );
2470        }
2471
2472        /**
2473         * Add a customize setting.
2474         *
2475         * @since 3.4.0
2476         * @since 4.5.0 Return added WP_Customize_Setting instance.
2477         * @access public
2478         *
2479         * @param WP_Customize_Setting|string $id   Customize Setting object, or ID.
2480         * @param array                       $args Setting arguments; passed to WP_Customize_Setting
2481         *                                          constructor.
2482         * @return WP_Customize_Setting             The instance of the setting that was added.
2483         */
2484        public function add_setting( $id, $args = array() ) {
2485                if ( $id instanceof WP_Customize_Setting ) {
2486                        $setting = $id;
2487                } else {
2488                        $class = 'WP_Customize_Setting';
2489
2490                        /** This filter is documented in wp-includes/class-wp-customize-manager.php */
2491                        $args = apply_filters( 'customize_dynamic_setting_args', $args, $id );
2492
2493                        /** This filter is documented in wp-includes/class-wp-customize-manager.php */
2494                        $class = apply_filters( 'customize_dynamic_setting_class', $class, $id, $args );
2495
2496                        $setting = new $class( $this, $id, $args );
2497                }
2498
2499                $this->settings[ $setting->id ] = $setting;
2500                return $setting;
2501        }
2502
2503        /**
2504         * Register any dynamically-created settings, such as those from $_POST['customized']
2505         * that have no corresponding setting created.
2506         *
2507         * This is a mechanism to "wake up" settings that have been dynamically created
2508         * on the front end and have been sent to WordPress in `$_POST['customized']`. When WP
2509         * loads, the dynamically-created settings then will get created and previewed
2510         * even though they are not directly created statically with code.
2511         *
2512         * @since 4.2.0
2513         * @access public
2514         *
2515         * @param array $setting_ids The setting IDs to add.
2516         * @return array The WP_Customize_Setting objects added.
2517         */
2518        public function add_dynamic_settings( $setting_ids ) {
2519                $new_settings = array();
2520                foreach ( $setting_ids as $setting_id ) {
2521                        // Skip settings already created
2522                        if ( $this->get_setting( $setting_id ) ) {
2523                                continue;
2524                        }
2525
2526                        $setting_args = false;
2527                        $setting_class = 'WP_Customize_Setting';
2528
2529                        /**
2530                         * Filters a dynamic setting's constructor args.
2531                         *
2532                         * For a dynamic setting to be registered, this filter must be employed
2533                         * to override the default false value with an array of args to pass to
2534                         * the WP_Customize_Setting constructor.
2535                         *
2536                         * @since 4.2.0
2537                         *
2538                         * @param false|array $setting_args The arguments to the WP_Customize_Setting constructor.
2539                         * @param string      $setting_id   ID for dynamic setting, usually coming from `$_POST['customized']`.
2540                         */
2541                        $setting_args = apply_filters( 'customize_dynamic_setting_args', $setting_args, $setting_id );
2542                        if ( false === $setting_args ) {
2543                                continue;
2544                        }
2545
2546                        /**
2547                         * Allow non-statically created settings to be constructed with custom WP_Customize_Setting subclass.
2548                         *
2549                         * @since 4.2.0
2550                         *
2551                         * @param string $setting_class WP_Customize_Setting or a subclass.
2552                         * @param string $setting_id    ID for dynamic setting, usually coming from `$_POST['customized']`.
2553                         * @param array  $setting_args  WP_Customize_Setting or a subclass.
2554                         */
2555                        $setting_class = apply_filters( 'customize_dynamic_setting_class', $setting_class, $setting_id, $setting_args );
2556
2557                        $setting = new $setting_class( $this, $setting_id, $setting_args );
2558
2559                        $this->add_setting( $setting );
2560                        $new_settings[] = $setting;
2561                }
2562                return $new_settings;
2563        }
2564
2565        /**
2566         * Retrieve a customize setting.
2567         *
2568         * @since 3.4.0
2569         *
2570         * @param string $id Customize Setting ID.
2571         * @return WP_Customize_Setting|void The setting, if set.
2572         */
2573        public function get_setting( $id ) {
2574                if ( isset( $this->settings[ $id ] ) ) {
2575                        return $this->settings[ $id ];
2576                }
2577        }
2578
2579        /**
2580         * Remove a customize setting.
2581         *
2582         * @since 3.4.0
2583         *
2584         * @param string $id Customize Setting ID.
2585         */
2586        public function remove_setting( $id ) {
2587                unset( $this->settings[ $id ] );
2588        }
2589
2590        /**
2591         * Add a customize panel.
2592         *
2593         * @since 4.0.0
2594         * @since 4.5.0 Return added WP_Customize_Panel instance.
2595         * @access public
2596         *
2597         * @param WP_Customize_Panel|string $id   Customize Panel object, or Panel ID.
2598         * @param array                     $args Optional. Panel arguments. Default empty array.
2599         *
2600         * @return WP_Customize_Panel             The instance of the panel that was added.
2601         */
2602        public function add_panel( $id, $args = array() ) {
2603                if ( $id instanceof WP_Customize_Panel ) {
2604                        $panel = $id;
2605                } else {
2606                        $panel = new WP_Customize_Panel( $this, $id, $args );
2607                }
2608
2609                $this->panels[ $panel->id ] = $panel;
2610                return $panel;
2611        }
2612
2613        /**
2614         * Retrieve a customize panel.
2615         *
2616         * @since 4.0.0
2617         * @access public
2618         *
2619         * @param string $id Panel ID to get.
2620         * @return WP_Customize_Panel|void Requested panel instance, if set.
2621         */
2622        public function get_panel( $id ) {
2623                if ( isset( $this->panels[ $id ] ) ) {
2624                        return $this->panels[ $id ];
2625                }
2626        }
2627
2628        /**
2629         * Remove a customize panel.
2630         *
2631         * @since 4.0.0
2632         * @access public
2633         *
2634         * @param string $id Panel ID to remove.
2635         */
2636        public function remove_panel( $id ) {
2637                // Removing core components this way is _doing_it_wrong().
2638                if ( in_array( $id, $this->components, true ) ) {
2639                        /* translators: 1: panel id, 2: link to 'customize_loaded_components' filter reference */
2640                        $message = sprintf( __( 'Removing %1$s manually will cause PHP warnings. Use the %2$s filter instead.' ),
2641                                $id,
2642                                '<a href="' . esc_url( 'https://developer.wordpress.org/reference/hooks/customize_loaded_components/' ) . '"><code>customize_loaded_components</code></a>'
2643                        );
2644
2645                        _doing_it_wrong( __METHOD__, $message, '4.5.0' );
2646                }
2647                unset( $this->panels[ $id ] );
2648        }
2649
2650        /**
2651         * Register a customize panel type.
2652         *
2653         * Registered types are eligible to be rendered via JS and created dynamically.
2654         *
2655         * @since 4.3.0
2656         * @access public
2657         *
2658         * @see WP_Customize_Panel
2659         *
2660         * @param string $panel Name of a custom panel which is a subclass of WP_Customize_Panel.
2661         */
2662        public function register_panel_type( $panel ) {
2663                $this->registered_panel_types[] = $panel;
2664        }
2665
2666        /**
2667         * Render JS templates for all registered panel types.
2668         *
2669         * @since 4.3.0
2670         * @access public
2671         */
2672        public function render_panel_templates() {
2673                foreach ( $this->registered_panel_types as $panel_type ) {
2674                        $panel = new $panel_type( $this, 'temp', array() );
2675                        $panel->print_template();
2676                }
2677        }
2678
2679        /**
2680         * Add a customize section.
2681         *
2682         * @since 3.4.0
2683         * @since 4.5.0 Return added WP_Customize_Section instance.
2684         * @access public
2685         *
2686         * @param WP_Customize_Section|string $id   Customize Section object, or Section ID.
2687         * @param array                       $args Section arguments.
2688         *
2689         * @return WP_Customize_Section             The instance of the section that was added.
2690         */
2691        public function add_section( $id, $args = array() ) {
2692                if ( $id instanceof WP_Customize_Section ) {
2693                        $section = $id;
2694                } else {
2695                        $section = new WP_Customize_Section( $this, $id, $args );
2696                }
2697
2698                $this->sections[ $section->id ] = $section;
2699                return $section;
2700        }
2701
2702        /**
2703         * Retrieve a customize section.
2704         *
2705         * @since 3.4.0
2706         *
2707         * @param string $id Section ID.
2708         * @return WP_Customize_Section|void The section, if set.
2709         */
2710        public function get_section( $id ) {
2711                if ( isset( $this->sections[ $id ] ) )
2712                        return $this->sections[ $id ];
2713        }
2714
2715        /**
2716         * Remove a customize section.
2717         *
2718         * @since 3.4.0
2719         *
2720         * @param string $id Section ID.
2721         */
2722        public function remove_section( $id ) {
2723                unset( $this->sections[ $id ] );
2724        }
2725
2726        /**
2727         * Register a customize section type.
2728         *
2729         * Registered types are eligible to be rendered via JS and created dynamically.
2730         *
2731         * @since 4.3.0
2732         * @access public
2733         *
2734         * @see WP_Customize_Section
2735         *
2736         * @param string $section Name of a custom section which is a subclass of WP_Customize_Section.
2737         */
2738        public function register_section_type( $section ) {
2739                $this->registered_section_types[] = $section;
2740        }
2741
2742        /**
2743         * Render JS templates for all registered section types.
2744         *
2745         * @since 4.3.0
2746         * @access public
2747         */
2748        public function render_section_templates() {
2749                foreach ( $this->registered_section_types as $section_type ) {
2750                        $section = new $section_type( $this, 'temp', array() );
2751                        $section->print_template();
2752                }
2753        }
2754
2755        /**
2756         * Add a customize control.
2757         *
2758         * @since 3.4.0
2759         * @since 4.5.0 Return added WP_Customize_Control instance.
2760         * @access public
2761         *
2762         * @param WP_Customize_Control|string $id   Customize Control object, or ID.
2763         * @param array                       $args Control arguments; passed to WP_Customize_Control
2764         *                                          constructor.
2765         * @return WP_Customize_Control             The instance of the control that was added.
2766         */
2767        public function add_control( $id, $args = array() ) {
2768                if ( $id instanceof WP_Customize_Control ) {
2769                        $control = $id;
2770                } else {
2771                        $control = new WP_Customize_Control( $this, $id, $args );
2772                }
2773
2774                $this->controls[ $control->id ] = $control;
2775                return $control;
2776        }
2777
2778        /**
2779         * Retrieve a customize control.
2780         *
2781         * @since 3.4.0
2782         *
2783         * @param string $id ID of the control.
2784         * @return WP_Customize_Control|void The control object, if set.
2785         */
2786        public function get_control( $id ) {
2787                if ( isset( $this->controls[ $id ] ) )
2788                        return $this->controls[ $id ];
2789        }
2790
2791        /**
2792         * Remove a customize control.
2793         *
2794         * @since 3.4.0
2795         *
2796         * @param string $id ID of the control.
2797         */
2798        public function remove_control( $id ) {
2799                unset( $this->controls[ $id ] );
2800        }
2801
2802        /**
2803         * Register a customize control type.
2804         *
2805         * Registered types are eligible to be rendered via JS and created dynamically.
2806         *
2807         * @since 4.1.0
2808         * @access public
2809         *
2810         * @param string $control Name of a custom control which is a subclass of
2811         *                        WP_Customize_Control.
2812         */
2813        public function register_control_type( $control ) {
2814                $this->registered_control_types[] = $control;
2815        }
2816
2817        /**
2818         * Render JS templates for all registered control types.
2819         *
2820         * @since 4.1.0
2821         * @access public
2822         */
2823        public function render_control_templates() {
2824                foreach ( $this->registered_control_types as $control_type ) {
2825                        $control = new $control_type( $this, 'temp', array(
2826                                'settings' => array(),
2827                        ) );
2828                        $control->print_template();
2829                }
2830                ?>
2831                <script type="text/html" id="tmpl-customize-control-notifications">
2832                        <ul>
2833                                <# _.each( data.notifications, function( notification ) { #>
2834                                        <li class="notice notice-{{ notification.type || 'info' }} {{ data.altNotice ? 'notice-alt' : '' }}" data-code="{{ notification.code }}" data-type="{{ notification.type }}">{{{ notification.message || notification.code }}}</li>
2835                                <# } ); #>
2836                        </ul>
2837                </script>
2838                <?php
2839        }
2840
2841        /**
2842         * Helper function to compare two objects by priority, ensuring sort stability via instance_number.
2843         *
2844         * @since 3.4.0
2845         * @deprecated 4.7.0 Use wp_list_sort()
2846         *
2847         * @param WP_Customize_Panel|WP_Customize_Section|WP_Customize_Control $a Object A.
2848         * @param WP_Customize_Panel|WP_Customize_Section|WP_Customize_Control $b Object B.
2849         * @return int
2850         */
2851        protected function _cmp_priority( $a, $b ) {
2852                _deprecated_function( __METHOD__, '4.7.0', 'wp_list_sort' );
2853
2854                if ( $a->priority === $b->priority ) {
2855                        return $a->instance_number - $b->instance_number;
2856                } else {
2857                        return $a->priority - $b->priority;
2858                }
2859        }
2860
2861        /**
2862         * Prepare panels, sections, and controls.
2863         *
2864         * For each, check if required related components exist,
2865         * whether the user has the necessary capabilities,
2866         * and sort by priority.
2867         *
2868         * @since 3.4.0
2869         */
2870        public function prepare_controls() {
2871
2872                $controls = array();
2873                $this->controls = wp_list_sort( $this->controls, array(
2874                        'priority'        => 'ASC',
2875                        'instance_number' => 'ASC',
2876                ), 'ASC', true );
2877
2878                foreach ( $this->controls as $id => $control ) {
2879                        if ( ! isset( $this->sections[ $control->section ] ) || ! $control->check_capabilities() ) {
2880                                continue;
2881                        }
2882
2883                        $this->sections[ $control->section ]->controls[] = $control;
2884                        $controls[ $id ] = $control;
2885                }
2886                $this->controls = $controls;
2887
2888                // Prepare sections.
2889                $this->sections = wp_list_sort( $this->sections, array(
2890                        'priority'        => 'ASC',
2891                        'instance_number' => 'ASC',
2892                ), 'ASC', true );
2893                $sections = array();
2894
2895                foreach ( $this->sections as $section ) {
2896                        if ( ! $section->check_capabilities() ) {
2897                                continue;
2898                        }
2899
2900
2901                        $section->controls = wp_list_sort( $section->controls, array(
2902                                'priority'        => 'ASC',
2903                                'instance_number' => 'ASC',
2904                        ) );
2905
2906                        if ( ! $section->panel ) {
2907                                // Top-level section.
2908                                $sections[ $section->id ] = $section;
2909                        } else {
2910                                // This section belongs to a panel.
2911                                if ( isset( $this->panels [ $section->panel ] ) ) {
2912                                        $this->panels[ $section->panel ]->sections[ $section->id ] = $section;
2913                                }
2914                        }
2915                }
2916                $this->sections = $sections;
2917
2918                // Prepare panels.
2919                $this->panels = wp_list_sort( $this->panels, array(
2920                        'priority'        => 'ASC',
2921                        'instance_number' => 'ASC',
2922                ), 'ASC', true );
2923                $panels = array();
2924
2925                foreach ( $this->panels as $panel ) {
2926                        if ( ! $panel->check_capabilities() ) {
2927                                continue;
2928                        }
2929
2930                        $panel->sections = wp_list_sort( $panel->sections, array(
2931                                'priority'        => 'ASC',
2932                                'instance_number' => 'ASC',
2933                        ), 'ASC', true );
2934                        $panels[ $panel->id ] = $panel;
2935                }
2936                $this->panels = $panels;
2937
2938                // Sort panels and top-level sections together.
2939                $this->containers = array_merge( $this->panels, $this->sections );
2940                $this->containers = wp_list_sort( $this->containers, array(
2941                        'priority'        => 'ASC',
2942                        'instance_number' => 'ASC',
2943                ), 'ASC', true );
2944        }
2945
2946        /**
2947         * Enqueue scripts for customize controls.
2948         *
2949         * @since 3.4.0
2950         */
2951        public function enqueue_control_scripts() {
2952                foreach ( $this->controls as $control ) {
2953                        $control->enqueue();
2954                }
2955        }
2956
2957        /**
2958         * Determine whether the user agent is iOS.
2959         *
2960         * @since 4.4.0
2961         * @access public
2962         *
2963         * @return bool Whether the user agent is iOS.
2964         */
2965        public function is_ios() {
2966                return wp_is_mobile() && preg_match( '/iPad|iPod|iPhone/', $_SERVER['HTTP_USER_AGENT'] );
2967        }
2968
2969        /**
2970         * Get the template string for the Customizer pane document title.
2971         *
2972         * @since 4.4.0
2973         * @access public
2974         *
2975         * @return string The template string for the document title.
2976         */
2977        public function get_document_title_template() {
2978                if ( $this->is_theme_active() ) {
2979                        /* translators: %s: document title from the preview */
2980                        $document_title_tmpl = __( 'Customize: %s' );
2981                } else {
2982                        /* translators: %s: document title from the preview */
2983                        $document_title_tmpl = __( 'Live Preview: %s' );
2984                }
2985                $document_title_tmpl = html_entity_decode( $document_title_tmpl, ENT_QUOTES, 'UTF-8' ); // Because exported to JS and assigned to document.title.
2986                return $document_title_tmpl;
2987        }
2988
2989        /**
2990         * Set the initial URL to be previewed.
2991         *
2992         * URL is validated.
2993         *
2994         * @since 4.4.0
2995         * @access public
2996         *
2997         * @param string $preview_url URL to be previewed.
2998         */
2999        public function set_preview_url( $preview_url ) {
3000                $preview_url = esc_url_raw( $preview_url );
3001                $this->preview_url = wp_validate_redirect( $preview_url, home_url( '/' ) );
3002        }
3003
3004        /**
3005         * Get the initial URL to be previewed.
3006         *
3007         * @since 4.4.0
3008         * @access public
3009         *
3010         * @return string URL being previewed.
3011         */
3012        public function get_preview_url() {
3013                if ( empty( $this->preview_url ) ) {
3014                        $preview_url = home_url( '/' );
3015                } else {
3016                        $preview_url = $this->preview_url;
3017                }
3018                return $preview_url;
3019        }
3020
3021        /**
3022         * Determines whether the admin and the frontend are on different domains.
3023         *
3024         * @since 4.7.0
3025         * @access public
3026         *
3027         * @return bool Whether cross-domain.
3028         */
3029        public function is_cross_domain() {
3030                $admin_origin = wp_parse_url( admin_url() );
3031                $home_origin = wp_parse_url( home_url() );
3032                $cross_domain = ( strtolower( $admin_origin['host'] ) !== strtolower( $home_origin['host'] ) );
3033                return $cross_domain;
3034        }
3035
3036        /**
3037         * Get URLs allowed to be previewed.
3038         *
3039         * If the front end and the admin are served from the same domain, load the
3040         * preview over ssl if the Customizer is being loaded over ssl. This avoids
3041         * insecure content warnings. This is not attempted if the admin and front end
3042         * are on different domains to avoid the case where the front end doesn't have
3043         * ssl certs. Domain mapping plugins can allow other urls in these conditions
3044         * using the customize_allowed_urls filter.
3045         *
3046         * @since 4.7.0
3047         * @access public
3048         *
3049         * @returns array Allowed URLs.
3050         */
3051        public function get_allowed_urls() {
3052                $allowed_urls = array( home_url( '/' ) );
3053
3054                if ( is_ssl() && ! $this->is_cross_domain() ) {
3055                        $allowed_urls[] = home_url( '/', 'https' );
3056                }
3057
3058                /**
3059                 * Filters the list of URLs allowed to be clicked and followed in the Customizer preview.
3060                 *
3061                 * @since 3.4.0
3062                 *
3063                 * @param array $allowed_urls An array of allowed URLs.
3064                 */
3065                $allowed_urls = array_unique( apply_filters( 'customize_allowed_urls', $allowed_urls ) );
3066
3067                return $allowed_urls;
3068        }
3069
3070        /**
3071         * Get messenger channel.
3072         *
3073         * @since 4.7.0
3074         * @access public
3075         *
3076         * @return string Messenger channel.
3077         */
3078        public function get_messenger_channel() {
3079                return $this->messenger_channel;
3080        }
3081
3082        /**
3083         * Set URL to link the user to when closing the Customizer.
3084         *
3085         * URL is validated.
3086         *
3087         * @since 4.4.0
3088         * @access public
3089         *
3090         * @param string $return_url URL for return link.
3091         */
3092        public function set_return_url( $return_url ) {
3093                $return_url = esc_url_raw( $return_url );
3094                $return_url = remove_query_arg( wp_removable_query_args(), $return_url );
3095                $return_url = wp_validate_redirect( $return_url );
3096                $this->return_url = $return_url;
3097        }
3098
3099        /**
3100         * Get URL to link the user to when closing the Customizer.
3101         *
3102         * @since 4.4.0
3103         * @access public
3104         *
3105         * @return string URL for link to close Customizer.
3106         */
3107        public function get_return_url() {
3108                $referer = wp_get_referer();
3109                $excluded_referer_basenames = array( 'customize.php', 'wp-login.php' );
3110
3111                if ( $this->return_url ) {
3112                        $return_url = $this->return_url;
3113                } else if ( $referer && ! in_array( basename( parse_url( $referer, PHP_URL_PATH ) ), $excluded_referer_basenames, true ) ) {
3114                        $return_url = $referer;
3115                } else if ( $this->preview_url ) {
3116                        $return_url = $this->preview_url;
3117                } else {
3118                        $return_url = home_url( '/' );
3119                }
3120                return $return_url;
3121        }
3122
3123        /**
3124         * Set the autofocused constructs.
3125         *
3126         * @since 4.4.0
3127         * @access public
3128         *
3129         * @param array $autofocus {
3130         *     Mapping of 'panel', 'section', 'control' to the ID which should be autofocused.
3131         *
3132         *     @type string [$control]  ID for control to be autofocused.
3133         *     @type string [$section]  ID for section to be autofocused.
3134         *     @type string [$panel]    ID for panel to be autofocused.
3135         * }
3136         */
3137        public function set_autofocus( $autofocus ) {
3138                $this->autofocus = array_filter( wp_array_slice_assoc( $autofocus, array( 'panel', 'section', 'control' ) ), 'is_string' );
3139        }
3140
3141        /**
3142         * Get the autofocused constructs.
3143         *
3144         * @since 4.4.0
3145         * @access public
3146         *
3147         * @return array {
3148         *     Mapping of 'panel', 'section', 'control' to the ID which should be autofocused.
3149         *
3150         *     @type string [$control]  ID for control to be autofocused.
3151         *     @type string [$section]  ID for section to be autofocused.
3152         *     @type string [$panel]    ID for panel to be autofocused.
3153         * }
3154         */
3155        public function get_autofocus() {
3156                return $this->autofocus;
3157        }
3158
3159        /**
3160         * Get nonces for the Customizer.
3161         *
3162         * @since 4.5.0
3163         * @return array Nonces.
3164         */
3165        public function get_nonces() {
3166                $nonces = array(
3167                        'save' => wp_create_nonce( 'save-customize_' . $this->get_stylesheet() ),
3168                        'preview' => wp_create_nonce( 'preview-customize_' . $this->get_stylesheet() ),
3169                );
3170
3171                /**
3172                 * Filters nonces for Customizer.
3173                 *
3174                 * @since 4.2.0
3175                 *
3176                 * @param array                $nonces Array of refreshed nonces for save and
3177                 *                                     preview actions.
3178                 * @param WP_Customize_Manager $this   WP_Customize_Manager instance.
3179                 */
3180                $nonces = apply_filters( 'customize_refresh_nonces', $nonces, $this );
3181
3182                return $nonces;
3183        }
3184
3185        /**
3186         * Print JavaScript settings for parent window.
3187         *
3188         * @since 4.4.0
3189         */
3190        public function customize_pane_settings() {
3191
3192                $login_url = add_query_arg( array(
3193                        'interim-login' => 1,
3194                        'customize-login' => 1,
3195                ), wp_login_url() );
3196
3197                // Ensure dirty flags are set for modified settings.
3198                foreach ( array_keys( $this->unsanitized_post_values() ) as $setting_id ) {
3199                        $setting = $this->get_setting( $setting_id );
3200                        if ( $setting ) {
3201                                $setting->dirty = true;
3202                        }
3203                }
3204
3205                // Prepare Customizer settings to pass to JavaScript.
3206                $settings = array(
3207                        'changeset' => array(
3208                                'uuid' => $this->changeset_uuid(),
3209                                'status' => $this->changeset_post_id() ? get_post_status( $this->changeset_post_id() ) : '',
3210                        ),
3211                        'timeouts' => array(
3212                                'windowRefresh' => 250,
3213                                'changesetAutoSave' => AUTOSAVE_INTERVAL * 1000,
3214                                'keepAliveCheck' => 2500,
3215                                'reflowPaneContents' => 100,
3216                                'previewFrameSensitivity' => 2000,
3217                        ),
3218                        'theme'    => array(
3219                                'stylesheet' => $this->get_stylesheet(),
3220                                'active'     => $this->is_theme_active(),
3221                        ),
3222                        'url'      => array(
3223                                'preview'       => esc_url_raw( $this->get_preview_url() ),
3224                                'parent'        => esc_url_raw( admin_url() ),
3225                                'activated'     => esc_url_raw( home_url( '/' ) ),
3226                                'ajax'          => esc_url_raw( admin_url( 'admin-ajax.php', 'relative' ) ),
3227                                'allowed'       => array_map( 'esc_url_raw', $this->get_allowed_urls() ),
3228                                'isCrossDomain' => $this->is_cross_domain(),
3229                                'home'          => esc_url_raw( home_url( '/' ) ),
3230                                'login'         => esc_url_raw( $login_url ),
3231                        ),
3232                        'browser'  => array(
3233                                'mobile' => wp_is_mobile(),
3234                                'ios'    => $this->is_ios(),
3235                        ),
3236                        'panels'   => array(),
3237                        'sections' => array(),
3238                        'nonce'    => $this->get_nonces(),
3239                        'autofocus' => $this->get_autofocus(),
3240                        'documentTitleTmpl' => $this->get_document_title_template(),
3241                        'previewableDevices' => $this->get_previewable_devices(),
3242                );
3243
3244                // Prepare Customize Section objects to pass to JavaScript.
3245                foreach ( $this->sections() as $id => $section ) {
3246                        if ( $section->check_capabilities() ) {
3247                                $settings['sections'][ $id ] = $section->json();
3248                        }
3249                }
3250
3251                // Prepare Customize Panel objects to pass to JavaScript.
3252                foreach ( $this->panels() as $panel_id => $panel ) {
3253                        if ( $panel->check_capabilities() ) {
3254                                $settings['panels'][ $panel_id ] = $panel->json();
3255                                foreach ( $panel->sections as $section_id => $section ) {
3256                                        if ( $section->check_capabilities() ) {
3257                                                $settings['sections'][ $section_id ] = $section->json();
3258                                        }
3259                                }
3260                        }
3261                }
3262
3263                ?>
3264                <script type="text/javascript">
3265                        var _wpCustomizeSettings = <?php echo wp_json_encode( $settings ); ?>;
3266                        _wpCustomizeSettings.controls = {};
3267                        _wpCustomizeSettings.settings = {};
3268                        <?php
3269
3270                        // Serialize settings one by one to improve memory usage.
3271                        echo "(function ( s ){\n";
3272                        foreach ( $this->settings() as $setting ) {
3273                                if ( $setting->check_capabilities() ) {
3274                                        printf(
3275                                                "s[%s] = %s;\n",
3276                                                wp_json_encode( $setting->id ),
3277                                                wp_json_encode( $setting->json() )
3278                                        );
3279                                }
3280                        }
3281                        echo "})( _wpCustomizeSettings.settings );\n";
3282
3283                        // Serialize controls one by one to improve memory usage.
3284                        echo "(function ( c ){\n";
3285                        foreach ( $this->controls() as $control ) {
3286                                if ( $control->check_capabilities() ) {
3287                                        printf(
3288                                                "c[%s] = %s;\n",
3289                                                wp_json_encode( $control->id ),
3290                                                wp_json_encode( $control->json() )
3291                                        );
3292                                }
3293                        }
3294                        echo "})( _wpCustomizeSettings.controls );\n";
3295                ?>
3296                </script>
3297                <?php
3298        }
3299
3300        /**
3301         * Returns a list of devices to allow previewing.
3302         *
3303         * @access public
3304         * @since 4.5.0
3305         *
3306         * @return array List of devices with labels and default setting.
3307         */
3308        public function get_previewable_devices() {
3309                $devices = array(
3310                        'desktop' => array(
3311                                'label' => __( 'Enter desktop preview mode' ),
3312                                'default' => true,
3313                        ),
3314                        'tablet' => array(
3315                                'label' => __( 'Enter tablet preview mode' ),
3316                        ),
3317                        'mobile' => array(
3318                                'label' => __( 'Enter mobile preview mode' ),
3319                        ),
3320                );
3321
3322                /**
3323                 * Filters the available devices to allow previewing in the Customizer.
3324                 *
3325                 * @since 4.5.0
3326                 *
3327                 * @see WP_Customize_Manager::get_previewable_devices()
3328                 *
3329                 * @param array $devices List of devices with labels and default setting.
3330                 */
3331                $devices = apply_filters( 'customize_previewable_devices', $devices );
3332
3333                return $devices;
3334        }
3335
3336        /**
3337         * Register some default controls.
3338         *
3339         * @since 3.4.0
3340         */
3341        public function register_controls() {
3342
3343                /* Panel, Section, and Control Types */
3344                $this->register_panel_type( 'WP_Customize_Panel' );
3345                $this->register_section_type( 'WP_Customize_Section' );
3346                $this->register_section_type( 'WP_Customize_Sidebar_Section' );
3347                $this->register_control_type( 'WP_Customize_Color_Control' );
3348                $this->register_control_type( 'WP_Customize_Media_Control' );
3349                $this->register_control_type( 'WP_Customize_Upload_Control' );
3350                $this->register_control_type( 'WP_Customize_Image_Control' );
3351                $this->register_control_type( 'WP_Customize_Background_Image_Control' );
3352                $this->register_control_type( 'WP_Customize_Background_Position_Control' );
3353                $this->register_control_type( 'WP_Customize_Cropped_Image_Control' );
3354                $this->register_control_type( 'WP_Customize_Site_Icon_Control' );
3355                $this->register_control_type( 'WP_Customize_Theme_Control' );
3356
3357                /* Themes */
3358
3359                $this->add_section( new WP_Customize_Themes_Section( $this, 'themes', array(
3360                        'title'      => $this->theme()->display( 'Name' ),
3361                        'capability' => 'switch_themes',
3362                        'priority'   => 0,
3363                ) ) );
3364
3365                // Themes Setting (unused - the theme is considerably more fundamental to the Customizer experience).
3366                $this->add_setting( new WP_Customize_Filter_Setting( $this, 'active_theme', array(
3367                        'capability' => 'switch_themes',
3368                ) ) );
3369
3370                require_once( ABSPATH . 'wp-admin/includes/theme.php' );
3371
3372                // Theme Controls.
3373
3374                // Add a control for the active/original theme.
3375                if ( ! $this->is_theme_active() ) {
3376                        $themes = wp_prepare_themes_for_js( array( wp_get_theme( $this->original_stylesheet ) ) );
3377                        $active_theme = current( $themes );
3378                        $active_theme['isActiveTheme'] = true;
3379                        $this->add_control( new WP_Customize_Theme_Control( $this, $active_theme['id'], array(
3380                                'theme'    => $active_theme,
3381                                'section'  => 'themes',
3382                                'settings' => 'active_theme',
3383                        ) ) );
3384                }
3385
3386                $themes = wp_prepare_themes_for_js();
3387                foreach ( $themes as $theme ) {
3388                        if ( $theme['active'] || $theme['id'] === $this->original_stylesheet ) {
3389                                continue;
3390                        }
3391
3392                        $theme_id = 'theme_' . $theme['id'];
3393                        $theme['isActiveTheme'] = false;
3394                        $this->add_control( new WP_Customize_Theme_Control( $this, $theme_id, array(
3395                                'theme'    => $theme,
3396                                'section'  => 'themes',
3397                                'settings' => 'active_theme',
3398                        ) ) );
3399                }
3400
3401                /* Site Identity */
3402
3403                $this->add_section( 'title_tagline', array(
3404                        'title'    => __( 'Site Identity' ),
3405                        'priority' => 20,
3406                ) );
3407
3408                $this->add_setting( 'blogname', array(
3409                        'default'    => get_option( 'blogname' ),
3410                        'type'       => 'option',
3411                        'capability' => 'manage_options',
3412                ) );
3413
3414                $this->add_control( 'blogname', array(
3415                        'label'      => __( 'Site Title' ),
3416                        'section'    => 'title_tagline',
3417                ) );
3418
3419                $this->add_setting( 'blogdescription', array(
3420                        'default'    => get_option( 'blogdescription' ),
3421                        'type'       => 'option',
3422                        'capability' => 'manage_options',
3423                ) );
3424
3425                $this->add_control( 'blogdescription', array(
3426                        'label'      => __( 'Tagline' ),
3427                        'section'    => 'title_tagline',
3428                ) );
3429
3430                // Add a setting to hide header text if the theme doesn't support custom headers.
3431                if ( ! current_theme_supports( 'custom-header', 'header-text' ) ) {
3432                        $this->add_setting( 'header_text', array(
3433                                'theme_supports'    => array( 'custom-logo', 'header-text' ),
3434                                'default'           => 1,
3435                                'sanitize_callback' => 'absint',
3436                        ) );
3437
3438                        $this->add_control( 'header_text', array(
3439                                'label'    => __( 'Display Site Title and Tagline' ),
3440                                'section'  => 'title_tagline',
3441                                'settings' => 'header_text',
3442                                'type'     => 'checkbox',
3443                        ) );
3444                }
3445
3446                $this->add_setting( 'site_icon', array(
3447                        'type'       => 'option',
3448                        'capability' => 'manage_options',
3449                        'transport'  => 'postMessage', // Previewed with JS in the Customizer controls window.
3450                ) );
3451
3452                $this->add_control( new WP_Customize_Site_Icon_Control( $this, 'site_icon', array(
3453                        'label'       => __( 'Site Icon' ),
3454                        'description' => sprintf(
3455                                /* translators: %s: site icon size in pixels */
3456                                __( 'The Site Icon is used as a browser and app icon for your site. Icons must be square, and at least %s pixels wide and tall.' ),
3457                                '<strong>512</strong>'
3458                        ),
3459                        'section'     => 'title_tagline',
3460                        'priority'    => 60,
3461                        'height'      => 512,
3462                        'width'       => 512,
3463                ) ) );
3464
3465                $this->add_setting( 'custom_logo', array(
3466                        'theme_supports' => array( 'custom-logo' ),
3467                        'transport'      => 'postMessage',
3468                ) );
3469
3470                $custom_logo_args = get_theme_support( 'custom-logo' );
3471                $this->add_control( new WP_Customize_Cropped_Image_Control( $this, 'custom_logo', array(
3472                        'label'         => __( 'Logo' ),
3473                        'section'       => 'title_tagline',
3474                        'priority'      => 8,
3475                        'height'        => $custom_logo_args[0]['height'],
3476                        'width'         => $custom_logo_args[0]['width'],
3477                        'flex_height'   => $custom_logo_args[0]['flex-height'],
3478                        'flex_width'    => $custom_logo_args[0]['flex-width'],
3479                        'button_labels' => array(
3480                                'select'       => __( 'Select logo' ),
3481                                'change'       => __( 'Change logo' ),
3482                                'remove'       => __( 'Remove' ),
3483                                'default'      => __( 'Default' ),
3484                                'placeholder'  => __( 'No logo selected' ),
3485                                'frame_title'  => __( 'Select logo' ),
3486                                'frame_button' => __( 'Choose logo' ),
3487                        ),
3488                ) ) );
3489
3490                $this->selective_refresh->add_partial( 'custom_logo', array(
3491                        'settings'            => array( 'custom_logo' ),
3492                        'selector'            => '.custom-logo-link',
3493                        'render_callback'     => array( $this, '_render_custom_logo_partial' ),
3494                        'container_inclusive' => true,
3495                ) );
3496
3497                /* Colors */
3498
3499                $this->add_section( 'colors', array(
3500                        'title'          => __( 'Colors' ),
3501                        'priority'       => 40,
3502                ) );
3503
3504                $this->add_setting( 'header_textcolor', array(
3505                        'theme_supports' => array( 'custom-header', 'header-text' ),
3506                        'default'        => get_theme_support( 'custom-header', 'default-text-color' ),
3507
3508                        'sanitize_callback'    => array( $this, '_sanitize_header_textcolor' ),
3509                        'sanitize_js_callback' => 'maybe_hash_hex_color',
3510                ) );
3511
3512                // Input type: checkbox
3513                // With custom value
3514                $this->add_control( 'display_header_text', array(
3515                        'settings' => 'header_textcolor',
3516                        'label'    => __( 'Display Site Title and Tagline' ),
3517                        'section'  => 'title_tagline',
3518                        'type'     => 'checkbox',
3519                        'priority' => 40,
3520                ) );
3521
3522                $this->add_control( new WP_Customize_Color_Control( $this, 'header_textcolor', array(
3523                        'label'   => __( 'Header Text Color' ),
3524                        'section' => 'colors',
3525                ) ) );
3526
3527                // Input type: Color
3528                // With sanitize_callback
3529                $this->add_setting( 'background_color', array(
3530                        'default'        => get_theme_support( 'custom-background', 'default-color' ),
3531                        'theme_supports' => 'custom-background',
3532
3533                        'sanitize_callback'    => 'sanitize_hex_color_no_hash',
3534                        'sanitize_js_callback' => 'maybe_hash_hex_color',
3535                ) );
3536
3537                $this->add_control( new WP_Customize_Color_Control( $this, 'background_color', array(
3538                        'label'   => __( 'Background Color' ),
3539                        'section' => 'colors',
3540                ) ) );
3541
3542                /* Custom Header */
3543
3544                if ( current_theme_supports( 'custom-header', 'video' ) ) {
3545                        $title = __( 'Header Media' );
3546                        $description = '<p>' . __( 'If you add a video, the image will be used as a fallback while the video loads.' ) . '</p>';
3547
3548                        // @todo Customizer sections should support having notifications just like controls do. See <https://core.trac.wordpress.org/ticket/38794>.
3549                        $description .= '<div class="customize-control-notifications-container header-video-not-currently-previewable" style="display: none"><ul>';
3550                        $description .= '<li class="notice notice-info">' . __( 'This theme doesn\'t support video headers on this page. Navigate to the front page or another page that supports video headers.' ) . '</li>';
3551                        $description .= '</ul></div>';
3552                        $width = absint( get_theme_support( 'custom-header', 'width' ) );
3553                        $height = absint( get_theme_support( 'custom-header', 'height' ) );
3554                        if ( $width && $height ) {
3555                                $control_description = sprintf(
3556                                        /* translators: 1: .mp4, 2: header size in pixels */
3557                                        __( 'Upload your video in %1$s format and minimize its file size for best results. Your theme recommends dimensions of %2$s pixels.' ),
3558                                        '<code>.mp4</code>',
3559                                        sprintf( '<strong>%s &times; %s</strong>', $width, $height )
3560                                );
3561                        } elseif ( $width ) {
3562                                $control_description = sprintf(
3563                                        /* translators: 1: .mp4, 2: header width in pixels */
3564                                        __( 'Upload your video in %1$s format and minimize its file size for best results. Your theme recommends a width of %2$s pixels.' ),
3565                                        '<code>.mp4</code>',
3566                                        sprintf( '<strong>%s</strong>', $width )
3567                                );
3568                        } else {
3569                                $control_description = sprintf(
3570                                        /* translators: 1: .mp4, 2: header height in pixels */
3571                                        __( 'Upload your video in %1$s format and minimize its file size for best results. Your theme recommends a height of %2$s pixels.' ),
3572                                        '<code>.mp4</code>',
3573                                        sprintf( '<strong>%s</strong>', $height )
3574                                );
3575                        }
3576                } else {
3577                        $title = __( 'Header Image' );
3578                        $description = '';
3579                        $control_description = '';
3580                }
3581
3582                $this->add_section( 'header_image', array(
3583                        'title'          => $title,
3584                        'description'    => $description,
3585                        'theme_supports' => 'custom-header',
3586                        'priority'       => 60,
3587                ) );
3588
3589                $this->add_setting( 'header_video', array(
3590                        'theme_supports'    => array( 'custom-header', 'video' ),
3591                        'transport'         => 'postMessage',
3592                        'sanitize_callback' => 'absint',
3593                        'validate_callback' => array( $this, '_validate_header_video' ),
3594                ) );
3595
3596                $this->add_setting( 'external_header_video', array(
3597                        'theme_supports'    => array( 'custom-header', 'video' ),
3598                        'transport'         => 'postMessage',
3599                        'sanitize_callback' => 'esc_url',
3600                        'validate_callback' => array( $this, '_validate_external_header_video' ),
3601                ) );
3602
3603                $this->add_setting( new WP_Customize_Filter_Setting( $this, 'header_image', array(
3604                        'default'        => sprintf( get_theme_support( 'custom-header', 'default-image' ), get_template_directory_uri(), get_stylesheet_directory_uri() ),
3605                        'theme_supports' => 'custom-header',
3606                ) ) );
3607
3608                $this->add_setting( new WP_Customize_Header_Image_Setting( $this, 'header_image_data', array(
3609                        'theme_supports' => 'custom-header',
3610                ) ) );
3611
3612                /*
3613                 * Switch image settings to postMessage when video support is enabled since
3614                 * it entails that the_custom_header_markup() will be used, and thus selective
3615                 * refresh can be utilized.
3616                 */
3617                if ( current_theme_supports( 'custom-header', 'video' ) ) {
3618                        $this->get_setting( 'header_image' )->transport = 'postMessage';
3619                        $this->get_setting( 'header_image_data' )->transport = 'postMessage';
3620                }
3621
3622                $this->add_control( new WP_Customize_Media_Control( $this, 'header_video', array(
3623                        'theme_supports' => array( 'custom-header', 'video' ),
3624                        'label'          => __( 'Header Video' ),
3625                        'description'    => $control_description,
3626                        'section'        => 'header_image',
3627                        'mime_type'      => 'video',
3628                        // @todo These button_labels can be removed once WP_Customize_Media_Control provides mime_type-specific labels automatically. See <https://core.trac.wordpress.org/ticket/38796>.
3629                        'button_labels'  => array(
3630                                'select'       => __( 'Select Video' ),
3631                                'change'       => __( 'Change Video' ),
3632                                'placeholder'  => __( 'No video selected' ),
3633                                'frame_title'  => __( 'Select Video' ),
3634                                'frame_button' => __( 'Choose Video' ),
3635                        ),
3636                        'active_callback' => 'is_header_video_active',
3637                ) ) );
3638
3639                $this->add_control( 'external_header_video', array(
3640                        'theme_supports' => array( 'custom-header', 'video' ),
3641                        'type'           => 'url',
3642                        'description'    => __( 'Or, enter a YouTube URL:' ),
3643                        'section'        => 'header_image',
3644                        'active_callback'=> 'is_front_page',
3645                ) );
3646
3647                $this->add_control( new WP_Customize_Header_Image_Control( $this ) );
3648
3649                $this->selective_refresh->add_partial( 'custom_header', array(
3650                        'selector'            => '#wp-custom-header',
3651                        'render_callback'     => 'the_custom_header_markup',
3652                        'settings'            => array( 'header_video', 'external_header_video', 'header_image' ), // The image is used as a video fallback here.
3653                        'container_inclusive' => true,
3654                ) );
3655
3656                /* Custom Background */
3657
3658                $this->add_section( 'background_image', array(
3659                        'title'          => __( 'Background Image' ),
3660                        'theme_supports' => 'custom-background',
3661                        'priority'       => 80,
3662                ) );
3663
3664                $this->add_setting( 'background_image', array(
3665                        'default'        => get_theme_support( 'custom-background', 'default-image' ),
3666                        'theme_supports' => 'custom-background',
3667                        'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
3668                ) );
3669
3670                $this->add_setting( new WP_Customize_Background_Image_Setting( $this, 'background_image_thumb', array(
3671                        'theme_supports' => 'custom-background',
3672                        'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
3673                ) ) );
3674
3675                $this->add_control( new WP_Customize_Background_Image_Control( $this ) );
3676
3677                $this->add_setting( 'background_preset', array(
3678                        'default'        => get_theme_support( 'custom-background', 'default-preset' ),
3679                        'theme_supports' => 'custom-background',
3680                        'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
3681                ) );
3682
3683                $this->add_control( 'background_preset', array(
3684                        'label'      => _x( 'Preset', 'Background Preset' ),
3685                        'section'    => 'background_image',
3686                        'type'       => 'select',
3687                        'choices'    => array(
3688                                'default' => _x( 'Default', 'Default Preset' ),
3689                                'fill'    => __( 'Fill Screen' ),
3690                                'fit'     => __( 'Fit to Screen' ),
3691                                'repeat'  => _x( 'Repeat', 'Repeat Image' ),
3692                                'custom'  => _x( 'Custom', 'Custom Preset' ),
3693                        ),
3694                ) );
3695
3696                $this->add_setting( 'background_position_x', array(
3697                        'default'        => get_theme_support( 'custom-background', 'default-position-x' ),
3698                        'theme_supports' => 'custom-background',
3699                        'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
3700                ) );
3701
3702                $this->add_setting( 'background_position_y', array(
3703                        'default'        => get_theme_support( 'custom-background', 'default-position-y' ),
3704                        'theme_supports' => 'custom-background',
3705                        'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
3706                ) );
3707
3708                $this->add_control( new WP_Customize_Background_Position_Control( $this, 'background_position', array(
3709                        'label'    => __( 'Image Position' ),
3710                        'section'  => 'background_image',
3711                        'settings' => array(
3712                                'x' => 'background_position_x',
3713                                'y' => 'background_position_y',
3714                        ),
3715                ) ) );
3716
3717                $this->add_setting( 'background_size', array(
3718                        'default'        => get_theme_support( 'custom-background', 'default-size' ),
3719                        'theme_supports' => 'custom-background',
3720                        'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
3721                ) );
3722
3723                $this->add_control( 'background_size', array(
3724                        'label'      => __( 'Image Size' ),
3725                        'section'    => 'background_image',
3726                        'type'       => 'select',
3727                        'choices'    => array(
3728                                'auto'    => __( 'Original' ),
3729                                'contain' => __( 'Fit to Screen' ),
3730                                'cover'   => __( 'Fill Screen' ),
3731                        ),
3732                ) );
3733
3734                $this->add_setting( 'background_repeat', array(
3735                        'default'           => get_theme_support( 'custom-background', 'default-repeat' ),
3736                        'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
3737                        'theme_supports'    => 'custom-background',
3738                ) );
3739
3740                $this->add_control( 'background_repeat', array(
3741                        'label'    => __( 'Repeat Background Image' ),
3742                        'section'  => 'background_image',
3743                        'type'     => 'checkbox',
3744                ) );
3745
3746                $this->add_setting( 'background_attachment', array(
3747                        'default'           => get_theme_support( 'custom-background', 'default-attachment' ),
3748                        'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
3749                        'theme_supports'    => 'custom-background',
3750                ) );
3751
3752                $this->add_control( 'background_attachment', array(
3753                        'label'    => __( 'Scroll with Page' ),
3754                        'section'  => 'background_image',
3755                        'type'     => 'checkbox',
3756                ) );
3757
3758
3759                // If the theme is using the default background callback, we can update
3760                // the background CSS using postMessage.
3761                if ( get_theme_support( 'custom-background', 'wp-head-callback' ) === '_custom_background_cb' ) {
3762                        foreach ( array( 'color', 'image', 'preset', 'position_x', 'position_y', 'size', 'repeat', 'attachment' ) as $prop ) {
3763                                $this->get_setting( 'background_' . $prop )->transport = 'postMessage';
3764                        }
3765                }
3766
3767                /*
3768                 * Static Front Page
3769                 * See also https://core.trac.wordpress.org/ticket/19627 which introduces the the static-front-page theme_support.
3770                 * The following replicates behavior from options-reading.php.
3771                 */
3772
3773                $this->add_section( 'static_front_page', array(
3774                        'title' => __( 'Static Front Page' ),
3775                        'priority' => 120,
3776                        'description' => __( 'Your theme supports a static front page.' ),
3777                        'active_callback' => array( $this, 'has_published_pages' ),
3778                ) );
3779
3780                $this->add_setting( 'show_on_front', array(
3781                        'default' => get_option( 'show_on_front' ),
3782                        'capability' => 'manage_options',
3783                        'type' => 'option',
3784                ) );
3785
3786                $this->add_control( 'show_on_front', array(
3787                        'label' => __( 'Front page displays' ),
3788                        'section' => 'static_front_page',
3789                        'type' => 'radio',
3790                        'choices' => array(
3791                                'posts' => __( 'Your latest posts' ),
3792                                'page'  => __( 'A static page' ),
3793                        ),
3794                ) );
3795
3796                $this->add_setting( 'page_on_front', array(
3797                        'type'       => 'option',
3798                        'capability' => 'manage_options',
3799                ) );
3800
3801                $this->add_control( 'page_on_front', array(
3802                        'label' => __( 'Front page' ),
3803                        'section' => 'static_front_page',
3804                        'type' => 'dropdown-pages',
3805                        'allow_addition' => true,
3806                ) );
3807
3808                $this->add_setting( 'page_for_posts', array(
3809                        'type' => 'option',
3810                        'capability' => 'manage_options',
3811                ) );
3812
3813                $this->add_control( 'page_for_posts', array(
3814                        'label' => __( 'Posts page' ),
3815                        'section' => 'static_front_page',
3816                        'type' => 'dropdown-pages',
3817                        'allow_addition' => true,
3818                ) );
3819
3820                /* Custom CSS */
3821                $this->add_section( 'custom_css', array(
3822                        'title'              => __( 'Additional CSS' ),
3823                        'priority'           => 200,
3824                        'description_hidden' => true,
3825                        'description'        => sprintf( '%s<br />%s',
3826                                __( 'CSS allows you to customize the appearance and layout of your site with code. Separate CSS is saved for each of your themes. In the editing area the Tab key enters a tab character. To move below this area by pressing Tab, press the Esc key followed by the Tab key.' ),
3827                                __( '<a href="https://codex.wordpress.org/Know_Your_Sources#CSS" class="external-link" target="_blank">Learn more about CSS<span class="screen-reader-text">(link opens in a new window)</span></a>' )
3828                        ),
3829                ) );
3830
3831                $custom_css_setting = new WP_Customize_Custom_CSS_Setting( $this, sprintf( 'custom_css[%s]', get_stylesheet() ), array(
3832                        'capability' => 'edit_css',
3833                        'default' => sprintf( "/*\n%s\n*/", __( "You can add your own CSS here.\n\nClick the help icon above to learn more." ) ),
3834                ) );
3835                $this->add_setting( $custom_css_setting );
3836
3837                $this->add_control( 'custom_css', array(
3838                        'type'     => 'textarea',
3839                        'section'  => 'custom_css',
3840                        'settings' => array( 'default' => $custom_css_setting->id ),
3841                ) );
3842        }
3843
3844        /**
3845         * Return whether there are published pages.
3846         *
3847         * Used as active callback for static front page section and controls.
3848         *
3849         * @access private
3850         * @since 4.7.0
3851         *
3852         * @returns bool Whether there are published (or to be published) pages.
3853         */
3854        public function has_published_pages() {
3855
3856                $setting = $this->get_setting( 'nav_menus_created_posts' );
3857                if ( $setting ) {
3858                        foreach ( $setting->value() as $post_id ) {
3859                                if ( 'page' === get_post_type( $post_id ) ) {
3860                                        return true;
3861                                }
3862                        }
3863                }
3864                return 0 !== count( get_pages() );
3865        }
3866
3867        /**
3868         * Add settings from the POST data that were not added with code, e.g. dynamically-created settings for Widgets
3869         *
3870         * @since 4.2.0
3871         * @access public
3872         *
3873         * @see add_dynamic_settings()
3874         */
3875        public function register_dynamic_settings() {
3876                $setting_ids = array_keys( $this->unsanitized_post_values() );
3877                $this->add_dynamic_settings( $setting_ids );
3878        }
3879
3880        /**
3881         * Callback for validating the header_textcolor value.
3882         *
3883         * Accepts 'blank', and otherwise uses sanitize_hex_color_no_hash().
3884         * Returns default text color if hex color is empty.
3885         *
3886         * @since 3.4.0
3887         *
3888         * @param string $color
3889         * @return mixed
3890         */
3891        public function _sanitize_header_textcolor( $color ) {
3892                if ( 'blank' === $color )
3893                        return 'blank';
3894
3895                $color = sanitize_hex_color_no_hash( $color );
3896                if ( empty( $color ) )
3897                        $color = get_theme_support( 'custom-header', 'default-text-color' );
3898
3899                return $color;
3900        }
3901
3902        /**
3903         * Callback for validating a background setting value.
3904         *
3905         * @since 4.7.0
3906         * @access private
3907         *
3908         * @param string $value Repeat value.
3909         * @param WP_Customize_Setting $setting Setting.
3910         * @return string|WP_Error Background value or validation error.
3911         */
3912        public function _sanitize_background_setting( $value, $setting ) {
3913                if ( 'background_repeat' === $setting->id ) {
3914                        if ( ! in_array( $value, array( 'repeat-x', 'repeat-y', 'repeat', 'no-repeat' ) ) ) {
3915                                return new WP_Error( 'invalid_value', __( 'Invalid value for background repeat.' ) );
3916                        }
3917                } else if ( 'background_attachment' === $setting->id ) {
3918                        if ( ! in_array( $value, array( 'fixed', 'scroll' ) ) ) {
3919                                return new WP_Error( 'invalid_value', __( 'Invalid value for background attachment.' ) );
3920                        }
3921                } else if ( 'background_position_x' === $setting->id ) {
3922                        if ( ! in_array( $value, array( 'left', 'center', 'right' ), true ) ) {
3923                                return new WP_Error( 'invalid_value', __( 'Invalid value for background position X.' ) );
3924                        }
3925                } else if ( 'background_position_y' === $setting->id ) {
3926                        if ( ! in_array( $value, array( 'top', 'center', 'bottom' ), true ) ) {
3927                                return new WP_Error( 'invalid_value', __( 'Invalid value for background position Y.' ) );
3928                        }
3929                } else if ( 'background_size' === $setting->id ) {
3930                        if ( ! in_array( $value, array( 'auto', 'contain', 'cover' ), true ) ) {
3931                                return new WP_Error( 'invalid_value', __( 'Invalid value for background size.' ) );
3932                        }
3933                } else if ( 'background_preset' === $setting->id ) {
3934                        if ( ! in_array( $value, array( 'default', 'fill', 'fit', 'repeat', 'custom' ), true ) ) {
3935                                return new WP_Error( 'invalid_value', __( 'Invalid value for background size.' ) );
3936                        }
3937                } else if ( 'background_image' === $setting->id || 'background_image_thumb' === $setting->id ) {
3938                        $value = empty( $value ) ? '' : esc_url_raw( $value );
3939                } else {
3940                        return new WP_Error( 'unrecognized_setting', __( 'Unrecognized background setting.' ) );
3941                }
3942                return $value;
3943        }
3944
3945        /**
3946         * Export header video settings to facilitate selective refresh.
3947         *
3948         * @since 4.7.0
3949         *
3950         * @param array $response Response.
3951         * @param WP_Customize_Selective_Refresh $selective_refresh Selective refresh component.
3952         * @param array $partials Array of partials.
3953         * @return array
3954         */
3955        public function export_header_video_settings( $response, $selective_refresh, $partials ) {
3956                if ( isset( $partials['custom_header'] ) ) {
3957                        $response['custom_header_settings'] = get_header_video_settings();
3958                }
3959
3960                return $response;
3961        }
3962
3963        /**
3964         * Callback for validating the header_video value.
3965         *
3966         * Ensures that the selected video is less than 8MB and provides an error message.
3967         *
3968         * @since 4.7.0
3969         *
3970         * @param WP_Error $validity
3971         * @param mixed $value
3972         * @return mixed
3973         */
3974        public function _validate_header_video( $validity, $value ) {
3975                $video = get_attached_file( absint( $value ) );
3976                if ( $video ) {
3977                        $size = filesize( $video );
3978                        if ( 8 < $size / pow( 1024, 2 ) ) { // Check whether the size is larger than 8MB.
3979                                $validity->add( 'size_too_large',
3980                                        __( 'This video file is too large to use as a header video. Try a shorter video or optimize the compression settings and re-upload a file that is less than 8MB. Or, upload your video to YouTube and link it with the option below.' )
3981                                );
3982                        }
3983                        if ( '.mp4' !== substr( $video, -4 ) && '.mov' !== substr( $video, -4 ) ) { // Check for .mp4 or .mov format, which (assuming h.264 encoding) are the only cross-browser-supported formats.
3984                                $validity->add( 'invalid_file_type', sprintf(
3985                                        /* translators: 1: .mp4, 2: .mov */
3986                                        __( 'Only %1$s or %2$s files may be used for header video. Please convert your video file and try again, or, upload your video to YouTube and link it with the option below.' ),
3987                                        '<code>.mp4</code>',
3988                                        '<code>.mov</code>'
3989                                ) );
3990                        }
3991                }
3992                return $validity;
3993        }
3994
3995        /**
3996         * Callback for validating the external_header_video value.
3997         *
3998         * Ensures that the provided URL is supported.
3999         *
4000         * @since 4.7.0
4001         *
4002         * @param WP_Error $validity
4003         * @param mixed $value
4004         * @return mixed
4005         */
4006        public function _validate_external_header_video( $validity, $value ) {
4007                $video = esc_url_raw( $value );
4008                if ( $video ) {
4009                        if ( ! preg_match( '#^https?://(?:www\.)?(?:youtube\.com/watch|youtu\.be/)#', $video ) ) {
4010                                $validity->add( 'invalid_url', __( 'Please enter a valid YouTube URL.' ) );
4011                        }
4012                }
4013                return $validity;
4014        }
4015
4016        /**
4017         * Callback for rendering the custom logo, used in the custom_logo partial.
4018         *
4019         * This method exists because the partial object and context data are passed
4020         * into a partial's render_callback so we cannot use get_custom_logo() as
4021         * the render_callback directly since it expects a blog ID as the first
4022         * argument. When WP no longer supports PHP 5.3, this method can be removed
4023         * in favor of an anonymous function.
4024         *
4025         * @see WP_Customize_Manager::register_controls()
4026         *
4027         * @since 4.5.0
4028         * @access private
4029         *
4030         * @return string Custom logo.
4031         */
4032        public function _render_custom_logo_partial() {
4033                return get_custom_logo();
4034        }
4035}