Make WordPress Core

source: branches/4.9/src/wp-includes/class-wp-customize-manager.php @ 42811

Last change on this file since 42811 was 42811, checked in by SergeyBiryukov, 4 years ago

General: Replace Cheatin’ uh? with friendlier error messages.

While intended as a playful error message, Cheatin’ uh? can be interpreted as insulting or accusatory in an already stressful situation. This replaces Cheatin’ with more meaningful error messages, depending on the error that occurs.

Props ElectricFeet, EricMeyer, karmatosed, dd32, BandonRandon, melchoyce, kristastevens for language; dmsnell for original patch; peterwilsoncc.
Merged [42648] and [42719] to the 4.9 branch.
Fixes #38332.

  • Property svn:eol-style set to native
File size: 192.6 KB
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         * @var WP_Theme
29         */
30        protected $theme;
31
32        /**
33         * The directory name of the previously active theme (within the theme_root).
34         *
35         * @since 3.4.0
36         * @var string
37         */
38        protected $original_stylesheet;
39
40        /**
41         * Whether this is a Customizer pageload.
42         *
43         * @since 3.4.0
44         * @var bool
45         */
46        protected $previewing = false;
47
48        /**
49         * Methods and properties dealing with managing widgets in the Customizer.
50         *
51         * @since 3.9.0
52         * @var WP_Customize_Widgets
53         */
54        public $widgets;
55
56        /**
57         * Methods and properties dealing with managing nav menus in the Customizer.
58         *
59         * @since 4.3.0
60         * @var WP_Customize_Nav_Menus
61         */
62        public $nav_menus;
63
64        /**
65         * Methods and properties dealing with selective refresh in the Customizer preview.
66         *
67         * @since 4.5.0
68         * @var WP_Customize_Selective_Refresh
69         */
70        public $selective_refresh;
71
72        /**
73         * Registered instances of WP_Customize_Setting.
74         *
75         * @since 3.4.0
76         * @var array
77         */
78        protected $settings = array();
79
80        /**
81         * Sorted top-level instances of WP_Customize_Panel and WP_Customize_Section.
82         *
83         * @since 4.0.0
84         * @var array
85         */
86        protected $containers = array();
87
88        /**
89         * Registered instances of WP_Customize_Panel.
90         *
91         * @since 4.0.0
92         * @var array
93         */
94        protected $panels = array();
95
96        /**
97         * List of core components.
98         *
99         * @since 4.5.0
100         * @var array
101         */
102        protected $components = array( 'widgets', 'nav_menus' );
103
104        /**
105         * Registered instances of WP_Customize_Section.
106         *
107         * @since 3.4.0
108         * @var array
109         */
110        protected $sections = array();
111
112        /**
113         * Registered instances of WP_Customize_Control.
114         *
115         * @since 3.4.0
116         * @var array
117         */
118        protected $controls = array();
119
120        /**
121         * Panel types that may be rendered from JS templates.
122         *
123         * @since 4.3.0
124         * @var array
125         */
126        protected $registered_panel_types = array();
127
128        /**
129         * Section types that may be rendered from JS templates.
130         *
131         * @since 4.3.0
132         * @var array
133         */
134        protected $registered_section_types = array();
135
136        /**
137         * Control types that may be rendered from JS templates.
138         *
139         * @since 4.1.0
140         * @var array
141         */
142        protected $registered_control_types = array();
143
144        /**
145         * Initial URL being previewed.
146         *
147         * @since 4.4.0
148         * @var string
149         */
150        protected $preview_url;
151
152        /**
153         * URL to link the user to when closing the Customizer.
154         *
155         * @since 4.4.0
156         * @var string
157         */
158        protected $return_url;
159
160        /**
161         * Mapping of 'panel', 'section', 'control' to the ID which should be autofocused.
162         *
163         * @since 4.4.0
164         * @var array
165         */
166        protected $autofocus = array();
167
168        /**
169         * Messenger channel.
170         *
171         * @since 4.7.0
172         * @var string
173         */
174        protected $messenger_channel;
175
176        /**
177         * Whether the autosave revision of the changeset should be loaded.
178         *
179         * @since 4.9.0
180         * @var bool
181         */
182        protected $autosaved = false;
183
184        /**
185         * Whether the changeset branching is allowed.
186         *
187         * @since 4.9.0
188         * @var bool
189         */
190        protected $branching = true;
191
192        /**
193         * Whether settings should be previewed.
194         *
195         * @since 4.9.0
196         * @var bool
197         */
198        protected $settings_previewed = true;
199
200        /**
201         * Whether a starter content changeset was saved.
202         *
203         * @since 4.9.0
204         * @var bool
205         */
206        protected $saved_starter_content_changeset = false;
207
208        /**
209         * Unsanitized values for Customize Settings parsed from $_POST['customized'].
210         *
211         * @var array
212         */
213        private $_post_values;
214
215        /**
216         * Changeset UUID.
217         *
218         * @since 4.7.0
219         * @var string
220         */
221        private $_changeset_uuid;
222
223        /**
224         * Changeset post ID.
225         *
226         * @since 4.7.0
227         * @var int|false
228         */
229        private $_changeset_post_id;
230
231        /**
232         * Changeset data loaded from a customize_changeset post.
233         *
234         * @since 4.7.0
235         * @var array
236         */
237        private $_changeset_data;
238
239        /**
240         * Constructor.
241         *
242         * @since 3.4.0
243         * @since 4.7.0 Added $args param.
244         *
245         * @param array $args {
246         *     Args.
247         *
248         *     @type null|string|false $changeset_uuid     Changeset UUID, the `post_name` for the customize_changeset post containing the customized state.
249         *                                                 Defaults to `null` resulting in a UUID to be immediately generated. If `false` is provided, then
250         *                                                 then the changeset UUID will be determined during `after_setup_theme`: when the
251         *                                                 `customize_changeset_branching` filter returns false, then the default UUID will be that
252         *                                                 of the most recent `customize_changeset` post that has a status other than 'auto-draft',
253         *                                                 'publish', or 'trash'. Otherwise, if changeset branching is enabled, then a random UUID will be used.
254         *     @type string            $theme              Theme to be previewed (for theme switch). Defaults to customize_theme or theme query params.
255         *     @type string            $messenger_channel  Messenger channel. Defaults to customize_messenger_channel query param.
256         *     @type bool              $settings_previewed If settings should be previewed. Defaults to true.
257         *     @type bool              $branching          If changeset branching is allowed; otherwise, changesets are linear. Defaults to true.
258         *     @type bool              $autosaved          If data from a changeset's autosaved revision should be loaded if it exists. Defaults to false.
259         * }
260         */
261        public function __construct( $args = array() ) {
262
263                $args = array_merge(
264                        array_fill_keys( array( 'changeset_uuid', 'theme', 'messenger_channel', 'settings_previewed', 'autosaved', 'branching' ), null ),
265                        $args
266                );
267
268                // Note that the UUID format will be validated in the setup_theme() method.
269                if ( ! isset( $args['changeset_uuid'] ) ) {
270                        $args['changeset_uuid'] = wp_generate_uuid4();
271                }
272
273                // The theme and messenger_channel should be supplied via $args, but they are also looked at in the $_REQUEST global here for back-compat.
274                if ( ! isset( $args['theme'] ) ) {
275                        if ( isset( $_REQUEST['customize_theme'] ) ) {
276                                $args['theme'] = wp_unslash( $_REQUEST['customize_theme'] );
277                        } elseif ( isset( $_REQUEST['theme'] ) ) { // Deprecated.
278                                $args['theme'] = wp_unslash( $_REQUEST['theme'] );
279                        }
280                }
281                if ( ! isset( $args['messenger_channel'] ) && isset( $_REQUEST['customize_messenger_channel'] ) ) {
282                        $args['messenger_channel'] = sanitize_key( wp_unslash( $_REQUEST['customize_messenger_channel'] ) );
283                }
284
285                $this->original_stylesheet = get_stylesheet();
286                $this->theme = wp_get_theme( 0 === validate_file( $args['theme'] ) ? $args['theme'] : null );
287                $this->messenger_channel = $args['messenger_channel'];
288                $this->_changeset_uuid = $args['changeset_uuid'];
289
290                foreach ( array( 'settings_previewed', 'autosaved', 'branching' ) as $key ) {
291                        if ( isset( $args[ $key ] ) ) {
292                                $this->$key = (bool) $args[ $key ];
293                        }
294                }
295
296                require_once( ABSPATH . WPINC . '/class-wp-customize-setting.php' );
297                require_once( ABSPATH . WPINC . '/class-wp-customize-panel.php' );
298                require_once( ABSPATH . WPINC . '/class-wp-customize-section.php' );
299                require_once( ABSPATH . WPINC . '/class-wp-customize-control.php' );
300
301                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-color-control.php' );
302                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-media-control.php' );
303                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-upload-control.php' );
304                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-image-control.php' );
305                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-background-image-control.php' );
306                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-background-position-control.php' );
307                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-cropped-image-control.php' );
308                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-site-icon-control.php' );
309                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-header-image-control.php' );
310                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-theme-control.php' );
311                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-code-editor-control.php' );
312                require_once( ABSPATH . WPINC . '/customize/class-wp-widget-area-customize-control.php' );
313                require_once( ABSPATH . WPINC . '/customize/class-wp-widget-form-customize-control.php' );
314                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-control.php' );
315                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-item-control.php' );
316                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-location-control.php' );
317                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-name-control.php' );
318                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-locations-control.php' );
319                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-auto-add-control.php' );
320                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-new-menu-control.php' ); // @todo Remove in 5.0. See #42364.
321
322                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menus-panel.php' );
323
324                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-themes-panel.php' );
325                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-themes-section.php' );
326                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-sidebar-section.php' );
327                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-section.php' );
328                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-new-menu-section.php' ); // @todo Remove in 5.0. See #42364.
329
330                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-custom-css-setting.php' );
331                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-filter-setting.php' );
332                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-header-image-setting.php' );
333                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-background-image-setting.php' );
334                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-item-setting.php' );
335                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-setting.php' );
336
337                /**
338                 * Filters the core Customizer components to load.
339                 *
340                 * This allows Core components to be excluded from being instantiated by
341                 * filtering them out of the array. Note that this filter generally runs
342                 * during the {@see 'plugins_loaded'} action, so it cannot be added
343                 * in a theme.
344                 *
345                 * @since 4.4.0
346                 *
347                 * @see WP_Customize_Manager::__construct()
348                 *
349                 * @param array                $components List of core components to load.
350                 * @param WP_Customize_Manager $this       WP_Customize_Manager instance.
351                 */
352                $components = apply_filters( 'customize_loaded_components', $this->components, $this );
353
354                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-selective-refresh.php' );
355                $this->selective_refresh = new WP_Customize_Selective_Refresh( $this );
356
357                if ( in_array( 'widgets', $components, true ) ) {
358                        require_once( ABSPATH . WPINC . '/class-wp-customize-widgets.php' );
359                        $this->widgets = new WP_Customize_Widgets( $this );
360                }
361
362                if ( in_array( 'nav_menus', $components, true ) ) {
363                        require_once( ABSPATH . WPINC . '/class-wp-customize-nav-menus.php' );
364                        $this->nav_menus = new WP_Customize_Nav_Menus( $this );
365                }
366
367                add_action( 'setup_theme', array( $this, 'setup_theme' ) );
368                add_action( 'wp_loaded',   array( $this, 'wp_loaded' ) );
369
370                // Do not spawn cron (especially the alternate cron) while running the Customizer.
371                remove_action( 'init', 'wp_cron' );
372
373                // Do not run update checks when rendering the controls.
374                remove_action( 'admin_init', '_maybe_update_core' );
375                remove_action( 'admin_init', '_maybe_update_plugins' );
376                remove_action( 'admin_init', '_maybe_update_themes' );
377
378                add_action( 'wp_ajax_customize_save',                     array( $this, 'save' ) );
379                add_action( 'wp_ajax_customize_trash',                    array( $this, 'handle_changeset_trash_request' ) );
380                add_action( 'wp_ajax_customize_refresh_nonces',           array( $this, 'refresh_nonces' ) );
381                add_action( 'wp_ajax_customize_load_themes',              array( $this, 'handle_load_themes_request' ) );
382                add_filter( 'heartbeat_settings',                         array( $this, 'add_customize_screen_to_heartbeat_settings' ) );
383                add_filter( 'heartbeat_received',                         array( $this, 'check_changeset_lock_with_heartbeat' ), 10, 3 );
384                add_action( 'wp_ajax_customize_override_changeset_lock',  array( $this, 'handle_override_changeset_lock_request' ) );
385                add_action( 'wp_ajax_customize_dismiss_autosave_or_lock', array( $this, 'handle_dismiss_autosave_or_lock_request' ) );
386
387                add_action( 'customize_register',                 array( $this, 'register_controls' ) );
388                add_action( 'customize_register',                 array( $this, 'register_dynamic_settings' ), 11 ); // allow code to create settings first
389                add_action( 'customize_controls_init',            array( $this, 'prepare_controls' ) );
390                add_action( 'customize_controls_enqueue_scripts', array( $this, 'enqueue_control_scripts' ) );
391
392                // Render Common, Panel, Section, and Control templates.
393                add_action( 'customize_controls_print_footer_scripts', array( $this, 'render_panel_templates' ), 1 );
394                add_action( 'customize_controls_print_footer_scripts', array( $this, 'render_section_templates' ), 1 );
395                add_action( 'customize_controls_print_footer_scripts', array( $this, 'render_control_templates' ), 1 );
396
397                // Export header video settings with the partial response.
398                add_filter( 'customize_render_partials_response', array( $this, 'export_header_video_settings' ), 10, 3 );
399
400                // Export the settings to JS via the _wpCustomizeSettings variable.
401                add_action( 'customize_controls_print_footer_scripts', array( $this, 'customize_pane_settings' ), 1000 );
402
403                // Add theme update notices.
404                if ( current_user_can( 'install_themes' ) || current_user_can( 'update_themes' ) ) {
405                        require_once ABSPATH . '/wp-admin/includes/update.php';
406                        add_action( 'customize_controls_print_footer_scripts', 'wp_print_admin_notice_templates' );
407                }
408        }
409
410        /**
411         * Return true if it's an Ajax request.
412         *
413         * @since 3.4.0
414         * @since 4.2.0 Added `$action` param.
415         *
416         * @param string|null $action Whether the supplied Ajax action is being run.
417         * @return bool True if it's an Ajax request, false otherwise.
418         */
419        public function doing_ajax( $action = null ) {
420                if ( ! wp_doing_ajax() ) {
421                        return false;
422                }
423
424                if ( ! $action ) {
425                        return true;
426                } else {
427                        /*
428                         * Note: we can't just use doing_action( "wp_ajax_{$action}" ) because we need
429                         * to check before admin-ajax.php gets to that point.
430                         */
431                        return isset( $_REQUEST['action'] ) && wp_unslash( $_REQUEST['action'] ) === $action;
432                }
433        }
434
435        /**
436         * Custom wp_die wrapper. Returns either the standard message for UI
437         * or the Ajax message.
438         *
439         * @since 3.4.0
440         *
441         * @param mixed $ajax_message Ajax return
442         * @param mixed $message UI message
443         */
444        protected function wp_die( $ajax_message, $message = null ) {
445                if ( $this->doing_ajax() ) {
446                        wp_die( $ajax_message );
447                }
448
449                if ( ! $message ) {
450                        $message = __( 'Something went wrong.' );
451                }
452
453                if ( $this->messenger_channel ) {
454                        ob_start();
455                        wp_enqueue_scripts();
456                        wp_print_scripts( array( 'customize-base' ) );
457
458                        $settings = array(
459                                'messengerArgs' => array(
460                                        'channel' => $this->messenger_channel,
461                                        'url' => wp_customize_url(),
462                                ),
463                                'error' => $ajax_message,
464                        );
465                        ?>
466                        <script>
467                        ( function( api, settings ) {
468                                var preview = new api.Messenger( settings.messengerArgs );
469                                preview.send( 'iframe-loading-error', settings.error );
470                        } )( wp.customize, <?php echo wp_json_encode( $settings ) ?> );
471                        </script>
472                        <?php
473                        $message .= ob_get_clean();
474                }
475
476                wp_die( $message );
477        }
478
479        /**
480         * Return the Ajax wp_die() handler if it's a customized request.
481         *
482         * @since 3.4.0
483         * @deprecated 4.7.0
484         *
485         * @return callable Die handler.
486         */
487        public function wp_die_handler() {
488                _deprecated_function( __METHOD__, '4.7.0' );
489
490                if ( $this->doing_ajax() || isset( $_POST['customized'] ) ) {
491                        return '_ajax_wp_die_handler';
492                }
493
494                return '_default_wp_die_handler';
495        }
496
497        /**
498         * Start preview and customize theme.
499         *
500         * Check if customize query variable exist. Init filters to filter the current theme.
501         *
502         * @since 3.4.0
503         *
504         * @global string $pagenow
505         */
506        public function setup_theme() {
507                global $pagenow;
508
509                // Check permissions for customize.php access since this method is called before customize.php can run any code,
510                if ( 'customize.php' === $pagenow && ! current_user_can( 'customize' ) ) {
511                        if ( ! is_user_logged_in() ) {
512                                auth_redirect();
513                        } else {
514                                wp_die(
515                                        '<h1>' . __( 'You need a higher level of permission.' ) . '</h1>' .
516                                        '<p>' . __( 'Sorry, you are not allowed to customize this site.' ) . '</p>',
517                                        403
518                                );
519                        }
520                        return;
521                }
522
523                // If a changeset was provided is invalid.
524                if ( isset( $this->_changeset_uuid ) && false !== $this->_changeset_uuid && ! wp_is_uuid( $this->_changeset_uuid ) ) {
525                        $this->wp_die( -1, __( 'Invalid changeset UUID' ) );
526                }
527
528                /*
529                 * Clear incoming post data if the user lacks a CSRF token (nonce). Note that the customizer
530                 * application will inject the customize_preview_nonce query parameter into all Ajax requests.
531                 * For similar behavior elsewhere in WordPress, see rest_cookie_check_errors() which logs out
532                 * a user when a valid nonce isn't present.
533                 */
534                $has_post_data_nonce = (
535                        check_ajax_referer( 'preview-customize_' . $this->get_stylesheet(), 'nonce', false )
536                        ||
537                        check_ajax_referer( 'save-customize_' . $this->get_stylesheet(), 'nonce', false )
538                        ||
539                        check_ajax_referer( 'preview-customize_' . $this->get_stylesheet(), 'customize_preview_nonce', false )
540                );
541                if ( ! current_user_can( 'customize' ) || ! $has_post_data_nonce ) {
542                        unset( $_POST['customized'] );
543                        unset( $_REQUEST['customized'] );
544                }
545
546                /*
547                 * If unauthenticated then require a valid changeset UUID to load the preview.
548                 * In this way, the UUID serves as a secret key. If the messenger channel is present,
549                 * then send unauthenticated code to prompt re-auth.
550                 */
551                if ( ! current_user_can( 'customize' ) && ! $this->changeset_post_id() ) {
552                        $this->wp_die( $this->messenger_channel ? 0 : -1, __( 'Non-existent changeset UUID.' ) );
553                }
554
555                if ( ! headers_sent() ) {
556                        send_origin_headers();
557                }
558
559                // Hide the admin bar if we're embedded in the customizer iframe.
560                if ( $this->messenger_channel ) {
561                        show_admin_bar( false );
562                }
563
564                if ( $this->is_theme_active() ) {
565                        // Once the theme is loaded, we'll validate it.
566                        add_action( 'after_setup_theme', array( $this, 'after_setup_theme' ) );
567                } else {
568                        // If the requested theme is not the active theme and the user doesn't have the
569                        // switch_themes cap, bail.
570                        if ( ! current_user_can( 'switch_themes' ) ) {
571                                $this->wp_die( -1, __( 'Sorry, you are not allowed to edit theme options on this site.' ) );
572                        }
573
574                        // If the theme has errors while loading, bail.
575                        if ( $this->theme()->errors() ) {
576                                $this->wp_die( -1, $this->theme()->errors()->get_error_message() );
577                        }
578
579                        // If the theme isn't allowed per multisite settings, bail.
580                        if ( ! $this->theme()->is_allowed() ) {
581                                $this->wp_die( -1, __( 'The requested theme does not exist.' ) );
582                        }
583                }
584
585                // Make sure changeset UUID is established immediately after the theme is loaded.
586                add_action( 'after_setup_theme', array( $this, 'establish_loaded_changeset' ), 5 );
587
588                /*
589                 * Import theme starter content for fresh installations when landing in the customizer.
590                 * Import starter content at after_setup_theme:100 so that any
591                 * add_theme_support( 'starter-content' ) calls will have been made.
592                 */
593                if ( get_option( 'fresh_site' ) && 'customize.php' === $pagenow ) {
594                        add_action( 'after_setup_theme', array( $this, 'import_theme_starter_content' ), 100 );
595                }
596
597                $this->start_previewing_theme();
598        }
599
600        /**
601         * Establish the loaded changeset.
602         *
603         * This method runs right at after_setup_theme and applies the 'customize_changeset_branching' filter to determine
604         * whether concurrent changesets are allowed. Then if the Customizer is not initialized with a `changeset_uuid` param,
605         * this method will determine which UUID should be used. If changeset branching is disabled, then the most saved
606         * changeset will be loaded by default. Otherwise, if there are no existing saved changesets or if changeset branching is
607         * enabled, then a new UUID will be generated.
608         *
609         * @since 4.9.0
610         * @global string $pagenow
611         */
612        public function establish_loaded_changeset() {
613                global $pagenow;
614
615                if ( empty( $this->_changeset_uuid ) ) {
616                        $changeset_uuid = null;
617
618                        if ( ! $this->branching() && $this->is_theme_active() ) {
619                                $unpublished_changeset_posts = $this->get_changeset_posts( array(
620                                        'post_status' => array_diff( get_post_stati(), array( 'auto-draft', 'publish', 'trash', 'inherit', 'private' ) ),
621                                        'exclude_restore_dismissed' => false,
622                                        'author' => 'any',
623                                        'posts_per_page' => 1,
624                                        'order' => 'DESC',
625                                        'orderby' => 'date',
626                                ) );
627                                $unpublished_changeset_post = array_shift( $unpublished_changeset_posts );
628                                if ( ! empty( $unpublished_changeset_post ) && wp_is_uuid( $unpublished_changeset_post->post_name ) ) {
629                                        $changeset_uuid = $unpublished_changeset_post->post_name;
630                                }
631                        }
632
633                        // If no changeset UUID has been set yet, then generate a new one.
634                        if ( empty( $changeset_uuid ) ) {
635                                $changeset_uuid = wp_generate_uuid4();
636                        }
637
638                        $this->_changeset_uuid = $changeset_uuid;
639                }
640
641                if ( is_admin() && 'customize.php' === $pagenow ) {
642                        $this->set_changeset_lock( $this->changeset_post_id() );
643                }
644        }
645
646        /**
647         * Callback to validate a theme once it is loaded
648         *
649         * @since 3.4.0
650         */
651        public function after_setup_theme() {
652                $doing_ajax_or_is_customized = ( $this->doing_ajax() || isset( $_POST['customized'] ) );
653                if ( ! $doing_ajax_or_is_customized && ! validate_current_theme() ) {
654                        wp_redirect( 'themes.php?broken=true' );
655                        exit;
656                }
657        }
658
659        /**
660         * If the theme to be previewed isn't the active theme, add filter callbacks
661         * to swap it out at runtime.
662         *
663         * @since 3.4.0
664         */
665        public function start_previewing_theme() {
666                // Bail if we're already previewing.
667                if ( $this->is_preview() ) {
668                        return;
669                }
670
671                $this->previewing = true;
672
673                if ( ! $this->is_theme_active() ) {
674                        add_filter( 'template', array( $this, 'get_template' ) );
675                        add_filter( 'stylesheet', array( $this, 'get_stylesheet' ) );
676                        add_filter( 'pre_option_current_theme', array( $this, 'current_theme' ) );
677
678                        // @link: https://core.trac.wordpress.org/ticket/20027
679                        add_filter( 'pre_option_stylesheet', array( $this, 'get_stylesheet' ) );
680                        add_filter( 'pre_option_template', array( $this, 'get_template' ) );
681
682                        // Handle custom theme roots.
683                        add_filter( 'pre_option_stylesheet_root', array( $this, 'get_stylesheet_root' ) );
684                        add_filter( 'pre_option_template_root', array( $this, 'get_template_root' ) );
685                }
686
687                /**
688                 * Fires once the Customizer theme preview has started.
689                 *
690                 * @since 3.4.0
691                 *
692                 * @param WP_Customize_Manager $this WP_Customize_Manager instance.
693                 */
694                do_action( 'start_previewing_theme', $this );
695        }
696
697        /**
698         * Stop previewing the selected theme.
699         *
700         * Removes filters to change the current theme.
701         *
702         * @since 3.4.0
703         */
704        public function stop_previewing_theme() {
705                if ( ! $this->is_preview() ) {
706                        return;
707                }
708
709                $this->previewing = false;
710
711                if ( ! $this->is_theme_active() ) {
712                        remove_filter( 'template', array( $this, 'get_template' ) );
713                        remove_filter( 'stylesheet', array( $this, 'get_stylesheet' ) );
714                        remove_filter( 'pre_option_current_theme', array( $this, 'current_theme' ) );
715
716                        // @link: https://core.trac.wordpress.org/ticket/20027
717                        remove_filter( 'pre_option_stylesheet', array( $this, 'get_stylesheet' ) );
718                        remove_filter( 'pre_option_template', array( $this, 'get_template' ) );
719
720                        // Handle custom theme roots.
721                        remove_filter( 'pre_option_stylesheet_root', array( $this, 'get_stylesheet_root' ) );
722                        remove_filter( 'pre_option_template_root', array( $this, 'get_template_root' ) );
723                }
724
725                /**
726                 * Fires once the Customizer theme preview has stopped.
727                 *
728                 * @since 3.4.0
729                 *
730                 * @param WP_Customize_Manager $this WP_Customize_Manager instance.
731                 */
732                do_action( 'stop_previewing_theme', $this );
733        }
734
735        /**
736         * Gets whether settings are or will be previewed.
737         *
738         * @since 4.9.0
739         * @see WP_Customize_Setting::preview()
740         *
741         * @return bool
742         */
743        public function settings_previewed() {
744                return $this->settings_previewed;
745        }
746
747        /**
748         * Gets whether data from a changeset's autosaved revision should be loaded if it exists.
749         *
750         * @since 4.9.0
751         * @see WP_Customize_Manager::changeset_data()
752         *
753         * @return bool Is using autosaved changeset revision.
754         */
755        public function autosaved() {
756                return $this->autosaved;
757        }
758
759        /**
760         * Whether the changeset branching is allowed.
761         *
762         * @since 4.9.0
763         * @see WP_Customize_Manager::establish_loaded_changeset()
764         *
765         * @return bool Is changeset branching.
766         */
767        public function branching() {
768
769                /**
770                 * Filters whether or not changeset branching is allowed.
771                 *
772                 * By default in core, when changeset branching is not allowed, changesets will operate
773                 * linearly in that only one saved changeset will exist at a time (with a 'draft' or
774                 * 'future' status). This makes the Customizer operate in a way that is similar to going to
775                 * "edit" to one existing post: all users will be making changes to the same post, and autosave
776                 * revisions will be made for that post.
777                 *
778                 * By contrast, when changeset branching is allowed, then the model is like users going
779                 * to "add new" for a page and each user makes changes independently of each other since
780                 * they are all operating on their own separate pages, each getting their own separate
781                 * initial auto-drafts and then once initially saved, autosave revisions on top of that
782                 * user's specific post.
783                 *
784                 * Since linear changesets are deemed to be more suitable for the majority of WordPress users,
785                 * they are the default. For WordPress sites that have heavy site management in the Customizer
786                 * by multiple users then branching changesets should be enabled by means of this filter.
787                 *
788                 * @since 4.9.0
789                 *
790                 * @param bool                 $allow_branching Whether branching is allowed. If `false`, the default,
791                 *                                              then only one saved changeset exists at a time.
792                 * @param WP_Customize_Manager $wp_customize    Manager instance.
793                 */
794                $this->branching = apply_filters( 'customize_changeset_branching', $this->branching, $this );
795
796                return $this->branching;
797        }
798
799        /**
800         * Get the changeset UUID.
801         *
802         * @since 4.7.0
803         * @see WP_Customize_Manager::establish_loaded_changeset()
804         *
805         * @return string UUID.
806         */
807        public function changeset_uuid() {
808                if ( empty( $this->_changeset_uuid ) ) {
809                        $this->establish_loaded_changeset();
810                }
811                return $this->_changeset_uuid;
812        }
813
814        /**
815         * Get the theme being customized.
816         *
817         * @since 3.4.0
818         *
819         * @return WP_Theme
820         */
821        public function theme() {
822                if ( ! $this->theme ) {
823                        $this->theme = wp_get_theme();
824                }
825                return $this->theme;
826        }
827
828        /**
829         * Get the registered settings.
830         *
831         * @since 3.4.0
832         *
833         * @return array
834         */
835        public function settings() {
836                return $this->settings;
837        }
838
839        /**
840         * Get the registered controls.
841         *
842         * @since 3.4.0
843         *
844         * @return array
845         */
846        public function controls() {
847                return $this->controls;
848        }
849
850        /**
851         * Get the registered containers.
852         *
853         * @since 4.0.0
854         *
855         * @return array
856         */
857        public function containers() {
858                return $this->containers;
859        }
860
861        /**
862         * Get the registered sections.
863         *
864         * @since 3.4.0
865         *
866         * @return array
867         */
868        public function sections() {
869                return $this->sections;
870        }
871
872        /**
873         * Get the registered panels.
874         *
875         * @since 4.0.0
876         *
877         * @return array Panels.
878         */
879        public function panels() {
880                return $this->panels;
881        }
882
883        /**
884         * Checks if the current theme is active.
885         *
886         * @since 3.4.0
887         *
888         * @return bool
889         */
890        public function is_theme_active() {
891                return $this->get_stylesheet() == $this->original_stylesheet;
892        }
893
894        /**
895         * Register styles/scripts and initialize the preview of each setting
896         *
897         * @since 3.4.0
898         */
899        public function wp_loaded() {
900
901                // Unconditionally register core types for panels, sections, and controls in case plugin unhooks all customize_register actions.
902                $this->register_panel_type( 'WP_Customize_Panel' );
903                $this->register_panel_type( 'WP_Customize_Themes_Panel' );
904                $this->register_section_type( 'WP_Customize_Section' );
905                $this->register_section_type( 'WP_Customize_Sidebar_Section' );
906                $this->register_section_type( 'WP_Customize_Themes_Section' );
907                $this->register_control_type( 'WP_Customize_Color_Control' );
908                $this->register_control_type( 'WP_Customize_Media_Control' );
909                $this->register_control_type( 'WP_Customize_Upload_Control' );
910                $this->register_control_type( 'WP_Customize_Image_Control' );
911                $this->register_control_type( 'WP_Customize_Background_Image_Control' );
912                $this->register_control_type( 'WP_Customize_Background_Position_Control' );
913                $this->register_control_type( 'WP_Customize_Cropped_Image_Control' );
914                $this->register_control_type( 'WP_Customize_Site_Icon_Control' );
915                $this->register_control_type( 'WP_Customize_Theme_Control' );
916                $this->register_control_type( 'WP_Customize_Code_Editor_Control' );
917                $this->register_control_type( 'WP_Customize_Date_Time_Control' );
918
919                /**
920                 * Fires once WordPress has loaded, allowing scripts and styles to be initialized.
921                 *
922                 * @since 3.4.0
923                 *
924                 * @param WP_Customize_Manager $this WP_Customize_Manager instance.
925                 */
926                do_action( 'customize_register', $this );
927
928                if ( $this->settings_previewed() ) {
929                        foreach ( $this->settings as $setting ) {
930                                $setting->preview();
931                        }
932                }
933
934                if ( $this->is_preview() && ! is_admin() ) {
935                        $this->customize_preview_init();
936                }
937        }
938
939        /**
940         * Prevents Ajax requests from following redirects when previewing a theme
941         * by issuing a 200 response instead of a 30x.
942         *
943         * Instead, the JS will sniff out the location header.
944         *
945         * @since 3.4.0
946         * @deprecated 4.7.0
947         *
948         * @param int $status Status.
949         * @return int
950         */
951        public function wp_redirect_status( $status ) {
952                _deprecated_function( __FUNCTION__, '4.7.0' );
953
954                if ( $this->is_preview() && ! is_admin() ) {
955                        return 200;
956                }
957
958                return $status;
959        }
960
961        /**
962         * Find the changeset post ID for a given changeset UUID.
963         *
964         * @since 4.7.0
965         *
966         * @param string $uuid Changeset UUID.
967         * @return int|null Returns post ID on success and null on failure.
968         */
969        public function find_changeset_post_id( $uuid ) {
970                $cache_group = 'customize_changeset_post';
971                $changeset_post_id = wp_cache_get( $uuid, $cache_group );
972                if ( $changeset_post_id && 'customize_changeset' === get_post_type( $changeset_post_id ) ) {
973                        return $changeset_post_id;
974                }
975
976                $changeset_post_query = new WP_Query( array(
977                        'post_type' => 'customize_changeset',
978                        'post_status' => get_post_stati(),
979                        'name' => $uuid,
980                        'posts_per_page' => 1,
981                        'no_found_rows' => true,
982                        'cache_results' => true,
983                        'update_post_meta_cache' => false,
984                        'update_post_term_cache' => false,
985                        'lazy_load_term_meta' => false,
986                ) );
987                if ( ! empty( $changeset_post_query->posts ) ) {
988                        // Note: 'fields'=>'ids' is not being used in order to cache the post object as it will be needed.
989                        $changeset_post_id = $changeset_post_query->posts[0]->ID;
990                        wp_cache_set( $uuid, $changeset_post_id, $cache_group );
991                        return $changeset_post_id;
992                }
993
994                return null;
995        }
996
997        /**
998         * Get changeset posts.
999         *
1000         * @since 4.9.0
1001         *
1002         * @param array $args {
1003         *     Args to pass into `get_posts()` to query changesets.
1004         *
1005         *     @type int    $posts_per_page             Number of posts to return. Defaults to -1 (all posts).
1006         *     @type int    $author                     Post author. Defaults to current user.
1007         *     @type string $post_status                Status of changeset. Defaults to 'auto-draft'.
1008         *     @type bool   $exclude_restore_dismissed  Whether to exclude changeset auto-drafts that have been dismissed. Defaults to true.
1009         * }
1010         * @return WP_Post[] Auto-draft changesets.
1011         */
1012        protected function get_changeset_posts( $args = array() ) {
1013                $default_args = array(
1014                        'exclude_restore_dismissed' => true,
1015                        'posts_per_page' => -1,
1016                        'post_type' => 'customize_changeset',
1017                        'post_status' => 'auto-draft',
1018                        'order' => 'DESC',
1019                        'orderby' => 'date',
1020                        'no_found_rows' => true,
1021                        'cache_results' => true,
1022                        'update_post_meta_cache' => false,
1023                        'update_post_term_cache' => false,
1024                        'lazy_load_term_meta' => false,
1025                );
1026                if ( get_current_user_id() ) {
1027                        $default_args['author'] = get_current_user_id();
1028                }
1029                $args = array_merge( $default_args, $args );
1030
1031                if ( ! empty( $args['exclude_restore_dismissed'] ) ) {
1032                        unset( $args['exclude_restore_dismissed'] );
1033                        $args['meta_query'] = array(
1034                                array(
1035                                        'key' => '_customize_restore_dismissed',
1036                                        'compare' => 'NOT EXISTS',
1037                                ),
1038                        );
1039                }
1040
1041                return get_posts( $args );
1042        }
1043
1044        /**
1045         * Dismiss all of the current user's auto-drafts (other than the present one).
1046         *
1047         * @since 4.9.0
1048         * @return int The number of auto-drafts that were dismissed.
1049         */
1050        protected function dismiss_user_auto_draft_changesets() {
1051                $changeset_autodraft_posts = $this->get_changeset_posts( array(
1052                        'post_status' => 'auto-draft',
1053                        'exclude_restore_dismissed' => true,
1054                        'posts_per_page' => -1,
1055                ) );
1056                $dismissed = 0;
1057                foreach ( $changeset_autodraft_posts as $autosave_autodraft_post ) {
1058                        if ( $autosave_autodraft_post->ID === $this->changeset_post_id() ) {
1059                                continue;
1060                        }
1061                        if ( update_post_meta( $autosave_autodraft_post->ID, '_customize_restore_dismissed', true ) ) {
1062                                $dismissed++;
1063                        }
1064                }
1065                return $dismissed;
1066        }
1067
1068        /**
1069         * Get the changeset post id for the loaded changeset.
1070         *
1071         * @since 4.7.0
1072         *
1073         * @return int|null Post ID on success or null if there is no post yet saved.
1074         */
1075        public function changeset_post_id() {
1076                if ( ! isset( $this->_changeset_post_id ) ) {
1077                        $post_id = $this->find_changeset_post_id( $this->changeset_uuid() );
1078                        if ( ! $post_id ) {
1079                                $post_id = false;
1080                        }
1081                        $this->_changeset_post_id = $post_id;
1082                }
1083                if ( false === $this->_changeset_post_id ) {
1084                        return null;
1085                }
1086                return $this->_changeset_post_id;
1087        }
1088
1089        /**
1090         * Get the data stored in a changeset post.
1091         *
1092         * @since 4.7.0
1093         *
1094         * @param int $post_id Changeset post ID.
1095         * @return array|WP_Error Changeset data or WP_Error on error.
1096         */
1097        protected function get_changeset_post_data( $post_id ) {
1098                if ( ! $post_id ) {
1099                        return new WP_Error( 'empty_post_id' );
1100                }
1101                $changeset_post = get_post( $post_id );
1102                if ( ! $changeset_post ) {
1103                        return new WP_Error( 'missing_post' );
1104                }
1105                if ( 'revision' === $changeset_post->post_type ) {
1106                        if ( 'customize_changeset' !== get_post_type( $changeset_post->post_parent ) ) {
1107                                return new WP_Error( 'wrong_post_type' );
1108                        }
1109                } elseif ( 'customize_changeset' !== $changeset_post->post_type ) {
1110                        return new WP_Error( 'wrong_post_type' );
1111                }
1112                $changeset_data = json_decode( $changeset_post->post_content, true );
1113                if ( function_exists( 'json_last_error' ) && json_last_error() ) {
1114                        return new WP_Error( 'json_parse_error', '', json_last_error() );
1115                }
1116                if ( ! is_array( $changeset_data ) ) {
1117                        return new WP_Error( 'expected_array' );
1118                }
1119                return $changeset_data;
1120        }
1121
1122        /**
1123         * Get changeset data.
1124         *
1125         * @since 4.7.0
1126         * @since 4.9.0 This will return the changeset's data with a user's autosave revision merged on top, if one exists and $autosaved is true.
1127         *
1128         * @return array Changeset data.
1129         */
1130        public function changeset_data() {
1131                if ( isset( $this->_changeset_data ) ) {
1132                        return $this->_changeset_data;
1133                }
1134                $changeset_post_id = $this->changeset_post_id();
1135                if ( ! $changeset_post_id ) {
1136                        $this->_changeset_data = array();
1137                } else {
1138                        if ( $this->autosaved() && is_user_logged_in() ) {
1139                                $autosave_post = wp_get_post_autosave( $changeset_post_id, get_current_user_id() );
1140                                if ( $autosave_post ) {
1141                                        $data = $this->get_changeset_post_data( $autosave_post->ID );
1142                                        if ( ! is_wp_error( $data ) ) {
1143                                                $this->_changeset_data = $data;
1144                                        }
1145                                }
1146                        }
1147
1148                        // Load data from the changeset if it was not loaded from an autosave.
1149                        if ( ! isset( $this->_changeset_data ) ) {
1150                                $data = $this->get_changeset_post_data( $changeset_post_id );
1151                                if ( ! is_wp_error( $data ) ) {
1152                                        $this->_changeset_data = $data;
1153                                } else {
1154                                        $this->_changeset_data = array();
1155                                }
1156                        }
1157                }
1158                return $this->_changeset_data;
1159        }
1160
1161        /**
1162         * Starter content setting IDs.
1163         *
1164         * @since 4.7.0
1165         * @var array
1166         */
1167        protected $pending_starter_content_settings_ids = array();
1168
1169        /**
1170         * Import theme starter content into the customized state.
1171         *
1172         * @since 4.7.0
1173         *
1174         * @param array $starter_content Starter content. Defaults to `get_theme_starter_content()`.
1175         */
1176        function import_theme_starter_content( $starter_content = array() ) {
1177                if ( empty( $starter_content ) ) {
1178                        $starter_content = get_theme_starter_content();
1179                }
1180
1181                $changeset_data = array();
1182                if ( $this->changeset_post_id() ) {
1183                        /*
1184                         * Don't re-import starter content into a changeset saved persistently.
1185                         * This will need to be revisited in the future once theme switching
1186                         * is allowed with drafted/scheduled changesets, since switching to
1187                         * another theme could result in more starter content being applied.
1188                         * However, when doing an explicit save it is currently possible for
1189                         * nav menus and nav menu items specifically to lose their starter_content
1190                         * flags, thus resulting in duplicates being created since they fail
1191                         * to get re-used. See #40146.
1192                         */
1193                        if ( 'auto-draft' !== get_post_status( $this->changeset_post_id() ) ) {
1194                                return;
1195                        }
1196
1197                        $changeset_data = $this->get_changeset_post_data( $this->changeset_post_id() );
1198                }
1199
1200                $sidebars_widgets = isset( $starter_content['widgets'] ) && ! empty( $this->widgets ) ? $starter_content['widgets'] : array();
1201                $attachments = isset( $starter_content['attachments'] ) && ! empty( $this->nav_menus ) ? $starter_content['attachments'] : array();
1202                $posts = isset( $starter_content['posts'] ) && ! empty( $this->nav_menus ) ? $starter_content['posts'] : array();
1203                $options = isset( $starter_content['options'] ) ? $starter_content['options'] : array();
1204                $nav_menus = isset( $starter_content['nav_menus'] ) && ! empty( $this->nav_menus ) ? $starter_content['nav_menus'] : array();
1205                $theme_mods = isset( $starter_content['theme_mods'] ) ? $starter_content['theme_mods'] : array();
1206
1207                // Widgets.
1208                $max_widget_numbers = array();
1209                foreach ( $sidebars_widgets as $sidebar_id => $widgets ) {
1210                        $sidebar_widget_ids = array();
1211                        foreach ( $widgets as $widget ) {
1212                                list( $id_base, $instance ) = $widget;
1213
1214                                if ( ! isset( $max_widget_numbers[ $id_base ] ) ) {
1215
1216                                        // When $settings is an array-like object, get an intrinsic array for use with array_keys().
1217                                        $settings = get_option( "widget_{$id_base}", array() );
1218                                        if ( $settings instanceof ArrayObject || $settings instanceof ArrayIterator ) {
1219                                                $settings = $settings->getArrayCopy();
1220                                        }
1221
1222                                        // Find the max widget number for this type.
1223                                        $widget_numbers = array_keys( $settings );
1224                                        if ( count( $widget_numbers ) > 0 ) {
1225                                                $widget_numbers[] = 1;
1226                                                $max_widget_numbers[ $id_base ] = call_user_func_array( 'max', $widget_numbers );
1227                                        } else {
1228                                                $max_widget_numbers[ $id_base ] = 1;
1229                                        }
1230                                }
1231                                $max_widget_numbers[ $id_base ] += 1;
1232
1233                                $widget_id = sprintf( '%s-%d', $id_base, $max_widget_numbers[ $id_base ] );
1234                                $setting_id = sprintf( 'widget_%s[%d]', $id_base, $max_widget_numbers[ $id_base ] );
1235
1236                                $setting_value = $this->widgets->sanitize_widget_js_instance( $instance );
1237                                if ( empty( $changeset_data[ $setting_id ] ) || ! empty( $changeset_data[ $setting_id ]['starter_content'] ) ) {
1238                                        $this->set_post_value( $setting_id, $setting_value );
1239                                        $this->pending_starter_content_settings_ids[] = $setting_id;
1240                                }
1241                                $sidebar_widget_ids[] = $widget_id;
1242                        }
1243
1244                        $setting_id = sprintf( 'sidebars_widgets[%s]', $sidebar_id );
1245                        if ( empty( $changeset_data[ $setting_id ] ) || ! empty( $changeset_data[ $setting_id ]['starter_content'] ) ) {
1246                                $this->set_post_value( $setting_id, $sidebar_widget_ids );
1247                                $this->pending_starter_content_settings_ids[] = $setting_id;
1248                        }
1249                }
1250
1251                $starter_content_auto_draft_post_ids = array();
1252                if ( ! empty( $changeset_data['nav_menus_created_posts']['value'] ) ) {
1253                        $starter_content_auto_draft_post_ids = array_merge( $starter_content_auto_draft_post_ids, $changeset_data['nav_menus_created_posts']['value'] );
1254                }
1255
1256                // Make an index of all the posts needed and what their slugs are.
1257                $needed_posts = array();
1258                $attachments = $this->prepare_starter_content_attachments( $attachments );
1259                foreach ( $attachments as $attachment ) {
1260                        $key = 'attachment:' . $attachment['post_name'];
1261                        $needed_posts[ $key ] = true;
1262                }
1263                foreach ( array_keys( $posts ) as $post_symbol ) {
1264                        if ( empty( $posts[ $post_symbol ]['post_name'] ) && empty( $posts[ $post_symbol ]['post_title'] ) ) {
1265                                unset( $posts[ $post_symbol ] );
1266                                continue;
1267                        }
1268                        if ( empty( $posts[ $post_symbol ]['post_name'] ) ) {
1269                                $posts[ $post_symbol ]['post_name'] = sanitize_title( $posts[ $post_symbol ]['post_title'] );
1270                        }
1271                        if ( empty( $posts[ $post_symbol ]['post_type'] ) ) {
1272                                $posts[ $post_symbol ]['post_type'] = 'post';
1273                        }
1274                        $needed_posts[ $posts[ $post_symbol ]['post_type'] . ':' . $posts[ $post_symbol ]['post_name'] ] = true;
1275                }
1276                $all_post_slugs = array_merge(
1277                        wp_list_pluck( $attachments, 'post_name' ),
1278                        wp_list_pluck( $posts, 'post_name' )
1279                );
1280
1281                /*
1282                 * Obtain all post types referenced in starter content to use in query.
1283                 * This is needed because 'any' will not account for post types not yet registered.
1284                 */
1285                $post_types = array_filter( array_merge( array( 'attachment' ), wp_list_pluck( $posts, 'post_type' ) ) );
1286
1287                // Re-use auto-draft starter content posts referenced in the current customized state.
1288                $existing_starter_content_posts = array();
1289                if ( ! empty( $starter_content_auto_draft_post_ids ) ) {
1290                        $existing_posts_query = new WP_Query( array(
1291                                'post__in' => $starter_content_auto_draft_post_ids,
1292                                'post_status' => 'auto-draft',
1293                                'post_type' => $post_types,
1294                                'posts_per_page' => -1,
1295                        ) );
1296                        foreach ( $existing_posts_query->posts as $existing_post ) {
1297                                $post_name = $existing_post->post_name;
1298                                if ( empty( $post_name ) ) {
1299                                        $post_name = get_post_meta( $existing_post->ID, '_customize_draft_post_name', true );
1300                                }
1301                                $existing_starter_content_posts[ $existing_post->post_type . ':' . $post_name ] = $existing_post;
1302                        }
1303                }
1304
1305                // Re-use non-auto-draft posts.
1306                if ( ! empty( $all_post_slugs ) ) {
1307                        $existing_posts_query = new WP_Query( array(
1308                                'post_name__in' => $all_post_slugs,
1309                                'post_status' => array_diff( get_post_stati(), array( 'auto-draft' ) ),
1310                                'post_type' => 'any',
1311                                'posts_per_page' => -1,
1312                        ) );
1313                        foreach ( $existing_posts_query->posts as $existing_post ) {
1314                                $key = $existing_post->post_type . ':' . $existing_post->post_name;
1315                                if ( isset( $needed_posts[ $key ] ) && ! isset( $existing_starter_content_posts[ $key ] ) ) {
1316                                        $existing_starter_content_posts[ $key ] = $existing_post;
1317                                }
1318                        }
1319                }
1320
1321                // Attachments are technically posts but handled differently.
1322                if ( ! empty( $attachments ) ) {
1323
1324                        $attachment_ids = array();
1325
1326                        foreach ( $attachments as $symbol => $attachment ) {
1327                                $file_array = array(
1328                                        'name' => $attachment['file_name'],
1329                                );
1330                                $file_path = $attachment['file_path'];
1331                                $attachment_id = null;
1332                                $attached_file = null;
1333                                if ( isset( $existing_starter_content_posts[ 'attachment:' . $attachment['post_name'] ] ) ) {
1334                                        $attachment_post = $existing_starter_content_posts[ 'attachment:' . $attachment['post_name'] ];
1335                                        $attachment_id = $attachment_post->ID;
1336                                        $attached_file = get_attached_file( $attachment_id );
1337                                        if ( empty( $attached_file ) || ! file_exists( $attached_file ) ) {
1338                                                $attachment_id = null;
1339                                                $attached_file = null;
1340                                        } elseif ( $this->get_stylesheet() !== get_post_meta( $attachment_post->ID, '_starter_content_theme', true ) ) {
1341
1342                                                // Re-generate attachment metadata since it was previously generated for a different theme.
1343                                                $metadata = wp_generate_attachment_metadata( $attachment_post->ID, $attached_file );
1344                                                wp_update_attachment_metadata( $attachment_id, $metadata );
1345                                                update_post_meta( $attachment_id, '_starter_content_theme', $this->get_stylesheet() );
1346                                        }
1347                                }
1348
1349                                // Insert the attachment auto-draft because it doesn't yet exist or the attached file is gone.
1350                                if ( ! $attachment_id ) {
1351
1352                                        // Copy file to temp location so that original file won't get deleted from theme after sideloading.
1353                                        $temp_file_name = wp_tempnam( basename( $file_path ) );
1354                                        if ( $temp_file_name && copy( $file_path, $temp_file_name ) ) {
1355                                                $file_array['tmp_name'] = $temp_file_name;
1356                                        }
1357                                        if ( empty( $file_array['tmp_name'] ) ) {
1358                                                continue;
1359                                        }
1360
1361                                        $attachment_post_data = array_merge(
1362                                                wp_array_slice_assoc( $attachment, array( 'post_title', 'post_content', 'post_excerpt' ) ),
1363                                                array(
1364                                                        'post_status' => 'auto-draft', // So attachment will be garbage collected in a week if changeset is never published.
1365                                                )
1366                                        );
1367
1368                                        // In PHP < 5.6 filesize() returns 0 for the temp files unless we clear the file status cache.
1369                                        // Technically, PHP < 5.6.0 || < 5.5.13 || < 5.4.29 but no need to be so targeted.
1370                                        // See https://bugs.php.net/bug.php?id=65701
1371                                        if ( version_compare( PHP_VERSION, '5.6', '<' ) ) {
1372                                                clearstatcache();
1373                                        }
1374
1375                                        $attachment_id = media_handle_sideload( $file_array, 0, null, $attachment_post_data );
1376                                        if ( is_wp_error( $attachment_id ) ) {
1377                                                continue;
1378                                        }
1379                                        update_post_meta( $attachment_id, '_starter_content_theme', $this->get_stylesheet() );
1380                                        update_post_meta( $attachment_id, '_customize_draft_post_name', $attachment['post_name'] );
1381                                }
1382
1383                                $attachment_ids[ $symbol ] = $attachment_id;
1384                        }
1385                        $starter_content_auto_draft_post_ids = array_merge( $starter_content_auto_draft_post_ids, array_values( $attachment_ids ) );
1386                }
1387
1388                // Posts & pages.
1389                if ( ! empty( $posts ) ) {
1390                        foreach ( array_keys( $posts ) as $post_symbol ) {
1391                                if ( empty( $posts[ $post_symbol ]['post_type'] ) || empty( $posts[ $post_symbol ]['post_name'] ) ) {
1392                                        continue;
1393                                }
1394                                $post_type = $posts[ $post_symbol ]['post_type'];
1395                                if ( ! empty( $posts[ $post_symbol ]['post_name'] ) ) {
1396                                        $post_name = $posts[ $post_symbol ]['post_name'];
1397                                } elseif ( ! empty( $posts[ $post_symbol ]['post_title'] ) ) {
1398                                        $post_name = sanitize_title( $posts[ $post_symbol ]['post_title'] );
1399                                } else {
1400                                        continue;
1401                                }
1402
1403                                // Use existing auto-draft post if one already exists with the same type and name.
1404                                if ( isset( $existing_starter_content_posts[ $post_type . ':' . $post_name ] ) ) {
1405                                        $posts[ $post_symbol ]['ID'] = $existing_starter_content_posts[ $post_type . ':' . $post_name ]->ID;
1406                                        continue;
1407                                }
1408
1409                                // Translate the featured image symbol.
1410                                if ( ! empty( $posts[ $post_symbol ]['thumbnail'] )
1411                                        && preg_match( '/^{{(?P<symbol>.+)}}$/', $posts[ $post_symbol ]['thumbnail'], $matches )
1412                                        && isset( $attachment_ids[ $matches['symbol'] ] ) ) {
1413                                        $posts[ $post_symbol ]['meta_input']['_thumbnail_id'] = $attachment_ids[ $matches['symbol'] ];
1414                                }
1415
1416                                if ( ! empty( $posts[ $post_symbol ]['template'] ) ) {
1417                                        $posts[ $post_symbol ]['meta_input']['_wp_page_template'] = $posts[ $post_symbol ]['template'];
1418                                }
1419
1420                                $r = $this->nav_menus->insert_auto_draft_post( $posts[ $post_symbol ] );
1421                                if ( $r instanceof WP_Post ) {
1422                                        $posts[ $post_symbol ]['ID'] = $r->ID;
1423                                }
1424                        }
1425
1426                        $starter_content_auto_draft_post_ids = array_merge( $starter_content_auto_draft_post_ids, wp_list_pluck( $posts, 'ID' ) );
1427                }
1428
1429                // The nav_menus_created_posts setting is why nav_menus component is dependency for adding posts.
1430                if ( ! empty( $this->nav_menus ) && ! empty( $starter_content_auto_draft_post_ids ) ) {
1431                        $setting_id = 'nav_menus_created_posts';
1432                        $this->set_post_value( $setting_id, array_unique( array_values( $starter_content_auto_draft_post_ids ) ) );
1433                        $this->pending_starter_content_settings_ids[] = $setting_id;
1434                }
1435
1436                // Nav menus.
1437                $placeholder_id = -1;
1438                $reused_nav_menu_setting_ids = array();
1439                foreach ( $nav_menus as $nav_menu_location => $nav_menu ) {
1440
1441                        $nav_menu_term_id = null;
1442                        $nav_menu_setting_id = null;
1443                        $matches = array();
1444
1445                        // Look for an existing placeholder menu with starter content to re-use.
1446                        foreach ( $changeset_data as $setting_id => $setting_params ) {
1447                                $can_reuse = (
1448                                        ! empty( $setting_params['starter_content'] )
1449                                        &&
1450                                        ! in_array( $setting_id, $reused_nav_menu_setting_ids, true )
1451                                        &&
1452                                        preg_match( '#^nav_menu\[(?P<nav_menu_id>-?\d+)\]$#', $setting_id, $matches )
1453                                );
1454                                if ( $can_reuse ) {
1455                                        $nav_menu_term_id = intval( $matches['nav_menu_id'] );
1456                                        $nav_menu_setting_id = $setting_id;
1457                                        $reused_nav_menu_setting_ids[] = $setting_id;
1458                                        break;
1459                                }
1460                        }
1461
1462                        if ( ! $nav_menu_term_id ) {
1463                                while ( isset( $changeset_data[ sprintf( 'nav_menu[%d]', $placeholder_id ) ] ) ) {
1464                                        $placeholder_id--;
1465                                }
1466                                $nav_menu_term_id = $placeholder_id;
1467                                $nav_menu_setting_id = sprintf( 'nav_menu[%d]', $placeholder_id );
1468                        }
1469
1470                        $this->set_post_value( $nav_menu_setting_id, array(
1471                                'name' => isset( $nav_menu['name'] ) ? $nav_menu['name'] : $nav_menu_location,
1472                        ) );
1473                        $this->pending_starter_content_settings_ids[] = $nav_menu_setting_id;
1474
1475                        // @todo Add support for menu_item_parent.
1476                        $position = 0;
1477                        foreach ( $nav_menu['items'] as $nav_menu_item ) {
1478                                $nav_menu_item_setting_id = sprintf( 'nav_menu_item[%d]', $placeholder_id-- );
1479                                if ( ! isset( $nav_menu_item['position'] ) ) {
1480                                        $nav_menu_item['position'] = $position++;
1481                                }
1482                                $nav_menu_item['nav_menu_term_id'] = $nav_menu_term_id;
1483
1484                                if ( isset( $nav_menu_item['object_id'] ) ) {
1485                                        if ( 'post_type' === $nav_menu_item['type'] && preg_match( '/^{{(?P<symbol>.+)}}$/', $nav_menu_item['object_id'], $matches ) && isset( $posts[ $matches['symbol'] ] ) ) {
1486                                                $nav_menu_item['object_id'] = $posts[ $matches['symbol'] ]['ID'];
1487                                                if ( empty( $nav_menu_item['title'] ) ) {
1488                                                        $original_object = get_post( $nav_menu_item['object_id'] );
1489                                                        $nav_menu_item['title'] = $original_object->post_title;
1490                                                }
1491                                        } else {
1492                                                continue;
1493                                        }
1494                                } else {
1495                                        $nav_menu_item['object_id'] = 0;
1496                                }
1497
1498                                if ( empty( $changeset_data[ $nav_menu_item_setting_id ] ) || ! empty( $changeset_data[ $nav_menu_item_setting_id ]['starter_content'] ) ) {
1499                                        $this->set_post_value( $nav_menu_item_setting_id, $nav_menu_item );
1500                                        $this->pending_starter_content_settings_ids[] = $nav_menu_item_setting_id;
1501                                }
1502                        }
1503
1504                        $setting_id = sprintf( 'nav_menu_locations[%s]', $nav_menu_location );
1505                        if ( empty( $changeset_data[ $setting_id ] ) || ! empty( $changeset_data[ $setting_id ]['starter_content'] ) ) {
1506                                $this->set_post_value( $setting_id, $nav_menu_term_id );
1507                                $this->pending_starter_content_settings_ids[] = $setting_id;
1508                        }
1509                }
1510
1511                // Options.
1512                foreach ( $options as $name => $value ) {
1513                        if ( preg_match( '/^{{(?P<symbol>.+)}}$/', $value, $matches ) ) {
1514                                if ( isset( $posts[ $matches['symbol'] ] ) ) {
1515                                        $value = $posts[ $matches['symbol'] ]['ID'];
1516                                } elseif ( isset( $attachment_ids[ $matches['symbol'] ] ) ) {
1517                                        $value = $attachment_ids[ $matches['symbol'] ];
1518                                } else {
1519                                        continue;
1520                                }
1521                        }
1522
1523                        if ( empty( $changeset_data[ $name ] ) || ! empty( $changeset_data[ $name ]['starter_content'] ) ) {
1524                                $this->set_post_value( $name, $value );
1525                                $this->pending_starter_content_settings_ids[] = $name;
1526                        }
1527                }
1528
1529                // Theme mods.
1530                foreach ( $theme_mods as $name => $value ) {
1531                        if ( preg_match( '/^{{(?P<symbol>.+)}}$/', $value, $matches ) ) {
1532                                if ( isset( $posts[ $matches['symbol'] ] ) ) {
1533                                        $value = $posts[ $matches['symbol'] ]['ID'];
1534                                } elseif ( isset( $attachment_ids[ $matches['symbol'] ] ) ) {
1535                                        $value = $attachment_ids[ $matches['symbol'] ];
1536                                } else {
1537                                        continue;
1538                                }
1539                        }
1540
1541                        // Handle header image as special case since setting has a legacy format.
1542                        if ( 'header_image' === $name ) {
1543                                $name = 'header_image_data';
1544                                $metadata = wp_get_attachment_metadata( $value );
1545                                if ( empty( $metadata ) ) {
1546                                        continue;
1547                                }
1548                                $value = array(
1549                                        'attachment_id' => $value,
1550                                        'url' => wp_get_attachment_url( $value ),
1551                                        'height' => $metadata['height'],
1552                                        'width' => $metadata['width'],
1553                                );
1554                        } elseif ( 'background_image' === $name ) {
1555                                $value = wp_get_attachment_url( $value );
1556                        }
1557
1558                        if ( empty( $changeset_data[ $name ] ) || ! empty( $changeset_data[ $name ]['starter_content'] ) ) {
1559                                $this->set_post_value( $name, $value );
1560                                $this->pending_starter_content_settings_ids[] = $name;
1561                        }
1562                }
1563
1564                if ( ! empty( $this->pending_starter_content_settings_ids ) ) {
1565                        if ( did_action( 'customize_register' ) ) {
1566                                $this->_save_starter_content_changeset();
1567                        } else {
1568                                add_action( 'customize_register', array( $this, '_save_starter_content_changeset' ), 1000 );
1569                        }
1570                }
1571        }
1572
1573        /**
1574         * Prepare starter content attachments.
1575         *
1576         * Ensure that the attachments are valid and that they have slugs and file name/path.
1577         *
1578         * @since 4.7.0
1579         *
1580         * @param array $attachments Attachments.
1581         * @return array Prepared attachments.
1582         */
1583        protected function prepare_starter_content_attachments( $attachments ) {
1584                $prepared_attachments = array();
1585                if ( empty( $attachments ) ) {
1586                        return $prepared_attachments;
1587                }
1588
1589                // Such is The WordPress Way.
1590                require_once( ABSPATH . 'wp-admin/includes/file.php' );
1591                require_once( ABSPATH . 'wp-admin/includes/media.php' );
1592                require_once( ABSPATH . 'wp-admin/includes/image.php' );
1593
1594                foreach ( $attachments as $symbol => $attachment ) {
1595
1596                        // A file is required and URLs to files are not currently allowed.
1597                        if ( empty( $attachment['file'] ) || preg_match( '#^https?://$#', $attachment['file'] ) ) {
1598                                continue;
1599                        }
1600
1601                        $file_path = null;
1602                        if ( file_exists( $attachment['file'] ) ) {
1603                                $file_path = $attachment['file']; // Could be absolute path to file in plugin.
1604                        } elseif ( is_child_theme() && file_exists( get_stylesheet_directory() . '/' . $attachment['file'] ) ) {
1605                                $file_path = get_stylesheet_directory() . '/' . $attachment['file'];
1606                        } elseif ( file_exists( get_template_directory() . '/' . $attachment['file'] ) ) {
1607                                $file_path = get_template_directory() . '/' . $attachment['file'];
1608                        } else {
1609                                continue;
1610                        }
1611                        $file_name = basename( $attachment['file'] );
1612
1613                        // Skip file types that are not recognized.
1614                        $checked_filetype = wp_check_filetype( $file_name );
1615                        if ( empty( $checked_filetype['type'] ) ) {
1616                                continue;
1617                        }
1618
1619                        // Ensure post_name is set since not automatically derived from post_title for new auto-draft posts.
1620                        if ( empty( $attachment['post_name'] ) ) {
1621                                if ( ! empty( $attachment['post_title'] ) ) {
1622                                        $attachment['post_name'] = sanitize_title( $attachment['post_title'] );
1623                                } else {
1624                                        $attachment['post_name'] = sanitize_title( preg_replace( '/\.\w+$/', '', $file_name ) );
1625                                }
1626                        }
1627
1628                        $attachment['file_name'] = $file_name;
1629                        $attachment['file_path'] = $file_path;
1630                        $prepared_attachments[ $symbol ] = $attachment;
1631                }
1632                return $prepared_attachments;
1633        }
1634
1635        /**
1636         * Save starter content changeset.
1637         *
1638         * @since 4.7.0
1639         */
1640        public function _save_starter_content_changeset() {
1641
1642                if ( empty( $this->pending_starter_content_settings_ids ) ) {
1643                        return;
1644                }
1645
1646                $this->save_changeset_post( array(
1647                        'data' => array_fill_keys( $this->pending_starter_content_settings_ids, array( 'starter_content' => true ) ),
1648                        'starter_content' => true,
1649                ) );
1650                $this->saved_starter_content_changeset = true;
1651
1652                $this->pending_starter_content_settings_ids = array();
1653        }
1654
1655        /**
1656         * Get dirty pre-sanitized setting values in the current customized state.
1657         *
1658         * The returned array consists of a merge of three sources:
1659         * 1. If the theme is not currently active, then the base array is any stashed
1660         *    theme mods that were modified previously but never published.
1661         * 2. The values from the current changeset, if it exists.
1662         * 3. If the user can customize, the values parsed from the incoming
1663         *    `$_POST['customized']` JSON data.
1664         * 4. Any programmatically-set post values via `WP_Customize_Manager::set_post_value()`.
1665         *
1666         * The name "unsanitized_post_values" is a carry-over from when the customized
1667         * state was exclusively sourced from `$_POST['customized']`. Nevertheless,
1668         * the value returned will come from the current changeset post and from the
1669         * incoming post data.
1670         *
1671         * @since 4.1.1
1672         * @since 4.7.0 Added $args param and merging with changeset values and stashed theme mods.
1673         *
1674         * @param array $args {
1675         *     Args.
1676         *
1677         *     @type bool $exclude_changeset Whether the changeset values should also be excluded. Defaults to false.
1678         *     @type bool $exclude_post_data Whether the post input values should also be excluded. Defaults to false when lacking the customize capability.
1679         * }
1680         * @return array
1681         */
1682        public function unsanitized_post_values( $args = array() ) {
1683                $args = array_merge(
1684                        array(
1685                                'exclude_changeset' => false,
1686                                'exclude_post_data' => ! current_user_can( 'customize' ),
1687                        ),
1688                        $args
1689                );
1690
1691                $values = array();
1692
1693                // Let default values be from the stashed theme mods if doing a theme switch and if no changeset is present.
1694                if ( ! $this->is_theme_active() ) {
1695                        $stashed_theme_mods = get_option( 'customize_stashed_theme_mods' );
1696                        $stylesheet = $this->get_stylesheet();
1697                        if ( isset( $stashed_theme_mods[ $stylesheet ] ) ) {
1698                                $values = array_merge( $values, wp_list_pluck( $stashed_theme_mods[ $stylesheet ], 'value' ) );
1699                        }
1700                }
1701
1702                if ( ! $args['exclude_changeset'] ) {
1703                        foreach ( $this->changeset_data() as $setting_id => $setting_params ) {
1704                                if ( ! array_key_exists( 'value', $setting_params ) ) {
1705                                        continue;
1706                                }
1707                                if ( isset( $setting_params['type'] ) && 'theme_mod' === $setting_params['type'] ) {
1708
1709                                        // Ensure that theme mods values are only used if they were saved under the current theme.
1710                                        $namespace_pattern = '/^(?P<stylesheet>.+?)::(?P<setting_id>.+)$/';
1711                                        if ( preg_match( $namespace_pattern, $setting_id, $matches ) && $this->get_stylesheet() === $matches['stylesheet'] ) {
1712                                                $values[ $matches['setting_id'] ] = $setting_params['value'];
1713                                        }
1714                                } else {
1715                                        $values[ $setting_id ] = $setting_params['value'];
1716                                }
1717                        }
1718                }
1719
1720                if ( ! $args['exclude_post_data'] ) {
1721                        if ( ! isset( $this->_post_values ) ) {
1722                                if ( isset( $_POST['customized'] ) ) {
1723                                        $post_values = json_decode( wp_unslash( $_POST['customized'] ), true );
1724                                } else {
1725                                        $post_values = array();
1726                                }
1727                                if ( is_array( $post_values ) ) {
1728                                        $this->_post_values = $post_values;
1729                                } else {
1730                                        $this->_post_values = array();
1731                                }
1732                        }
1733                        $values = array_merge( $values, $this->_post_values );
1734                }
1735                return $values;
1736        }
1737
1738        /**
1739         * Returns the sanitized value for a given setting from the current customized state.
1740         *
1741         * The name "post_value" is a carry-over from when the customized state was exclusively
1742         * sourced from `$_POST['customized']`. Nevertheless, the value returned will come
1743         * from the current changeset post and from the incoming post data.
1744         *
1745         * @since 3.4.0
1746         * @since 4.1.1 Introduced the `$default` parameter.
1747         * @since 4.6.0 `$default` is now returned early when the setting post value is invalid.
1748         *
1749         * @see WP_REST_Server::dispatch()
1750         * @see WP_REST_Request::sanitize_params()
1751         * @see WP_REST_Request::has_valid_params()
1752         *
1753         * @param WP_Customize_Setting $setting A WP_Customize_Setting derived object.
1754         * @param mixed                $default Value returned $setting has no post value (added in 4.2.0)
1755         *                                      or the post value is invalid (added in 4.6.0).
1756         * @return string|mixed $post_value Sanitized value or the $default provided.
1757         */
1758        public function post_value( $setting, $default = null ) {
1759                $post_values = $this->unsanitized_post_values();
1760                if ( ! array_key_exists( $setting->id, $post_values ) ) {
1761                        return $default;
1762                }
1763                $value = $post_values[ $setting->id ];
1764                $valid = $setting->validate( $value );
1765                if ( is_wp_error( $valid ) ) {
1766                        return $default;
1767                }
1768                $value = $setting->sanitize( $value );
1769                if ( is_null( $value ) || is_wp_error( $value ) ) {
1770                        return $default;
1771                }
1772                return $value;
1773        }
1774
1775        /**
1776         * Override a setting's value in the current customized state.
1777         *
1778         * The name "post_value" is a carry-over from when the customized state was
1779         * exclusively sourced from `$_POST['customized']`.
1780         *
1781         * @since 4.2.0
1782         *
1783         * @param string $setting_id ID for the WP_Customize_Setting instance.
1784         * @param mixed  $value      Post value.
1785         */
1786        public function set_post_value( $setting_id, $value ) {
1787                $this->unsanitized_post_values(); // Populate _post_values from $_POST['customized'].
1788                $this->_post_values[ $setting_id ] = $value;
1789
1790                /**
1791                 * Announce when a specific setting's unsanitized post value has been set.
1792                 *
1793                 * Fires when the WP_Customize_Manager::set_post_value() method is called.
1794                 *
1795                 * The dynamic portion of the hook name, `$setting_id`, refers to the setting ID.
1796                 *
1797                 * @since 4.4.0
1798                 *
1799                 * @param mixed                $value Unsanitized setting post value.
1800                 * @param WP_Customize_Manager $this  WP_Customize_Manager instance.
1801                 */
1802                do_action( "customize_post_value_set_{$setting_id}", $value, $this );
1803
1804                /**
1805                 * Announce when any setting's unsanitized post value has been set.
1806                 *
1807                 * Fires when the WP_Customize_Manager::set_post_value() method is called.
1808                 *
1809                 * This is useful for `WP_Customize_Setting` instances to watch
1810                 * in order to update a cached previewed value.
1811                 *
1812                 * @since 4.4.0
1813                 *
1814                 * @param string               $setting_id Setting ID.
1815                 * @param mixed                $value      Unsanitized setting post value.
1816                 * @param WP_Customize_Manager $this       WP_Customize_Manager instance.
1817                 */
1818                do_action( 'customize_post_value_set', $setting_id, $value, $this );
1819        }
1820
1821        /**
1822         * Print JavaScript settings.
1823         *
1824         * @since 3.4.0
1825         */
1826        public function customize_preview_init() {
1827
1828                /*
1829                 * Now that Customizer previews are loaded into iframes via GET requests
1830                 * and natural URLs with transaction UUIDs added, we need to ensure that
1831                 * the responses are never cached by proxies. In practice, this will not
1832                 * be needed if the user is logged-in anyway. But if anonymous access is
1833                 * allowed then the auth cookies would not be sent and WordPress would
1834                 * not send no-cache headers by default.
1835                 */
1836                if ( ! headers_sent() ) {
1837                        nocache_headers();
1838                        header( 'X-Robots: noindex, nofollow, noarchive' );
1839                }
1840                add_action( 'wp_head', 'wp_no_robots' );
1841                add_filter( 'wp_headers', array( $this, 'filter_iframe_security_headers' ) );
1842
1843                /*
1844                 * If preview is being served inside the customizer preview iframe, and
1845                 * if the user doesn't have customize capability, then it is assumed
1846                 * that the user's session has expired and they need to re-authenticate.
1847                 */
1848                if ( $this->messenger_channel && ! current_user_can( 'customize' ) ) {
1849                        $this->wp_die( -1, __( 'Unauthorized. You may remove the customize_messenger_channel param to preview as frontend.' ) );
1850                        return;
1851                }
1852
1853                $this->prepare_controls();
1854
1855                add_filter( 'wp_redirect', array( $this, 'add_state_query_params' ) );
1856
1857                wp_enqueue_script( 'customize-preview' );
1858                wp_enqueue_style( 'customize-preview' );
1859                add_action( 'wp_head', array( $this, 'customize_preview_loading_style' ) );
1860                add_action( 'wp_head', array( $this, 'remove_frameless_preview_messenger_channel' ) );
1861                add_action( 'wp_footer', array( $this, 'customize_preview_settings' ), 20 );
1862                add_filter( 'get_edit_post_link', '__return_empty_string' );
1863
1864                /**
1865                 * Fires once the Customizer preview has initialized and JavaScript
1866                 * settings have been printed.
1867                 *
1868                 * @since 3.4.0
1869                 *
1870                 * @param WP_Customize_Manager $this WP_Customize_Manager instance.
1871                 */
1872                do_action( 'customize_preview_init', $this );
1873        }
1874
1875        /**
1876         * Filter the X-Frame-Options and Content-Security-Policy headers to ensure frontend can load in customizer.
1877         *
1878         * @since 4.7.0
1879         *
1880         * @param array $headers Headers.
1881         * @return array Headers.
1882         */
1883        public function filter_iframe_security_headers( $headers ) {
1884                $customize_url = admin_url( 'customize.php' );
1885                $headers['X-Frame-Options'] = 'ALLOW-FROM ' . $customize_url;
1886                $headers['Content-Security-Policy'] = 'frame-ancestors ' . preg_replace( '#^(\w+://[^/]+).+?$#', '$1', $customize_url );
1887                return $headers;
1888        }
1889
1890        /**
1891         * Add customize state query params to a given URL if preview is allowed.
1892         *
1893         * @since 4.7.0
1894         * @see wp_redirect()
1895         * @see WP_Customize_Manager::get_allowed_url()
1896         *
1897         * @param string $url URL.
1898         * @return string URL.
1899         */
1900        public function add_state_query_params( $url ) {
1901                $parsed_original_url = wp_parse_url( $url );
1902                $is_allowed = false;
1903                foreach ( $this->get_allowed_urls() as $allowed_url ) {
1904                        $parsed_allowed_url = wp_parse_url( $allowed_url );
1905                        $is_allowed = (
1906                                $parsed_allowed_url['scheme'] === $parsed_original_url['scheme']
1907                                &&
1908                                $parsed_allowed_url['host'] === $parsed_original_url['host']
1909                                &&
1910                                0 === strpos( $parsed_original_url['path'], $parsed_allowed_url['path'] )
1911                        );
1912                        if ( $is_allowed ) {
1913                                break;
1914                        }
1915                }
1916
1917                if ( $is_allowed ) {
1918                        $query_params = array(
1919                                'customize_changeset_uuid' => $this->changeset_uuid(),
1920                        );
1921                        if ( ! $this->is_theme_active() ) {
1922                                $query_params['customize_theme'] = $this->get_stylesheet();
1923                        }
1924                        if ( $this->messenger_channel ) {
1925                                $query_params['customize_messenger_channel'] = $this->messenger_channel;
1926                        }
1927                        $url = add_query_arg( $query_params, $url );
1928                }
1929
1930                return $url;
1931        }
1932
1933        /**
1934         * Prevent sending a 404 status when returning the response for the customize
1935         * preview, since it causes the jQuery Ajax to fail. Send 200 instead.
1936         *
1937         * @since 4.0.0
1938         * @deprecated 4.7.0
1939         */
1940        public function customize_preview_override_404_status() {
1941                _deprecated_function( __METHOD__, '4.7.0' );
1942        }
1943
1944        /**
1945         * Print base element for preview frame.
1946         *
1947         * @since 3.4.0
1948         * @deprecated 4.7.0
1949         */
1950        public function customize_preview_base() {
1951                _deprecated_function( __METHOD__, '4.7.0' );
1952        }
1953
1954        /**
1955         * Print a workaround to handle HTML5 tags in IE < 9.
1956         *
1957         * @since 3.4.0
1958         * @deprecated 4.7.0 Customizer no longer supports IE8, so all supported browsers recognize HTML5.
1959         */
1960        public function customize_preview_html5() {
1961                _deprecated_function( __FUNCTION__, '4.7.0' );
1962        }
1963
1964        /**
1965         * Print CSS for loading indicators for the Customizer preview.
1966         *
1967         * @since 4.2.0
1968         */
1969        public function customize_preview_loading_style() {
1970                ?><style>
1971                        body.wp-customizer-unloading {
1972                                opacity: 0.25;
1973                                cursor: progress !important;
1974                                -webkit-transition: opacity 0.5s;
1975                                transition: opacity 0.5s;
1976                        }
1977                        body.wp-customizer-unloading * {
1978                                pointer-events: none !important;
1979                        }
1980                        form.customize-unpreviewable,
1981                        form.customize-unpreviewable input,
1982                        form.customize-unpreviewable select,
1983                        form.customize-unpreviewable button,
1984                        a.customize-unpreviewable,
1985                        area.customize-unpreviewable {
1986                                cursor: not-allowed !important;
1987                        }
1988                </style><?php
1989        }
1990
1991        /**
1992         * Remove customize_messenger_channel query parameter from the preview window when it is not in an iframe.
1993         *
1994         * This ensures that the admin bar will be shown. It also ensures that link navigation will
1995         * work as expected since the parent frame is not being sent the URL to navigate to.
1996         *
1997         * @since 4.7.0
1998         */
1999        public function remove_frameless_preview_messenger_channel() {
2000                if ( ! $this->messenger_channel ) {
2001                        return;
2002                }
2003                ?>
2004                <script>
2005                ( function() {
2006                        var urlParser, oldQueryParams, newQueryParams, i;
2007                        if ( parent !== window ) {
2008                                return;
2009                        }
2010                        urlParser = document.createElement( 'a' );
2011                        urlParser.href = location.href;
2012                        oldQueryParams = urlParser.search.substr( 1 ).split( /&/ );
2013                        newQueryParams = [];
2014                        for ( i = 0; i < oldQueryParams.length; i += 1 ) {
2015                                if ( ! /^customize_messenger_channel=/.test( oldQueryParams[ i ] ) ) {
2016                                        newQueryParams.push( oldQueryParams[ i ] );
2017                                }
2018                        }
2019                        urlParser.search = newQueryParams.join( '&' );
2020                        if ( urlParser.search !== location.search ) {
2021                                location.replace( urlParser.href );
2022                        }
2023                } )();
2024                </script>
2025                <?php
2026        }
2027
2028        /**
2029         * Print JavaScript settings for preview frame.
2030         *
2031         * @since 3.4.0
2032         */
2033        public function customize_preview_settings() {
2034                $post_values = $this->unsanitized_post_values( array( 'exclude_changeset' => true ) );
2035                $setting_validities = $this->validate_setting_values( $post_values );
2036                $exported_setting_validities = array_map( array( $this, 'prepare_setting_validity_for_js' ), $setting_validities );
2037
2038                // Note that the REQUEST_URI is not passed into home_url() since this breaks subdirectory installations.
2039                $self_url = empty( $_SERVER['REQUEST_URI'] ) ? home_url( '/' ) : esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) );
2040                $state_query_params = array(
2041                        'customize_theme',
2042                        'customize_changeset_uuid',
2043                        'customize_messenger_channel',
2044                );
2045                $self_url = remove_query_arg( $state_query_params, $self_url );
2046
2047                $allowed_urls = $this->get_allowed_urls();
2048                $allowed_hosts = array();
2049                foreach ( $allowed_urls as $allowed_url ) {
2050                        $parsed = wp_parse_url( $allowed_url );
2051                        if ( empty( $parsed['host'] ) ) {
2052                                continue;
2053                        }
2054                        $host = $parsed['host'];
2055                        if ( ! empty( $parsed['port'] ) ) {
2056                                $host .= ':' . $parsed['port'];
2057                        }
2058                        $allowed_hosts[] = $host;
2059                }
2060
2061                $switched_locale = switch_to_locale( get_user_locale() );
2062                $l10n = array(
2063                        'shiftClickToEdit' => __( 'Shift-click to edit this element.' ),
2064                        'linkUnpreviewable' => __( 'This link is not live-previewable.' ),
2065                        'formUnpreviewable' => __( 'This form is not live-previewable.' ),
2066                );
2067                if ( $switched_locale ) {
2068                        restore_previous_locale();
2069                }
2070
2071                $settings = array(
2072                        'changeset' => array(
2073                                'uuid' => $this->changeset_uuid(),
2074                                'autosaved' => $this->autosaved(),
2075                        ),
2076                        'timeouts' => array(
2077                                'selectiveRefresh' => 250,
2078                                'keepAliveSend' => 1000,
2079                        ),
2080                        'theme' => array(
2081                                'stylesheet' => $this->get_stylesheet(),
2082                                'active'     => $this->is_theme_active(),
2083                        ),
2084                        'url' => array(
2085                                'self' => $self_url,
2086                                'allowed' => array_map( 'esc_url_raw', $this->get_allowed_urls() ),
2087                                'allowedHosts' => array_unique( $allowed_hosts ),
2088                                'isCrossDomain' => $this->is_cross_domain(),
2089                        ),
2090                        'channel' => $this->messenger_channel,
2091                        'activePanels' => array(),
2092                        'activeSections' => array(),
2093                        'activeControls' => array(),
2094                        'settingValidities' => $exported_setting_validities,
2095                        'nonce' => current_user_can( 'customize' ) ? $this->get_nonces() : array(),
2096                        'l10n' => $l10n,
2097                        '_dirty' => array_keys( $post_values ),
2098                );
2099
2100                foreach ( $this->panels as $panel_id => $panel ) {
2101                        if ( $panel->check_capabilities() ) {
2102                                $settings['activePanels'][ $panel_id ] = $panel->active();
2103                                foreach ( $panel->sections as $section_id => $section ) {
2104                                        if ( $section->check_capabilities() ) {
2105                                                $settings['activeSections'][ $section_id ] = $section->active();
2106                                        }
2107                                }
2108                        }
2109                }
2110                foreach ( $this->sections as $id => $section ) {
2111                        if ( $section->check_capabilities() ) {
2112                                $settings['activeSections'][ $id ] = $section->active();
2113                        }
2114                }
2115                foreach ( $this->controls as $id => $control ) {
2116                        if ( $control->check_capabilities() ) {
2117                                $settings['activeControls'][ $id ] = $control->active();
2118                        }
2119                }
2120
2121                ?>
2122                <script type="text/javascript">
2123                        var _wpCustomizeSettings = <?php echo wp_json_encode( $settings ); ?>;
2124                        _wpCustomizeSettings.values = {};
2125                        (function( v ) {
2126                                <?php
2127                                /*
2128                                 * Serialize settings separately from the initial _wpCustomizeSettings
2129                                 * serialization in order to avoid a peak memory usage spike.
2130                                 * @todo We may not even need to export the values at all since the pane syncs them anyway.
2131                                 */
2132                                foreach ( $this->settings as $id => $setting ) {
2133                                        if ( $setting->check_capabilities() ) {
2134                                                printf(
2135                                                        "v[%s] = %s;\n",
2136                                                        wp_json_encode( $id ),
2137                                                        wp_json_encode( $setting->js_value() )
2138                                                );
2139                                        }
2140                                }
2141                                ?>
2142                        })( _wpCustomizeSettings.values );
2143                </script>
2144                <?php
2145        }
2146
2147        /**
2148         * Prints a signature so we can ensure the Customizer was properly executed.
2149         *
2150         * @since 3.4.0
2151         * @deprecated 4.7.0
2152         */
2153        public function customize_preview_signature() {
2154                _deprecated_function( __METHOD__, '4.7.0' );
2155        }
2156
2157        /**
2158         * Removes the signature in case we experience a case where the Customizer was not properly executed.
2159         *
2160         * @since 3.4.0
2161         * @deprecated 4.7.0
2162         *
2163         * @param mixed $return Value passed through for {@see 'wp_die_handler'} filter.
2164         * @return mixed Value passed through for {@see 'wp_die_handler'} filter.
2165         */
2166        public function remove_preview_signature( $return = null ) {
2167                _deprecated_function( __METHOD__, '4.7.0' );
2168
2169                return $return;
2170        }
2171
2172        /**
2173         * Is it a theme preview?
2174         *
2175         * @since 3.4.0
2176         *
2177         * @return bool True if it's a preview, false if not.
2178         */
2179        public function is_preview() {
2180                return (bool) $this->previewing;
2181        }
2182
2183        /**
2184         * Retrieve the template name of the previewed theme.
2185         *
2186         * @since 3.4.0
2187         *
2188         * @return string Template name.
2189         */
2190        public function get_template() {
2191                return $this->theme()->get_template();
2192        }
2193
2194        /**
2195         * Retrieve the stylesheet name of the previewed theme.
2196         *
2197         * @since 3.4.0
2198         *
2199         * @return string Stylesheet name.
2200         */
2201        public function get_stylesheet() {
2202                return $this->theme()->get_stylesheet();
2203        }
2204
2205        /**
2206         * Retrieve the template root of the previewed theme.
2207         *
2208         * @since 3.4.0
2209         *
2210         * @return string Theme root.
2211         */
2212        public function get_template_root() {
2213                return get_raw_theme_root( $this->get_template(), true );
2214        }
2215
2216        /**
2217         * Retrieve the stylesheet root of the previewed theme.
2218         *
2219         * @since 3.4.0
2220         *
2221         * @return string Theme root.
2222         */
2223        public function get_stylesheet_root() {
2224                return get_raw_theme_root( $this->get_stylesheet(), true );
2225        }
2226
2227        /**
2228         * Filters the current theme and return the name of the previewed theme.
2229         *
2230         * @since 3.4.0
2231         *
2232         * @param $current_theme {@internal Parameter is not used}
2233         * @return string Theme name.
2234         */
2235        public function current_theme( $current_theme ) {
2236                return $this->theme()->display('Name');
2237        }
2238
2239        /**
2240         * Validates setting values.
2241         *
2242         * Validation is skipped for unregistered settings or for values that are
2243         * already null since they will be skipped anyway. Sanitization is applied
2244         * to values that pass validation, and values that become null or `WP_Error`
2245         * after sanitizing are marked invalid.
2246         *
2247         * @since 4.6.0
2248         *
2249         * @see WP_REST_Request::has_valid_params()
2250         * @see WP_Customize_Setting::validate()
2251         *
2252         * @param array $setting_values Mapping of setting IDs to values to validate and sanitize.
2253         * @param array $options {
2254         *     Options.
2255         *
2256         *     @type bool $validate_existence  Whether a setting's existence will be checked.
2257         *     @type bool $validate_capability Whether the setting capability will be checked.
2258         * }
2259         * @return array Mapping of setting IDs to return value of validate method calls, either `true` or `WP_Error`.
2260         */
2261        public function validate_setting_values( $setting_values, $options = array() ) {
2262                $options = wp_parse_args( $options, array(
2263                        'validate_capability' => false,
2264                        'validate_existence' => false,
2265                ) );
2266
2267                $validities = array();
2268                foreach ( $setting_values as $setting_id => $unsanitized_value ) {
2269                        $setting = $this->get_setting( $setting_id );
2270                        if ( ! $setting ) {
2271                                if ( $options['validate_existence'] ) {
2272                                        $validities[ $setting_id ] = new WP_Error( 'unrecognized', __( 'Setting does not exist or is unrecognized.' ) );
2273                                }
2274                                continue;
2275                        }
2276                        if ( $options['validate_capability'] && ! current_user_can( $setting->capability ) ) {
2277                                $validity = new WP_Error( 'unauthorized', __( 'Unauthorized to modify setting due to capability.' ) );
2278                        } else {
2279                                if ( is_null( $unsanitized_value ) ) {
2280                                        continue;
2281                                }
2282                                $validity = $setting->validate( $unsanitized_value );
2283                        }
2284                        if ( ! is_wp_error( $validity ) ) {
2285                                /** This filter is documented in wp-includes/class-wp-customize-setting.php */
2286                                $late_validity = apply_filters( "customize_validate_{$setting->id}", new WP_Error(), $unsanitized_value, $setting );
2287                                if ( ! empty( $late_validity->errors ) ) {
2288                                        $validity = $late_validity;
2289                                }
2290                        }
2291                        if ( ! is_wp_error( $validity ) ) {
2292                                $value = $setting->sanitize( $unsanitized_value );
2293                                if ( is_null( $value ) ) {
2294                                        $validity = false;
2295                                } elseif ( is_wp_error( $value ) ) {
2296                                        $validity = $value;
2297                                }
2298                        }
2299                        if ( false === $validity ) {
2300                                $validity = new WP_Error( 'invalid_value', __( 'Invalid value.' ) );
2301                        }
2302                        $validities[ $setting_id ] = $validity;
2303                }
2304                return $validities;
2305        }
2306
2307        /**
2308         * Prepares setting validity for exporting to the client (JS).
2309         *
2310         * Converts `WP_Error` instance into array suitable for passing into the
2311         * `wp.customize.Notification` JS model.
2312         *
2313         * @since 4.6.0
2314         *
2315         * @param true|WP_Error $validity Setting validity.
2316         * @return true|array If `$validity` was a WP_Error, the error codes will be array-mapped
2317         *                    to their respective `message` and `data` to pass into the
2318         *                    `wp.customize.Notification` JS model.
2319         */
2320        public function prepare_setting_validity_for_js( $validity ) {
2321                if ( is_wp_error( $validity ) ) {
2322                        $notification = array();
2323                        foreach ( $validity->errors as $error_code => $error_messages ) {
2324                                $notification[ $error_code ] = array(
2325                                        'message' => join( ' ', $error_messages ),
2326                                        'data' => $validity->get_error_data( $error_code ),
2327                                );
2328                        }
2329                        return $notification;
2330                } else {
2331                        return true;
2332                }
2333        }
2334
2335        /**
2336         * Handle customize_save WP Ajax request to save/update a changeset.
2337         *
2338         * @since 3.4.0
2339         * @since 4.7.0 The semantics of this method have changed to update a changeset, optionally to also change the status and other attributes.
2340         */
2341        public function save() {
2342                if ( ! is_user_logged_in() ) {
2343                        wp_send_json_error( 'unauthenticated' );
2344                }
2345
2346                if ( ! $this->is_preview() ) {
2347                        wp_send_json_error( 'not_preview' );
2348                }
2349
2350                $action = 'save-customize_' . $this->get_stylesheet();
2351                if ( ! check_ajax_referer( $action, 'nonce', false ) ) {
2352                        wp_send_json_error( 'invalid_nonce' );
2353                }
2354
2355                $changeset_post_id = $this->changeset_post_id();
2356                $is_new_changeset = empty( $changeset_post_id );
2357                if ( $is_new_changeset ) {
2358                        if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->create_posts ) ) {
2359                                wp_send_json_error( 'cannot_create_changeset_post' );
2360                        }
2361                } else {
2362                        if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->edit_post, $changeset_post_id ) ) {
2363                                wp_send_json_error( 'cannot_edit_changeset_post' );
2364                        }
2365                }
2366
2367                if ( ! empty( $_POST['customize_changeset_data'] ) ) {
2368                        $input_changeset_data = json_decode( wp_unslash( $_POST['customize_changeset_data'] ), true );
2369                        if ( ! is_array( $input_changeset_data ) ) {
2370                                wp_send_json_error( 'invalid_customize_changeset_data' );
2371                        }
2372                } else {
2373                        $input_changeset_data = array();
2374                }
2375
2376                // Validate title.
2377                $changeset_title = null;
2378                if ( isset( $_POST['customize_changeset_title'] ) ) {
2379                        $changeset_title = sanitize_text_field( wp_unslash( $_POST['customize_changeset_title'] ) );
2380                }
2381
2382                // Validate changeset status param.
2383                $is_publish = null;
2384                $changeset_status = null;
2385                if ( isset( $_POST['customize_changeset_status'] ) ) {
2386                        $changeset_status = wp_unslash( $_POST['customize_changeset_status'] );
2387                        if ( ! get_post_status_object( $changeset_status ) || ! in_array( $changeset_status, array( 'draft', 'pending', 'publish', 'future' ), true ) ) {
2388                                wp_send_json_error( 'bad_customize_changeset_status', 400 );
2389                        }
2390                        $is_publish = ( 'publish' === $changeset_status || 'future' === $changeset_status );
2391                        if ( $is_publish && ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->publish_posts ) ) {
2392                                wp_send_json_error( 'changeset_publish_unauthorized', 403 );
2393                        }
2394                }
2395
2396                /*
2397                 * Validate changeset date param. Date is assumed to be in local time for
2398                 * the WP if in MySQL format (YYYY-MM-DD HH:MM:SS). Otherwise, the date
2399                 * is parsed with strtotime() so that ISO date format may be supplied
2400                 * or a string like "+10 minutes".
2401                 */
2402                $changeset_date_gmt = null;
2403                if ( isset( $_POST['customize_changeset_date'] ) ) {
2404                        $changeset_date = wp_unslash( $_POST['customize_changeset_date'] );
2405                        if ( preg_match( '/^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d$/', $changeset_date ) ) {
2406                                $mm = substr( $changeset_date, 5, 2 );
2407                                $jj = substr( $changeset_date, 8, 2 );
2408                                $aa = substr( $changeset_date, 0, 4 );
2409                                $valid_date = wp_checkdate( $mm, $jj, $aa, $changeset_date );
2410                                if ( ! $valid_date ) {
2411                                        wp_send_json_error( 'bad_customize_changeset_date', 400 );
2412                                }
2413                                $changeset_date_gmt = get_gmt_from_date( $changeset_date );
2414                        } else {
2415                                $timestamp = strtotime( $changeset_date );
2416                                if ( ! $timestamp ) {
2417                                        wp_send_json_error( 'bad_customize_changeset_date', 400 );
2418                                }
2419                                $changeset_date_gmt = gmdate( 'Y-m-d H:i:s', $timestamp );
2420                        }
2421                }
2422
2423                $lock_user_id = null;
2424                $autosave = ! empty( $_POST['customize_changeset_autosave'] );
2425                if ( ! $is_new_changeset ) {
2426                        $lock_user_id = wp_check_post_lock( $this->changeset_post_id() );
2427                }
2428
2429                // Force request to autosave when changeset is locked.
2430                if ( $lock_user_id && ! $autosave ) {
2431                        $autosave = true;
2432                        $changeset_status = null;
2433                        $changeset_date_gmt = null;
2434                }
2435
2436                if ( $autosave && ! defined( 'DOING_AUTOSAVE' ) ) { // Back-compat.
2437                        define( 'DOING_AUTOSAVE', true );
2438                }
2439
2440                $autosaved = false;
2441                $r = $this->save_changeset_post( array(
2442                        'status' => $changeset_status,
2443                        'title' => $changeset_title,
2444                        'date_gmt' => $changeset_date_gmt,
2445                        'data' => $input_changeset_data,
2446                        'autosave' => $autosave,
2447                ) );
2448                if ( $autosave && ! is_wp_error( $r ) ) {
2449                        $autosaved = true;
2450                }
2451
2452                // If the changeset was locked and an autosave request wasn't itself an error, then now explicitly return with a failure.
2453                if ( $lock_user_id && ! is_wp_error( $r ) ) {
2454                        $r = new WP_Error(
2455                                'changeset_locked',
2456                                __( 'Changeset is being edited by other user.' ),
2457                                array(
2458                                        'lock_user' => $this->get_lock_user_data( $lock_user_id ),
2459                                )
2460                        );
2461                }
2462
2463                if ( is_wp_error( $r ) ) {
2464                        $response = array(
2465                                'message' => $r->get_error_message(),
2466                                'code' => $r->get_error_code(),
2467                        );
2468                        if ( is_array( $r->get_error_data() ) ) {
2469                                $response = array_merge( $response, $r->get_error_data() );
2470                        } else {
2471                                $response['data'] = $r->get_error_data();
2472                        }
2473                } else {
2474                        $response = $r;
2475                        $changeset_post = get_post( $this->changeset_post_id() );
2476
2477                        // Dismiss all other auto-draft changeset posts for this user (they serve like autosave revisions), as there should only be one.
2478                        if ( $is_new_changeset ) {
2479                                $this->dismiss_user_auto_draft_changesets();
2480                        }
2481
2482                        // Note that if the changeset status was publish, then it will get set to trash if revisions are not supported.
2483                        $response['changeset_status'] = $changeset_post->post_status;
2484                        if ( $is_publish && 'trash' === $response['changeset_status'] ) {
2485                                $response['changeset_status'] = 'publish';
2486                        }
2487
2488                        if ( 'publish' !== $response['changeset_status'] ) {
2489                                $this->set_changeset_lock( $changeset_post->ID );
2490                        }
2491
2492                        if ( 'future' === $response['changeset_status'] ) {
2493                                $response['changeset_date'] = $changeset_post->post_date;
2494                        }
2495
2496                        if ( 'publish' === $response['changeset_status'] || 'trash' === $response['changeset_status'] ) {
2497                                $response['next_changeset_uuid'] = wp_generate_uuid4();
2498                        }
2499                }
2500
2501                if ( $autosave ) {
2502                        $response['autosaved'] = $autosaved;
2503                }
2504
2505                if ( isset( $response['setting_validities'] ) ) {
2506                        $response['setting_validities'] = array_map( array( $this, 'prepare_setting_validity_for_js' ), $response['setting_validities'] );
2507                }
2508
2509                /**
2510                 * Filters response data for a successful customize_save Ajax request.
2511                 *
2512                 * This filter does not apply if there was a nonce or authentication failure.
2513                 *
2514                 * @since 4.2.0
2515                 *
2516                 * @param array                $response Additional information passed back to the 'saved'
2517                 *                                       event on `wp.customize`.
2518                 * @param WP_Customize_Manager $this     WP_Customize_Manager instance.
2519                 */
2520                $response = apply_filters( 'customize_save_response', $response, $this );
2521
2522                if ( is_wp_error( $r ) ) {
2523                        wp_send_json_error( $response );
2524                } else {
2525                        wp_send_json_success( $response );
2526                }
2527        }
2528
2529        /**
2530         * Save the post for the loaded changeset.
2531         *
2532         * @since 4.7.0
2533         *
2534         * @param array $args {
2535         *     Args for changeset post.
2536         *
2537         *     @type array  $data            Optional additional changeset data. Values will be merged on top of any existing post values.
2538         *     @type string $status          Post status. Optional. If supplied, the save will be transactional and a post revision will be allowed.
2539         *     @type string $title           Post title. Optional.
2540         *     @type string $date_gmt        Date in GMT. Optional.
2541         *     @type int    $user_id         ID for user who is saving the changeset. Optional, defaults to the current user ID.
2542         *     @type bool   $starter_content Whether the data is starter content. If false (default), then $starter_content will be cleared for any $data being saved.
2543         *     @type bool   $autosave        Whether this is a request to create an autosave revision.
2544         * }
2545         *
2546         * @return array|WP_Error Returns array on success and WP_Error with array data on error.
2547         */
2548        function save_changeset_post( $args = array() ) {
2549
2550                $args = array_merge(
2551                        array(
2552                                'status' => null,
2553                                'title' => null,
2554                                'data' => array(),
2555                                'date_gmt' => null,
2556                                'user_id' => get_current_user_id(),
2557                                'starter_content' => false,
2558                                'autosave' => false,
2559                        ),
2560                        $args
2561                );
2562
2563                $changeset_post_id = $this->changeset_post_id();
2564                $existing_changeset_data = array();
2565                if ( $changeset_post_id ) {
2566                        $existing_status = get_post_status( $changeset_post_id );
2567                        if ( 'publish' === $existing_status || 'trash' === $existing_status ) {
2568                                return new WP_Error(
2569                                        'changeset_already_published',
2570                                        __( 'The previous set of changes has already been published. Please try saving your current set of changes again.' ),
2571                                        array(
2572                                                'next_changeset_uuid' => wp_generate_uuid4(),
2573                                        )
2574                                );
2575                        }
2576
2577                        $existing_changeset_data = $this->get_changeset_post_data( $changeset_post_id );
2578                        if ( is_wp_error( $existing_changeset_data ) ) {
2579                                return $existing_changeset_data;
2580                        }
2581                }
2582
2583                // Fail if attempting to publish but publish hook is missing.
2584                if ( 'publish' === $args['status'] && false === has_action( 'transition_post_status', '_wp_customize_publish_changeset' ) ) {
2585                        return new WP_Error( 'missing_publish_callback' );
2586                }
2587
2588                // Validate date.
2589                $now = gmdate( 'Y-m-d H:i:59' );
2590                if ( $args['date_gmt'] ) {
2591                        $is_future_dated = ( mysql2date( 'U', $args['date_gmt'], false ) > mysql2date( 'U', $now, false ) );
2592                        if ( ! $is_future_dated ) {
2593                                return new WP_Error( 'not_future_date', __( 'You must supply a future date to schedule.' ) ); // Only future dates are allowed.
2594                        }
2595
2596                        if ( ! $this->is_theme_active() && ( 'future' === $args['status'] || $is_future_dated ) ) {
2597                                return new WP_Error( 'cannot_schedule_theme_switches' ); // This should be allowed in the future, when theme is a regular setting.
2598                        }
2599                        $will_remain_auto_draft = ( ! $args['status'] && ( ! $changeset_post_id || 'auto-draft' === get_post_status( $changeset_post_id ) ) );
2600                        if ( $will_remain_auto_draft ) {
2601                                return new WP_Error( 'cannot_supply_date_for_auto_draft_changeset' );
2602                        }
2603                } elseif ( $changeset_post_id && 'future' === $args['status'] ) {
2604
2605                        // Fail if the new status is future but the existing post's date is not in the future.
2606                        $changeset_post = get_post( $changeset_post_id );
2607                        if ( mysql2date( 'U', $changeset_post->post_date_gmt, false ) <= mysql2date( 'U', $now, false ) ) {
2608                                return new WP_Error( 'not_future_date', __( 'You must supply a future date to schedule.' ) );
2609                        }
2610                }
2611
2612                if ( ! empty( $is_future_dated ) && 'publish' === $args['status'] ) {
2613                        $args['status'] = 'future';
2614                }
2615
2616                // Validate autosave param. See _wp_post_revision_fields() for why these fields are disallowed.
2617                if ( $args['autosave'] ) {
2618                        if ( $args['date_gmt'] ) {
2619                                return new WP_Error( 'illegal_autosave_with_date_gmt' );
2620                        } elseif ( $args['status'] ) {
2621                                return new WP_Error( 'illegal_autosave_with_status' );
2622                        } elseif ( $args['user_id'] && get_current_user_id() !== $args['user_id'] ) {
2623                                return new WP_Error( 'illegal_autosave_with_non_current_user' );
2624                        }
2625                }
2626
2627                // The request was made via wp.customize.previewer.save().
2628                $update_transactionally = (bool) $args['status'];
2629                $allow_revision = (bool) $args['status'];
2630
2631                // Amend post values with any supplied data.
2632                foreach ( $args['data'] as $setting_id => $setting_params ) {
2633                        if ( is_array( $setting_params ) && array_key_exists( 'value', $setting_params ) ) {
2634                                $this->set_post_value( $setting_id, $setting_params['value'] ); // Add to post values so that they can be validated and sanitized.
2635                        }
2636                }
2637
2638                // Note that in addition to post data, this will include any stashed theme mods.
2639                $post_values = $this->unsanitized_post_values( array(
2640                        'exclude_changeset' => true,
2641                        'exclude_post_data' => false,
2642                ) );
2643                $this->add_dynamic_settings( array_keys( $post_values ) ); // Ensure settings get created even if they lack an input value.
2644
2645                /*
2646                 * Get list of IDs for settings that have values different from what is currently
2647                 * saved in the changeset. By skipping any values that are already the same, the
2648                 * subset of changed settings can be passed into validate_setting_values to prevent
2649                 * an underprivileged modifying a single setting for which they have the capability
2650                 * from being blocked from saving. This also prevents a user from touching of the
2651                 * previous saved settings and overriding the associated user_id if they made no change.
2652                 */
2653                $changed_setting_ids = array();
2654                foreach ( $post_values as $setting_id => $setting_value ) {
2655                        $setting = $this->get_setting( $setting_id );
2656
2657                        if ( $setting && 'theme_mod' === $setting->type ) {
2658                                $prefixed_setting_id = $this->get_stylesheet() . '::' . $setting->id;
2659                        } else {
2660                                $prefixed_setting_id = $setting_id;
2661                        }
2662
2663                        $is_value_changed = (
2664                                ! isset( $existing_changeset_data[ $prefixed_setting_id ] )
2665                                ||
2666                                ! array_key_exists( 'value', $existing_changeset_data[ $prefixed_setting_id ] )
2667                                ||
2668                                $existing_changeset_data[ $prefixed_setting_id ]['value'] !== $setting_value
2669                        );
2670                        if ( $is_value_changed ) {
2671                                $changed_setting_ids[] = $setting_id;
2672                        }
2673                }
2674
2675                /**
2676                 * Fires before save validation happens.
2677                 *
2678                 * Plugins can add just-in-time {@see 'customize_validate_{$this->ID}'} filters
2679                 * at this point to catch any settings registered after `customize_register`.
2680                 * The dynamic portion of the hook name, `$this->ID` refers to the setting ID.
2681                 *
2682                 * @since 4.6.0
2683                 *
2684                 * @param WP_Customize_Manager $this WP_Customize_Manager instance.
2685                 */
2686                do_action( 'customize_save_validation_before', $this );
2687
2688                // Validate settings.
2689                $validated_values = array_merge(
2690                        array_fill_keys( array_keys( $args['data'] ), null ), // Make sure existence/capability checks are done on value-less setting updates.
2691                        $post_values
2692                );
2693                $setting_validities = $this->validate_setting_values( $validated_values, array(
2694                        'validate_capability' => true,
2695                        'validate_existence' => true,
2696                ) );
2697                $invalid_setting_count = count( array_filter( $setting_validities, 'is_wp_error' ) );
2698
2699                /*
2700                 * Short-circuit if there are invalid settings the update is transactional.
2701                 * A changeset update is transactional when a status is supplied in the request.
2702                 */
2703                if ( $update_transactionally && $invalid_setting_count > 0 ) {
2704                        $response = array(
2705                                'setting_validities' => $setting_validities,
2706                                /* translators: %s: number of invalid settings */
2707                                'message' => sprintf( _n( 'Unable to save due to %s invalid setting.', 'Unable to save due to %s invalid settings.', $invalid_setting_count ), number_format_i18n( $invalid_setting_count ) ),
2708                        );
2709                        return new WP_Error( 'transaction_fail', '', $response );
2710                }
2711
2712                // Obtain/merge data for changeset.
2713                $original_changeset_data = $this->get_changeset_post_data( $changeset_post_id );
2714                $data = $original_changeset_data;
2715                if ( is_wp_error( $data ) ) {
2716                        $data = array();
2717                }
2718
2719                // Ensure that all post values are included in the changeset data.
2720                foreach ( $post_values as $setting_id => $post_value ) {
2721                        if ( ! isset( $args['data'][ $setting_id ] ) ) {
2722                                $args['data'][ $setting_id ] = array();
2723                        }
2724                        if ( ! isset( $args['data'][ $setting_id ]['value'] ) ) {
2725                                $args['data'][ $setting_id ]['value'] = $post_value;
2726                        }
2727                }
2728
2729                foreach ( $args['data'] as $setting_id => $setting_params ) {
2730                        $setting = $this->get_setting( $setting_id );
2731                        if ( ! $setting || ! $setting->check_capabilities() ) {
2732                                continue;
2733                        }
2734
2735                        // Skip updating changeset for invalid setting values.
2736                        if ( isset( $setting_validities[ $setting_id ] ) && is_wp_error( $setting_validities[ $setting_id ] ) ) {
2737                                continue;
2738                        }
2739
2740                        $changeset_setting_id = $setting_id;
2741                        if ( 'theme_mod' === $setting->type ) {
2742                                $changeset_setting_id = sprintf( '%s::%s', $this->get_stylesheet(), $setting_id );
2743                        }
2744
2745                        if ( null === $setting_params ) {
2746                                // Remove setting from changeset entirely.
2747                                unset( $data[ $changeset_setting_id ] );
2748                        } else {
2749
2750                                if ( ! isset( $data[ $changeset_setting_id ] ) ) {
2751                                        $data[ $changeset_setting_id ] = array();
2752                                }
2753
2754                                // Merge any additional setting params that have been supplied with the existing params.
2755                                $merged_setting_params = array_merge( $data[ $changeset_setting_id ], $setting_params );
2756
2757                                // Skip updating setting params if unchanged (ensuring the user_id is not overwritten).
2758                                if ( $data[ $changeset_setting_id ] === $merged_setting_params ) {
2759                                        continue;
2760                                }
2761
2762                                $data[ $changeset_setting_id ] = array_merge(
2763                                        $merged_setting_params,
2764                                        array(
2765                                                'type' => $setting->type,
2766                                                'user_id' => $args['user_id'],
2767                                                'date_modified_gmt' => current_time( 'mysql', true ),
2768                                        )
2769                                );
2770
2771                                // Clear starter_content flag in data if changeset is not explicitly being updated for starter content.
2772                                if ( empty( $args['starter_content'] ) ) {
2773                                        unset( $data[ $changeset_setting_id ]['starter_content'] );
2774                                }
2775                        }
2776                }
2777
2778                $filter_context = array(
2779                        'uuid' => $this->changeset_uuid(),
2780                        'title' => $args['title'],
2781                        'status' => $args['status'],
2782                        'date_gmt' => $args['date_gmt'],
2783                        'post_id' => $changeset_post_id,
2784                        'previous_data' => is_wp_error( $original_changeset_data ) ? array() : $original_changeset_data,
2785                        'manager' => $this,
2786                );
2787
2788                /**
2789                 * Filters the settings' data that will be persisted into the changeset.
2790                 *
2791                 * Plugins may amend additional data (such as additional meta for settings) into the changeset with this filter.
2792                 *
2793                 * @since 4.7.0
2794                 *
2795                 * @param array $data Updated changeset data, mapping setting IDs to arrays containing a $value item and optionally other metadata.
2796                 * @param array $context {
2797                 *     Filter context.
2798                 *
2799                 *     @type string               $uuid          Changeset UUID.
2800                 *     @type string               $title         Requested title for the changeset post.
2801                 *     @type string               $status        Requested status for the changeset post.
2802                 *     @type string               $date_gmt      Requested date for the changeset post in MySQL format and GMT timezone.
2803                 *     @type int|false            $post_id       Post ID for the changeset, or false if it doesn't exist yet.
2804                 *     @type array                $previous_data Previous data contained in the changeset.
2805                 *     @type WP_Customize_Manager $manager       Manager instance.
2806                 * }
2807                 */
2808                $data = apply_filters( 'customize_changeset_save_data', $data, $filter_context );
2809
2810                // Switch theme if publishing changes now.
2811                if ( 'publish' === $args['status'] && ! $this->is_theme_active() ) {
2812                        // Temporarily stop previewing the theme to allow switch_themes() to operate properly.
2813                        $this->stop_previewing_theme();
2814                        switch_theme( $this->get_stylesheet() );
2815                        update_option( 'theme_switched_via_customizer', true );
2816                        $this->start_previewing_theme();
2817                }
2818
2819                // Gather the data for wp_insert_post()/wp_update_post().
2820                $json_options = 0;
2821                if ( defined( 'JSON_UNESCAPED_SLASHES' ) ) {
2822                        $json_options |= JSON_UNESCAPED_SLASHES; // Introduced in PHP 5.4. This is only to improve readability as slashes needn't be escaped in storage.
2823                }
2824                $json_options |= JSON_PRETTY_PRINT; // Also introduced in PHP 5.4, but WP defines constant for back compat. See WP Trac #30139.
2825                $post_array = array(
2826                        'post_content' => wp_json_encode( $data, $json_options ),
2827                );
2828                if ( $args['title'] ) {
2829                        $post_array['post_title'] = $args['title'];
2830                }
2831                if ( $changeset_post_id ) {
2832                        $post_array['ID'] = $changeset_post_id;
2833                } else {
2834                        $post_array['post_type'] = 'customize_changeset';
2835                        $post_array['post_name'] = $this->changeset_uuid();
2836                        $post_array['post_status'] = 'auto-draft';
2837                }
2838                if ( $args['status'] ) {
2839                        $post_array['post_status'] = $args['status'];
2840                }
2841
2842                // Reset post date to now if we are publishing, otherwise pass post_date_gmt and translate for post_date.
2843                if ( 'publish' === $args['status'] ) {
2844                        $post_array['post_date_gmt'] = '0000-00-00 00:00:00';
2845                        $post_array['post_date'] = '0000-00-00 00:00:00';
2846                } elseif ( $args['date_gmt'] ) {
2847                        $post_array['post_date_gmt'] = $args['date_gmt'];
2848                        $post_array['post_date'] = get_date_from_gmt( $args['date_gmt'] );
2849                } elseif ( $changeset_post_id && 'auto-draft' === get_post_status( $changeset_post_id ) ) {
2850                        /*
2851                         * Keep bumping the date for the auto-draft whenever it is modified;
2852                         * this extends its life, preserving it from garbage-collection via
2853                         * wp_delete_auto_drafts().
2854                         */
2855                        $post_array['post_date'] = current_time( 'mysql' );
2856                        $post_array['post_date_gmt'] = '';
2857                }
2858
2859                $this->store_changeset_revision = $allow_revision;
2860                add_filter( 'wp_save_post_revision_post_has_changed', array( $this, '_filter_revision_post_has_changed' ), 5, 3 );
2861
2862                // Update the changeset post. The publish_customize_changeset action will cause the settings in the changeset to be saved via WP_Customize_Setting::save().
2863                $has_kses = ( false !== has_filter( 'content_save_pre', 'wp_filter_post_kses' ) );
2864                if ( $has_kses ) {
2865                        kses_remove_filters(); // Prevent KSES from corrupting JSON in post_content.
2866                }
2867
2868                // Note that updating a post with publish status will trigger WP_Customize_Manager::publish_changeset_values().
2869                if ( $changeset_post_id ) {
2870                        if ( $args['autosave'] && 'auto-draft' !== get_post_status( $changeset_post_id ) ) {
2871                                // See _wp_translate_postdata() for why this is required as it will use the edit_post meta capability.
2872                                add_filter( 'map_meta_cap', array( $this, 'grant_edit_post_capability_for_changeset' ), 10, 4 );
2873                                $post_array['post_ID'] = $post_array['ID'];
2874                                $post_array['post_type'] = 'customize_changeset';
2875                                $r = wp_create_post_autosave( wp_slash( $post_array ) );
2876                                remove_filter( 'map_meta_cap', array( $this, 'grant_edit_post_capability_for_changeset' ), 10 );
2877                        } else {
2878                                $post_array['edit_date'] = true; // Prevent date clearing.
2879                                $r = wp_update_post( wp_slash( $post_array ), true );
2880
2881                                // Delete autosave revision for user when the changeset is updated.
2882                                if ( ! empty( $args['user_id'] ) ) {
2883                                        $autosave_draft = wp_get_post_autosave( $changeset_post_id, $args['user_id'] );
2884                                        if ( $autosave_draft ) {
2885                                                wp_delete_post( $autosave_draft->ID, true );
2886                                        }
2887                                }
2888                        }
2889                } else {
2890                        $r = wp_insert_post( wp_slash( $post_array ), true );
2891                        if ( ! is_wp_error( $r ) ) {
2892                                $this->_changeset_post_id = $r; // Update cached post ID for the loaded changeset.
2893                        }
2894                }
2895                if ( $has_kses ) {
2896                        kses_init_filters();
2897                }
2898                $this->_changeset_data = null; // Reset so WP_Customize_Manager::changeset_data() will re-populate with updated contents.
2899
2900                remove_filter( 'wp_save_post_revision_post_has_changed', array( $this, '_filter_revision_post_has_changed' ) );
2901
2902                $response = array(
2903                        'setting_validities' => $setting_validities,
2904                );
2905
2906                if ( is_wp_error( $r ) ) {
2907                        $response['changeset_post_save_failure'] = $r->get_error_code();
2908                        return new WP_Error( 'changeset_post_save_failure', '', $response );
2909                }
2910
2911                return $response;
2912        }
2913
2914        /**
2915         * Trash or delete a changeset post.
2916         *
2917         * The following re-formulates the logic from `wp_trash_post()` as done in
2918         * `wp_publish_post()`. The reason for bypassing `wp_trash_post()` is that it
2919         * will mutate the the `post_content` and the `post_name` when they should be
2920         * untouched.
2921         *
2922         * @since 4.9.0
2923         * @global wpdb $wpdb WordPress database abstraction object.
2924         * @see wp_trash_post()
2925         *
2926         * @param int|WP_Post $post The changeset post.
2927         * @return mixed A WP_Post object for the trashed post or an empty value on failure.
2928         */
2929        public function trash_changeset_post( $post ) {
2930                global $wpdb;
2931
2932                $post = get_post( $post );
2933
2934                if ( ! ( $post instanceof WP_Post ) ) {
2935                        return $post;
2936                }
2937                $post_id = $post->ID;
2938
2939                if ( ! EMPTY_TRASH_DAYS ) {
2940                        return wp_delete_post( $post_id, true );
2941                }
2942
2943                if ( 'trash' === get_post_status( $post ) ) {
2944                        return false;
2945                }
2946
2947                /** This filter is documented in wp-includes/post.php */
2948                $check = apply_filters( 'pre_trash_post', null, $post );
2949                if ( null !== $check ) {
2950                        return $check;
2951                }
2952
2953                /** This action is documented in wp-includes/post.php */
2954                do_action( 'wp_trash_post', $post_id );
2955
2956                add_post_meta( $post_id, '_wp_trash_meta_status', $post->post_status );
2957                add_post_meta( $post_id, '_wp_trash_meta_time', time() );
2958
2959                $old_status = $post->post_status;
2960                $new_status = 'trash';
2961                $wpdb->update( $wpdb->posts, array( 'post_status' => $new_status ), array( 'ID' => $post->ID ) );
2962                clean_post_cache( $post->ID );
2963
2964                $post->post_status = $new_status;
2965                wp_transition_post_status( $new_status, $old_status, $post );
2966
2967                /** This action is documented in wp-includes/post.php */
2968                do_action( 'edit_post', $post->ID, $post );
2969
2970                /** This action is documented in wp-includes/post.php */
2971                do_action( "save_post_{$post->post_type}", $post->ID, $post, true );
2972
2973                /** This action is documented in wp-includes/post.php */
2974                do_action( 'save_post', $post->ID, $post, true );
2975
2976                /** This action is documented in wp-includes/post.php */
2977                do_action( 'wp_insert_post', $post->ID, $post, true );
2978
2979                wp_trash_post_comments( $post_id );
2980
2981                /** This action is documented in wp-includes/post.php */
2982                do_action( 'trashed_post', $post_id );
2983
2984                return $post;
2985        }
2986
2987        /**
2988         * Handle request to trash a changeset.
2989         *
2990         * @since 4.9.0
2991         */
2992        public function handle_changeset_trash_request() {
2993                if ( ! is_user_logged_in() ) {
2994                        wp_send_json_error( 'unauthenticated' );
2995                }
2996
2997                if ( ! $this->is_preview() ) {
2998                        wp_send_json_error( 'not_preview' );
2999                }
3000
3001                if ( ! check_ajax_referer( 'trash_customize_changeset', 'nonce', false ) ) {
3002                        wp_send_json_error( array(
3003                                'code' => 'invalid_nonce',
3004                                'message' => __( 'There was an authentication problem. Please reload and try again.' ),
3005                        ) );
3006                }
3007
3008                $changeset_post_id = $this->changeset_post_id();
3009
3010                if ( ! $changeset_post_id ) {
3011                        wp_send_json_error( array(
3012                                'message' => __( 'No changes saved yet, so there is nothing to trash.' ),
3013                                'code' => 'non_existent_changeset',
3014                        ) );
3015                        return;
3016                }
3017
3018                if ( $changeset_post_id && ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->delete_post, $changeset_post_id ) ) {
3019                        wp_send_json_error( array(
3020                                'code' => 'changeset_trash_unauthorized',
3021                                'message' => __( 'Unable to trash changes.' ),
3022                        ) );
3023                }
3024
3025                if ( 'trash' === get_post_status( $changeset_post_id ) ) {
3026                        wp_send_json_error( array(
3027                                'message' => __( 'Changes have already been trashed.' ),
3028                                'code' => 'changeset_already_trashed',
3029                        ) );
3030                        return;
3031                }
3032
3033                $r = $this->trash_changeset_post( $changeset_post_id );
3034                if ( ! ( $r instanceof WP_Post ) ) {
3035                        wp_send_json_error( array(
3036                                'code' => 'changeset_trash_failure',
3037                                'message' => __( 'Unable to trash changes.' ),
3038                        ) );
3039                }
3040
3041                wp_send_json_success( array(
3042                        'message' => __( 'Changes trashed successfully.' ),
3043                ) );
3044        }
3045
3046        /**
3047         * Re-map 'edit_post' meta cap for a customize_changeset post to be the same as 'customize' maps.
3048         *
3049         * There is essentially a "meta meta" cap in play here, where 'edit_post' meta cap maps to
3050         * the 'customize' meta cap which then maps to 'edit_theme_options'. This is currently
3051         * required in core for `wp_create_post_autosave()` because it will call
3052         * `_wp_translate_postdata()` which in turn will check if a user can 'edit_post', but the
3053         * the caps for the customize_changeset post type are all mapping to the meta capability.
3054         * This should be able to be removed once #40922 is addressed in core.
3055         *
3056         * @since 4.9.0
3057         * @link https://core.trac.wordpress.org/ticket/40922
3058         * @see WP_Customize_Manager::save_changeset_post()
3059         * @see _wp_translate_postdata()
3060         *
3061         * @param array  $caps    Returns the user's actual capabilities.
3062         * @param string $cap     Capability name.
3063         * @param int    $user_id The user ID.
3064         * @param array  $args    Adds the context to the cap. Typically the object ID.
3065         * @return array Capabilities.
3066         */
3067        public function grant_edit_post_capability_for_changeset( $caps, $cap, $user_id, $args ) {
3068                if ( 'edit_post' === $cap && ! empty( $args[0] ) && 'customize_changeset' === get_post_type( $args[0] ) ) {
3069                        $post_type_obj = get_post_type_object( 'customize_changeset' );
3070                        $caps = map_meta_cap( $post_type_obj->cap->$cap, $user_id );
3071                }
3072                return $caps;
3073        }
3074
3075        /**
3076         * Marks the changeset post as being currently edited by the current user.
3077         *
3078         * @since 4.9.0
3079         *
3080         * @param int  $changeset_post_id Changeset post id.
3081         * @param bool $take_over Take over the changeset, default is false.
3082         */
3083        public function set_changeset_lock( $changeset_post_id, $take_over = false ) {
3084                if ( $changeset_post_id ) {
3085                        $can_override = ! (bool) get_post_meta( $changeset_post_id, '_edit_lock', true );
3086
3087                        if ( $take_over ) {
3088                                $can_override = true;
3089                        }
3090
3091                        if ( $can_override ) {
3092                                $lock = sprintf( '%s:%s', time(), get_current_user_id() );
3093                                update_post_meta( $changeset_post_id, '_edit_lock', $lock );
3094                        } else {
3095                                $this->refresh_changeset_lock( $changeset_post_id );
3096                        }
3097                }
3098        }
3099
3100        /**
3101         * Refreshes changeset lock with the current time if current user edited the changeset before.
3102         *
3103         * @since 4.9.0
3104         *
3105         * @param int $changeset_post_id Changeset post id.
3106         */
3107        public function refresh_changeset_lock( $changeset_post_id ) {
3108                if ( ! $changeset_post_id ) {
3109                        return;
3110                }
3111                $lock = get_post_meta( $changeset_post_id, '_edit_lock', true );
3112                $lock = explode( ':', $lock );
3113
3114                if ( $lock && ! empty( $lock[1] ) ) {
3115                        $user_id = intval( $lock[1] );
3116                        $current_user_id = get_current_user_id();
3117                        if ( $user_id === $current_user_id ) {
3118                                $lock = sprintf( '%s:%s', time(), $user_id );
3119                                update_post_meta( $changeset_post_id, '_edit_lock', $lock );
3120                        }
3121                }
3122        }
3123
3124        /**
3125         * Filter heartbeat settings for the Customizer.
3126         *
3127         * @since 4.9.0
3128         * @param array $settings Current settings to filter.
3129         * @return array Heartbeat settings.
3130         */
3131        public function add_customize_screen_to_heartbeat_settings( $settings ) {
3132                global $pagenow;
3133                if ( 'customize.php' === $pagenow ) {
3134                        $settings['screenId'] = 'customize';
3135                }
3136                return $settings;
3137        }
3138
3139        /**
3140         * Get lock user data.
3141         *
3142         * @since 4.9.0
3143         *
3144         * @param int $user_id User ID.
3145         * @return array|null User data formatted for client.
3146         */
3147        protected function get_lock_user_data( $user_id ) {
3148                if ( ! $user_id ) {
3149                        return null;
3150                }
3151                $lock_user = get_userdata( $user_id );
3152                if ( ! $lock_user ) {
3153                        return null;
3154                }
3155                return array(
3156                        'id' => $lock_user->ID,
3157                        'name' => $lock_user->display_name,
3158                        'avatar' => get_avatar_url( $lock_user->ID, array( 'size' => 128 ) ),
3159                );
3160        }
3161
3162        /**
3163         * Check locked changeset with heartbeat API.
3164         *
3165         * @since 4.9.0
3166         *
3167         * @param array  $response  The Heartbeat response.
3168         * @param array  $data      The $_POST data sent.
3169         * @param string $screen_id The screen id.
3170         * @return array The Heartbeat response.
3171         */
3172        public function check_changeset_lock_with_heartbeat( $response, $data, $screen_id ) {
3173                if ( isset( $data['changeset_uuid'] ) ) {
3174                        $changeset_post_id = $this->find_changeset_post_id( $data['changeset_uuid'] );
3175                } else {
3176                        $changeset_post_id = $this->changeset_post_id();
3177                }
3178
3179                if (
3180                        array_key_exists( 'check_changeset_lock', $data )
3181                        && 'customize' === $screen_id
3182                        && $changeset_post_id
3183                        && current_user_can( get_post_type_object( 'customize_changeset' )->cap->edit_post, $changeset_post_id )
3184                ) {
3185                        $lock_user_id = wp_check_post_lock( $changeset_post_id );
3186
3187                        if ( $lock_user_id ) {
3188                                $response['customize_changeset_lock_user'] = $this->get_lock_user_data( $lock_user_id );
3189                        } else {
3190
3191                                // Refreshing time will ensure that the user is sitting on customizer and has not closed the customizer tab.
3192                                $this->refresh_changeset_lock( $changeset_post_id );
3193                        }
3194                }
3195
3196                return $response;
3197        }
3198
3199        /**
3200         * Removes changeset lock when take over request is sent via Ajax.
3201         *
3202         * @since 4.9.0
3203         */
3204        public function handle_override_changeset_lock_request() {
3205                if ( ! $this->is_preview() ) {
3206                        wp_send_json_error( 'not_preview', 400 );
3207                }
3208
3209                if ( ! check_ajax_referer( 'customize_override_changeset_lock', 'nonce', false ) ) {
3210                        wp_send_json_error( array(
3211                                'code' => 'invalid_nonce',
3212                                'message' => __( 'Security check failed.' ),
3213                        ) );
3214                }
3215
3216                $changeset_post_id = $this->changeset_post_id();
3217
3218                if ( empty( $changeset_post_id ) ) {
3219                        wp_send_json_error( array(
3220                                'code' => 'no_changeset_found_to_take_over',
3221                                'message' => __( 'No changeset found to take over' ),
3222                        ) );
3223                }
3224
3225                if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->edit_post, $changeset_post_id ) ) {
3226                        wp_send_json_error( array(
3227                                'code' => 'cannot_remove_changeset_lock',
3228                                'message' => __( 'Sorry, you are not allowed to take over.' ),
3229                        ) );
3230                }
3231
3232                $this->set_changeset_lock( $changeset_post_id, true );
3233
3234                wp_send_json_success( 'changeset_taken_over' );
3235        }
3236
3237        /**
3238         * Whether a changeset revision should be made.
3239         *
3240         * @since 4.7.0
3241         * @var bool
3242         */
3243        protected $store_changeset_revision;
3244
3245        /**
3246         * Filters whether a changeset has changed to create a new revision.
3247         *
3248         * Note that this will not be called while a changeset post remains in auto-draft status.
3249         *
3250         * @since 4.7.0
3251         *
3252         * @param bool    $post_has_changed Whether the post has changed.
3253         * @param WP_Post $last_revision    The last revision post object.
3254         * @param WP_Post $post             The post object.
3255         *
3256         * @return bool Whether a revision should be made.
3257         */
3258        public function _filter_revision_post_has_changed( $post_has_changed, $last_revision, $post ) {
3259                unset( $last_revision );
3260                if ( 'customize_changeset' === $post->post_type ) {
3261                        $post_has_changed = $this->store_changeset_revision;
3262                }
3263                return $post_has_changed;
3264        }
3265
3266        /**
3267         * Publish changeset values.
3268         *
3269         * This will the values contained in a changeset, even changesets that do not
3270         * correspond to current manager instance. This is called by
3271         * `_wp_customize_publish_changeset()` when a customize_changeset post is
3272         * transitioned to the `publish` status. As such, this method should not be
3273         * called directly and instead `wp_publish_post()` should be used.
3274         *
3275         * Please note that if the settings in the changeset are for a non-activated
3276         * theme, the theme must first be switched to (via `switch_theme()`) before
3277         * invoking this method.
3278         *
3279         * @since 4.7.0
3280         * @see _wp_customize_publish_changeset()
3281         * @global wpdb $wpdb
3282         *
3283         * @param int $changeset_post_id ID for customize_changeset post. Defaults to the changeset for the current manager instance.
3284         * @return true|WP_Error True or error info.
3285         */
3286        public function _publish_changeset_values( $changeset_post_id ) {
3287                global $wpdb;
3288
3289                $publishing_changeset_data = $this->get_changeset_post_data( $changeset_post_id );
3290                if ( is_wp_error( $publishing_changeset_data ) ) {
3291                        return $publishing_changeset_data;
3292                }
3293
3294                $changeset_post = get_post( $changeset_post_id );
3295
3296                /*
3297                 * Temporarily override the changeset context so that it will be read
3298                 * in calls to unsanitized_post_values() and so that it will be available
3299                 * on the $wp_customize object passed to hooks during the save logic.
3300                 */
3301                $previous_changeset_post_id = $this->_changeset_post_id;
3302                $this->_changeset_post_id   = $changeset_post_id;
3303                $previous_changeset_uuid    = $this->_changeset_uuid;
3304                $this->_changeset_uuid      = $changeset_post->post_name;
3305                $previous_changeset_data    = $this->_changeset_data;
3306                $this->_changeset_data      = $publishing_changeset_data;
3307
3308                // Parse changeset data to identify theme mod settings and user IDs associated with settings to be saved.
3309                $setting_user_ids = array();
3310                $theme_mod_settings = array();
3311                $namespace_pattern = '/^(?P<stylesheet>.+?)::(?P<setting_id>.+)$/';
3312                $matches = array();
3313                foreach ( $this->_changeset_data as $raw_setting_id => $setting_params ) {
3314                        $actual_setting_id = null;
3315                        $is_theme_mod_setting = (
3316                                isset( $setting_params['value'] )
3317                                &&
3318                                isset( $setting_params['type'] )
3319                                &&
3320                                'theme_mod' === $setting_params['type']
3321                                &&
3322                                preg_match( $namespace_pattern, $raw_setting_id, $matches )
3323                        );
3324                        if ( $is_theme_mod_setting ) {
3325                                if ( ! isset( $theme_mod_settings[ $matches['stylesheet'] ] ) ) {
3326                                        $theme_mod_settings[ $matches['stylesheet'] ] = array();
3327                                }
3328                                $theme_mod_settings[ $matches['stylesheet'] ][ $matches['setting_id'] ] = $setting_params;
3329
3330                                if ( $this->get_stylesheet() === $matches['stylesheet'] ) {
3331                                        $actual_setting_id = $matches['setting_id'];
3332                                }
3333                        } else {
3334                                $actual_setting_id = $raw_setting_id;
3335                        }
3336
3337                        // Keep track of the user IDs for settings actually for this theme.
3338                        if ( $actual_setting_id && isset( $setting_params['user_id'] ) ) {
3339                                $setting_user_ids[ $actual_setting_id ] = $setting_params['user_id'];
3340                        }
3341                }
3342
3343                $changeset_setting_values = $this->unsanitized_post_values( array(
3344                        'exclude_post_data' => true,
3345                        'exclude_changeset' => false,
3346                ) );
3347                $changeset_setting_ids = array_keys( $changeset_setting_values );
3348                $this->add_dynamic_settings( $changeset_setting_ids );
3349
3350                /**
3351                 * Fires once the theme has switched in the Customizer, but before settings
3352                 * have been saved.
3353                 *
3354                 * @since 3.4.0
3355                 *
3356                 * @param WP_Customize_Manager $manager WP_Customize_Manager instance.
3357                 */
3358                do_action( 'customize_save', $this );
3359
3360                /*
3361                 * Ensure that all settings will allow themselves to be saved. Note that
3362                 * this is safe because the setting would have checked the capability
3363                 * when the setting value was written into the changeset. So this is why
3364                 * an additional capability check is not required here.
3365                 */
3366                $original_setting_capabilities = array();
3367                foreach ( $changeset_setting_ids as $setting_id ) {
3368                        $setting = $this->get_setting( $setting_id );
3369                        if ( $setting && ! isset( $setting_user_ids[ $setting_id ] ) ) {
3370                                $original_setting_capabilities[ $setting->id ] = $setting->capability;
3371                                $setting->capability = 'exist';
3372                        }
3373                }
3374
3375                $original_user_id = get_current_user_id();
3376                foreach ( $changeset_setting_ids as $setting_id ) {
3377                        $setting = $this->get_setting( $setting_id );
3378                        if ( $setting ) {
3379                                /*
3380                                 * Set the current user to match the user who saved the value into
3381                                 * the changeset so that any filters that apply during the save
3382                                 * process will respect the original user's capabilities. This
3383                                 * will ensure, for example, that KSES won't strip unsafe HTML
3384                                 * when a scheduled changeset publishes via WP Cron.
3385                                 */
3386                                if ( isset( $setting_user_ids[ $setting_id ] ) ) {
3387                                        wp_set_current_user( $setting_user_ids[ $setting_id ] );
3388                                } else {
3389                                        wp_set_current_user( $original_user_id );
3390                                }
3391
3392                                $setting->save();
3393                        }
3394                }
3395                wp_set_current_user( $original_user_id );
3396
3397                // Update the stashed theme mod settings, removing the active theme's stashed settings, if activated.
3398                if ( did_action( 'switch_theme' ) ) {
3399                        $other_theme_mod_settings = $theme_mod_settings;
3400                        unset( $other_theme_mod_settings[ $this->get_stylesheet() ] );
3401                        $this->update_stashed_theme_mod_settings( $other_theme_mod_settings );
3402                }
3403
3404                /**
3405                 * Fires after Customize settings have been saved.
3406                 *
3407                 * @since 3.6.0
3408                 *
3409                 * @param WP_Customize_Manager $manager WP_Customize_Manager instance.
3410                 */
3411                do_action( 'customize_save_after', $this );
3412
3413                // Restore original capabilities.
3414                foreach ( $original_setting_capabilities as $setting_id => $capability ) {
3415                        $setting = $this->get_setting( $setting_id );
3416                        if ( $setting ) {
3417                                $setting->capability = $capability;
3418                        }
3419                }
3420
3421                // Restore original changeset data.
3422                $this->_changeset_data    = $previous_changeset_data;
3423                $this->_changeset_post_id = $previous_changeset_post_id;
3424                $this->_changeset_uuid    = $previous_changeset_uuid;
3425
3426                /*
3427                 * Convert all autosave revisions into their own auto-drafts so that users can be prompted to
3428                 * restore them when a changeset is published, but they had been locked out from including
3429                 * their changes in the changeset.
3430                 */
3431                $revisions = wp_get_post_revisions( $changeset_post_id, array( 'check_enabled' => false ) );
3432                foreach ( $revisions as $revision ) {
3433                        if ( false !== strpos( $revision->post_name, "{$changeset_post_id}-autosave" ) ) {
3434                                $wpdb->update(
3435                                        $wpdb->posts,
3436                                        array(
3437                                                'post_status' => 'auto-draft',
3438                                                'post_type' => 'customize_changeset',
3439                                                'post_name' => wp_generate_uuid4(),
3440                                                'post_parent' => 0,
3441                                        ),
3442                                        array(
3443                                                'ID' => $revision->ID,
3444                                        )
3445                                );
3446                                clean_post_cache( $revision->ID );
3447                        }
3448                }
3449
3450                return true;
3451        }
3452
3453        /**
3454         * Update stashed theme mod settings.
3455         *
3456         * @since 4.7.0
3457         *
3458         * @param array $inactive_theme_mod_settings Mapping of stylesheet to arrays of theme mod settings.
3459         * @return array|false Returns array of updated stashed theme mods or false if the update failed or there were no changes.
3460         */
3461        protected function update_stashed_theme_mod_settings( $inactive_theme_mod_settings ) {
3462                $stashed_theme_mod_settings = get_option( 'customize_stashed_theme_mods' );
3463                if ( empty( $stashed_theme_mod_settings ) ) {
3464                        $stashed_theme_mod_settings = array();
3465                }
3466
3467                // Delete any stashed theme mods for the active theme since they would have been loaded and saved upon activation.
3468                unset( $stashed_theme_mod_settings[ $this->get_stylesheet() ] );
3469
3470                // Merge inactive theme mods with the stashed theme mod settings.
3471                foreach ( $inactive_theme_mod_settings as $stylesheet => $theme_mod_settings ) {
3472                        if ( ! isset( $stashed_theme_mod_settings[ $stylesheet ] ) ) {
3473                                $stashed_theme_mod_settings[ $stylesheet ] = array();
3474                        }
3475
3476                        $stashed_theme_mod_settings[ $stylesheet ] = array_merge(
3477                                $stashed_theme_mod_settings[ $stylesheet ],
3478                                $theme_mod_settings
3479                        );
3480                }
3481
3482                $autoload = false;
3483                $result = update_option( 'customize_stashed_theme_mods', $stashed_theme_mod_settings, $autoload );
3484                if ( ! $result ) {
3485                        return false;
3486                }
3487                return $stashed_theme_mod_settings;
3488        }
3489
3490        /**
3491         * Refresh nonces for the current preview.
3492         *
3493         * @since 4.2.0
3494         */
3495        public function refresh_nonces() {
3496                if ( ! $this->is_preview() ) {
3497                        wp_send_json_error( 'not_preview' );
3498                }
3499
3500                wp_send_json_success( $this->get_nonces() );
3501        }
3502
3503        /**
3504         * Delete a given auto-draft changeset or the autosave revision for a given changeset or delete changeset lock.
3505         *
3506         * @since 4.9.0
3507         */
3508        public function handle_dismiss_autosave_or_lock_request() {
3509                // Calls to dismiss_user_auto_draft_changesets() and wp_get_post_autosave() require non-zero get_current_user_id().
3510                if ( ! is_user_logged_in() ) {
3511                        wp_send_json_error( 'unauthenticated', 401 );
3512                }
3513
3514                if ( ! $this->is_preview() ) {
3515                        wp_send_json_error( 'not_preview', 400 );
3516                }
3517
3518                if ( ! check_ajax_referer( 'customize_dismiss_autosave_or_lock', 'nonce', false ) ) {
3519                        wp_send_json_error( 'invalid_nonce', 403 );
3520                }
3521
3522                $changeset_post_id = $this->changeset_post_id();
3523                $dismiss_lock = ! empty( $_POST['dismiss_lock'] );
3524                $dismiss_autosave = ! empty( $_POST['dismiss_autosave'] );
3525
3526                if ( $dismiss_lock ) {
3527                        if ( empty( $changeset_post_id ) && ! $dismiss_autosave ) {
3528                                wp_send_json_error( 'no_changeset_to_dismiss_lock', 404 );
3529                        }
3530                        if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->edit_post, $changeset_post_id ) && ! $dismiss_autosave ) {
3531                                wp_send_json_error( 'cannot_remove_changeset_lock', 403 );
3532                        }
3533
3534                        delete_post_meta( $changeset_post_id, '_edit_lock' );
3535
3536                        if ( ! $dismiss_autosave ) {
3537                                wp_send_json_success( 'changeset_lock_dismissed' );
3538                        }
3539                }
3540
3541                if ( $dismiss_autosave ) {
3542                        if ( empty( $changeset_post_id ) || 'auto-draft' === get_post_status( $changeset_post_id ) ) {
3543                                $dismissed = $this->dismiss_user_auto_draft_changesets();
3544                                if ( $dismissed > 0 ) {
3545                                        wp_send_json_success( 'auto_draft_dismissed' );
3546                                } else {
3547                                        wp_send_json_error( 'no_auto_draft_to_delete', 404 );
3548                                }
3549                        } else {
3550                                $revision = wp_get_post_autosave( $changeset_post_id, get_current_user_id() );
3551
3552                                if ( $revision ) {
3553                                        if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->delete_post, $changeset_post_id ) ) {
3554                                                wp_send_json_error( 'cannot_delete_autosave_revision', 403 );
3555                                        }
3556
3557                                        if ( ! wp_delete_post( $revision->ID, true ) ) {
3558                                                wp_send_json_error( 'autosave_revision_deletion_failure', 500 );
3559                                        } else {
3560                                                wp_send_json_success( 'autosave_revision_deleted' );
3561                                        }
3562                                } else {
3563                                        wp_send_json_error( 'no_autosave_revision_to_delete', 404 );
3564                                }
3565                        }
3566                }
3567
3568                wp_send_json_error( 'unknown_error', 500 );
3569        }
3570
3571        /**
3572         * Add a customize setting.
3573         *
3574         * @since 3.4.0
3575         * @since 4.5.0 Return added WP_Customize_Setting instance.
3576         *
3577         * @param WP_Customize_Setting|string $id   Customize Setting object, or ID.
3578         * @param array                       $args {
3579         *  Optional. Array of properties for the new WP_Customize_Setting. Default empty array.
3580         *
3581         *  @type string       $type                  Type of the setting. Default 'theme_mod'.
3582         *                                            Default 160.
3583         *  @type string       $capability            Capability required for the setting. Default 'edit_theme_options'
3584         *  @type string|array $theme_supports        Theme features required to support the panel. Default is none.
3585         *  @type string       $default               Default value for the setting. Default is empty string.
3586         *  @type string       $transport             Options for rendering the live preview of changes in Theme Customizer.
3587         *                                            Using 'refresh' makes the change visible by reloading the whole preview.
3588         *                                            Using 'postMessage' allows a custom JavaScript to handle live changes.
3589         *                                            @link https://developer.wordpress.org/themes/customize-api
3590         *                                            Default is 'refresh'
3591         *  @type callable     $validate_callback     Server-side validation callback for the setting's value.
3592         *  @type callable     $sanitize_callback     Callback to filter a Customize setting value in un-slashed form.
3593         *  @type callable     $sanitize_js_callback  Callback to convert a Customize PHP setting value to a value that is
3594         *                                            JSON serializable.
3595         *  @type bool         $dirty                 Whether or not the setting is initially dirty when created.
3596         * }
3597         * @return WP_Customize_Setting             The instance of the setting that was added.
3598         */
3599        public function add_setting( $id, $args = array() ) {
3600                if ( $id instanceof WP_Customize_Setting ) {
3601                        $setting = $id;
3602                } else {
3603                        $class = 'WP_Customize_Setting';
3604
3605                        /** This filter is documented in wp-includes/class-wp-customize-manager.php */
3606                        $args = apply_filters( 'customize_dynamic_setting_args', $args, $id );
3607
3608                        /** This filter is documented in wp-includes/class-wp-customize-manager.php */
3609                        $class = apply_filters( 'customize_dynamic_setting_class', $class, $id, $args );
3610
3611                        $setting = new $class( $this, $id, $args );
3612                }
3613
3614                $this->settings[ $setting->id ] = $setting;
3615                return $setting;
3616        }
3617
3618        /**
3619         * Register any dynamically-created settings, such as those from $_POST['customized']
3620         * that have no corresponding setting created.
3621         *
3622         * This is a mechanism to "wake up" settings that have been dynamically created
3623         * on the front end and have been sent to WordPress in `$_POST['customized']`. When WP
3624         * loads, the dynamically-created settings then will get created and previewed
3625         * even though they are not directly created statically with code.
3626         *
3627         * @since 4.2.0
3628         *
3629         * @param array $setting_ids The setting IDs to add.
3630         * @return array The WP_Customize_Setting objects added.
3631         */
3632        public function add_dynamic_settings( $setting_ids ) {
3633                $new_settings = array();
3634                foreach ( $setting_ids as $setting_id ) {
3635                        // Skip settings already created
3636                        if ( $this->get_setting( $setting_id ) ) {
3637                                continue;
3638                        }
3639
3640                        $setting_args = false;
3641                        $setting_class = 'WP_Customize_Setting';
3642
3643                        /**
3644                         * Filters a dynamic setting's constructor args.
3645                         *
3646                         * For a dynamic setting to be registered, this filter must be employed
3647                         * to override the default false value with an array of args to pass to
3648                         * the WP_Customize_Setting constructor.
3649                         *
3650                         * @since 4.2.0
3651                         *
3652                         * @param false|array $setting_args The arguments to the WP_Customize_Setting constructor.
3653                         * @param string      $setting_id   ID for dynamic setting, usually coming from `$_POST['customized']`.
3654                         */
3655                        $setting_args = apply_filters( 'customize_dynamic_setting_args', $setting_args, $setting_id );
3656                        if ( false === $setting_args ) {
3657                                continue;
3658                        }
3659
3660                        /**
3661                         * Allow non-statically created settings to be constructed with custom WP_Customize_Setting subclass.
3662                         *
3663                         * @since 4.2.0
3664                         *
3665                         * @param string $setting_class WP_Customize_Setting or a subclass.
3666                         * @param string $setting_id    ID for dynamic setting, usually coming from `$_POST['customized']`.
3667                         * @param array  $setting_args  WP_Customize_Setting or a subclass.
3668                         */
3669                        $setting_class = apply_filters( 'customize_dynamic_setting_class', $setting_class, $setting_id, $setting_args );
3670
3671                        $setting = new $setting_class( $this, $setting_id, $setting_args );
3672
3673                        $this->add_setting( $setting );
3674                        $new_settings[] = $setting;
3675                }
3676                return $new_settings;
3677        }
3678
3679        /**
3680         * Retrieve a customize setting.
3681         *
3682         * @since 3.4.0
3683         *
3684         * @param string $id Customize Setting ID.
3685         * @return WP_Customize_Setting|void The setting, if set.
3686         */
3687        public function get_setting( $id ) {
3688                if ( isset( $this->settings[ $id ] ) ) {
3689                        return $this->settings[ $id ];
3690                }
3691        }
3692
3693        /**
3694         * Remove a customize setting.
3695         *
3696         * @since 3.4.0
3697         *
3698         * @param string $id Customize Setting ID.
3699         */
3700        public function remove_setting( $id ) {
3701                unset( $this->settings[ $id ] );
3702        }
3703
3704        /**
3705         * Add a customize panel.
3706         *
3707         * @since 4.0.0
3708         * @since 4.5.0 Return added WP_Customize_Panel instance.
3709         *
3710         * @param WP_Customize_Panel|string $id   Customize Panel object, or Panel ID.
3711         * @param array                     $args {
3712         *  Optional. Array of properties for the new Panel object. Default empty array.
3713         *  @type int          $priority              Priority of the panel, defining the display order of panels and sections.
3714         *                                            Default 160.
3715         *  @type string       $capability            Capability required for the panel. Default `edit_theme_options`
3716         *  @type string|array $theme_supports        Theme features required to support the panel.
3717         *  @type string       $title                 Title of the panel to show in UI.
3718         *  @type string       $description           Description to show in the UI.
3719         *  @type string       $type                  Type of the panel.
3720         *  @type callable     $active_callback       Active callback.
3721         * }
3722         * @return WP_Customize_Panel             The instance of the panel that was added.
3723         */
3724        public function add_panel( $id, $args = array() ) {
3725                if ( $id instanceof WP_Customize_Panel ) {
3726                        $panel = $id;
3727                } else {
3728                        $panel = new WP_Customize_Panel( $this, $id, $args );
3729                }
3730
3731                $this->panels[ $panel->id ] = $panel;
3732                return $panel;
3733        }
3734
3735        /**
3736         * Retrieve a customize panel.
3737         *
3738         * @since 4.0.0
3739         *
3740         * @param string $id Panel ID to get.
3741         * @return WP_Customize_Panel|void Requested panel instance, if set.
3742         */
3743        public function get_panel( $id ) {
3744                if ( isset( $this->panels[ $id ] ) ) {
3745                        return $this->panels[ $id ];
3746                }
3747        }
3748
3749        /**
3750         * Remove a customize panel.
3751         *
3752         * @since 4.0.0
3753         *
3754         * @param string $id Panel ID to remove.
3755         */
3756        public function remove_panel( $id ) {
3757                // Removing core components this way is _doing_it_wrong().
3758                if ( in_array( $id, $this->components, true ) ) {
3759                        /* translators: 1: panel id, 2: link to 'customize_loaded_components' filter reference */
3760                        $message = sprintf( __( 'Removing %1$s manually will cause PHP warnings. Use the %2$s filter instead.' ),
3761                                $id,
3762                                '<a href="' . esc_url( 'https://developer.wordpress.org/reference/hooks/customize_loaded_components/' ) . '"><code>customize_loaded_components</code></a>'
3763                        );
3764
3765                        _doing_it_wrong( __METHOD__, $message, '4.5.0' );
3766                }
3767                unset( $this->panels[ $id ] );
3768        }
3769
3770        /**
3771         * Register a customize panel type.
3772         *
3773         * Registered types are eligible to be rendered via JS and created dynamically.
3774         *
3775         * @since 4.3.0
3776         *
3777         * @see WP_Customize_Panel
3778         *
3779         * @param string $panel Name of a custom panel which is a subclass of WP_Customize_Panel.
3780         */
3781        public function register_panel_type( $panel ) {
3782                $this->registered_panel_types[] = $panel;
3783        }
3784
3785        /**
3786         * Render JS templates for all registered panel types.
3787         *
3788         * @since 4.3.0
3789         */
3790        public function render_panel_templates() {
3791                foreach ( $this->registered_panel_types as $panel_type ) {
3792                        $panel = new $panel_type( $this, 'temp', array() );
3793                        $panel->print_template();
3794                }
3795        }
3796
3797        /**
3798         * Add a customize section.
3799         *
3800         * @since 3.4.0
3801         * @since 4.5.0 Return added WP_Customize_Section instance.
3802         *
3803         * @param WP_Customize_Section|string $id   Customize Section object, or Section ID.
3804         * @param array                     $args {
3805         *  Optional. Array of properties for the new Section object. Default empty array.
3806         *  @type int          $priority              Priority of the section, defining the display order of panels and sections.
3807         *                                            Default 160.
3808         *  @type string       $panel                 The panel this section belongs to (if any). Default empty.
3809         *  @type string       $capability            Capability required for the section. Default 'edit_theme_options'
3810         *  @type string|array $theme_supports        Theme features required to support the section.
3811         *  @type string       $title                 Title of the section to show in UI.
3812         *  @type string       $description           Description to show in the UI.
3813         *  @type string       $type                  Type of the section.
3814         *  @type callable     $active_callback       Active callback.
3815         *  @type bool         $description_hidden    Hide the description behind a help icon, instead of inline above the first control. Default false.
3816         * }
3817         * @return WP_Customize_Section             The instance of the section that was added.
3818         */
3819        public function add_section( $id, $args = array() ) {
3820                if ( $id instanceof WP_Customize_Section ) {
3821                        $section = $id;
3822                } else {
3823                        $section = new WP_Customize_Section( $this, $id, $args );
3824                }
3825
3826                $this->sections[ $section->id ] = $section;
3827                return $section;
3828        }
3829
3830        /**
3831         * Retrieve a customize section.
3832         *
3833         * @since 3.4.0
3834         *
3835         * @param string $id Section ID.
3836         * @return WP_Customize_Section|void The section, if set.
3837         */
3838        public function get_section( $id ) {
3839                if ( isset( $this->sections[ $id ] ) )
3840                        return $this->sections[ $id ];
3841        }
3842
3843        /**
3844         * Remove a customize section.
3845         *
3846         * @since 3.4.0
3847         *
3848         * @param string $id Section ID.
3849         */
3850        public function remove_section( $id ) {
3851                unset( $this->sections[ $id ] );
3852        }
3853
3854        /**
3855         * Register a customize section type.
3856         *
3857         * Registered types are eligible to be rendered via JS and created dynamically.
3858         *
3859         * @since 4.3.0
3860         *
3861         * @see WP_Customize_Section
3862         *
3863         * @param string $section Name of a custom section which is a subclass of WP_Customize_Section.
3864         */
3865        public function register_section_type( $section ) {
3866                $this->registered_section_types[] = $section;
3867        }
3868
3869        /**
3870         * Render JS templates for all registered section types.
3871         *
3872         * @since 4.3.0
3873         */
3874        public function render_section_templates() {
3875                foreach ( $this->registered_section_types as $section_type ) {
3876                        $section = new $section_type( $this, 'temp', array() );
3877                        $section->print_template();
3878                }
3879        }
3880
3881        /**
3882         * Add a customize control.
3883         *
3884         * @since 3.4.0
3885         * @since 4.5.0 Return added WP_Customize_Control instance.
3886         *
3887         * @param WP_Customize_Control|string $id   Customize Control object, or ID.
3888         * @param array                       $args {
3889         *  Optional. Array of properties for the new Control object. Default empty array.
3890         *
3891         *  @type array        $settings              All settings tied to the control. If undefined, defaults to `$setting`.
3892         *                                            IDs in the array correspond to the ID of a registered `WP_Customize_Setting`.
3893         *  @type string       $setting               The primary setting for the control (if there is one). Default is 'default'.
3894         *  @type string       $capability            Capability required to use this control. Normally derived from `$settings`.
3895         *  @type int          $priority              Order priority to load the control. Default 10.
3896         *  @type string       $section               The section this control belongs to. Default empty.
3897         *  @type string       $label                 Label for the control. Default empty.
3898         *  @type string       $description           Description for the control. Default empty.
3899         *  @type array        $choices               List of choices for 'radio' or 'select' type controls, where values
3900         *                                            are the keys, and labels are the values. Default empty array.
3901         *  @type array        $input_attrs           List of custom input attributes for control output, where attribute
3902         *                                            names are the keys and values are the values. Default empty array.
3903         *  @type bool         $allow_addition        Show UI for adding new content, currently only used for the
3904         *                                            dropdown-pages control. Default false.
3905         *  @type string       $type                  The type of the control. Default 'text'.
3906         *  @type callback     $active_callback       Active callback.
3907         * }
3908         * @return WP_Customize_Control             The instance of the control that was added.
3909         */
3910        public function add_control( $id, $args = array() ) {
3911                if ( $id instanceof WP_Customize_Control ) {
3912                        $control = $id;
3913                } else {
3914                        $control = new WP_Customize_Control( $this, $id, $args );
3915                }
3916
3917                $this->controls[ $control->id ] = $control;
3918                return $control;
3919        }
3920
3921        /**
3922         * Retrieve a customize control.
3923         *
3924         * @since 3.4.0
3925         *
3926         * @param string $id ID of the control.
3927         * @return WP_Customize_Control|void The control object, if set.
3928         */
3929        public function get_control( $id ) {
3930                if ( isset( $this->controls[ $id ] ) )
3931                        return $this->controls[ $id ];
3932        }
3933
3934        /**
3935         * Remove a customize control.
3936         *
3937         * @since 3.4.0
3938         *
3939         * @param string $id ID of the control.
3940         */
3941        public function remove_control( $id ) {
3942                unset( $this->controls[ $id ] );
3943        }
3944
3945        /**
3946         * Register a customize control type.
3947         *
3948         * Registered types are eligible to be rendered via JS and created dynamically.
3949         *
3950         * @since 4.1.0
3951         *
3952         * @param string $control Name of a custom control which is a subclass of
3953         *                        WP_Customize_Control.
3954         */
3955        public function register_control_type( $control ) {
3956                $this->registered_control_types[] = $control;
3957        }
3958
3959        /**
3960         * Render JS templates for all registered control types.
3961         *
3962         * @since 4.1.0
3963         */
3964        public function render_control_templates() {
3965                if ( $this->branching() ) {
3966                        $l10n = array(
3967                                /* translators: %s: User who is customizing the changeset in customizer. */
3968                                'locked' => __( '%s is already customizing this changeset. Please wait until they are done to try customizing. Your latest changes have been autosaved.' ),
3969                                /* translators: %s: User who is customizing the changeset in customizer. */
3970                                'locked_allow_override' => __( '%s is already customizing this changeset. Do you want to take over?' ),
3971                        );
3972                } else {
3973                        $l10n = array(
3974                                /* translators: %s: User who is customizing the changeset in customizer. */
3975                                'locked' => __( '%s is already customizing this site. Please wait until they are done to try customizing. Your latest changes have been autosaved.' ),
3976                                /* translators: %s: User who is customizing the changeset in customizer. */
3977                                'locked_allow_override' => __( '%s is already customizing this site. Do you want to take over?' ),
3978                        );
3979                }
3980
3981                foreach ( $this->registered_control_types as $control_type ) {
3982                        $control = new $control_type( $this, 'temp', array(
3983                                'settings' => array(),
3984                        ) );
3985                        $control->print_template();
3986                }
3987                ?>
3988
3989                <script type="text/html" id="tmpl-customize-control-default-content">
3990                        <#
3991                        var inputId = _.uniqueId( 'customize-control-default-input-' );
3992                        var descriptionId = _.uniqueId( 'customize-control-default-description-' );
3993                        var describedByAttr = data.description ? ' aria-describedby="' + descriptionId + '" ' : '';
3994                        #>
3995                        <# switch ( data.type ) {
3996                                case 'checkbox': #>
3997                                        <span class="customize-inside-control-row">
3998                                                <input
3999                                                        id="{{ inputId }}"
4000                                                        {{{ describedByAttr }}}
4001                                                        type="checkbox"
4002                                                        value="{{ data.value }}"
4003                                                        data-customize-setting-key-link="default"
4004                                                >
4005                                                <label for="{{ inputId }}">
4006                                                        {{ data.label }}
4007                                                </label>
4008                                                <# if ( data.description ) { #>
4009                                                        <span id="{{ descriptionId }}" class="description customize-control-description">{{{ data.description }}}</span>
4010                                                <# } #>
4011                                        </span>
4012                                        <#
4013                                        break;
4014                                case 'radio':
4015                                        if ( ! data.choices ) {
4016                                                return;
4017                                        }
4018                                        #>
4019                                        <# if ( data.label ) { #>
4020                                                <label for="{{ inputId }}" class="customize-control-title">
4021                                                        {{ data.label }}
4022                                                </label>
4023                                        <# } #>
4024                                        <# if ( data.description ) { #>
4025                                                <span id="{{ descriptionId }}" class="description customize-control-description">{{{ data.description }}}</span>
4026                                        <# } #>
4027                                        <# _.each( data.choices, function( val, key ) { #>
4028                                                <span class="customize-inside-control-row">
4029                                                        <#
4030                                                        var value, text;
4031                                                        if ( _.isObject( val ) ) {
4032                                                                value = val.value;
4033                                                                text = val.text;
4034                                                        } else {
4035                                                                value = key;
4036                                                                text = val;
4037                                                        }
4038                                                        #>
4039                                                        <input
4040                                                                id="{{ inputId + '-' + value }}"
4041                                                                type="radio"
4042                                                                value="{{ value }}"
4043                                                                name="{{ inputId }}"
4044                                                                data-customize-setting-key-link="default"
4045                                                                {{{ describedByAttr }}}
4046                                                        >
4047                                                        <label for="{{ inputId + '-' + value }}">{{ text }}</label>
4048                                                </span>
4049                                        <# } ); #>
4050                                        <#
4051                                        break;
4052                                default:
4053                                        #>
4054                                        <# if ( data.label ) { #>
4055                                                <label for="{{ inputId }}" class="customize-control-title">
4056                                                        {{ data.label }}
4057                                                </label>
4058                                        <# } #>
4059                                        <# if ( data.description ) { #>
4060                                                <span id="{{ descriptionId }}" class="description customize-control-description">{{{ data.description }}}</span>
4061                                        <# } #>
4062
4063                                        <#
4064                                        var inputAttrs = {
4065                                                id: inputId,
4066                                                'data-customize-setting-key-link': 'default'
4067                                        };
4068                                        if ( 'textarea' === data.type ) {
4069                                                inputAttrs.rows = '5';
4070                                        } else if ( 'button' === data.type ) {
4071                                                inputAttrs['class'] = 'button button-secondary';
4072                                                inputAttrs.type = 'button';
4073                                        } else {
4074                                                inputAttrs.type = data.type;
4075                                        }
4076                                        if ( data.description ) {
4077                                                inputAttrs['aria-describedby'] = descriptionId;
4078                                        }
4079                                        _.extend( inputAttrs, data.input_attrs );
4080                                        #>
4081
4082                                        <# if ( 'button' === data.type ) { #>
4083                                                <button
4084                                                        <# _.each( _.extend( inputAttrs ), function( value, key ) { #>
4085                                                                {{{ key }}}="{{ value }}"
4086                                                        <# } ); #>
4087                                                >{{ inputAttrs.value }}</button>
4088                                        <# } else if ( 'textarea' === data.type ) { #>
4089                                                <textarea
4090                                                        <# _.each( _.extend( inputAttrs ), function( value, key ) { #>
4091                                                                {{{ key }}}="{{ value }}"
4092                                                        <# }); #>
4093                                                >{{ inputAttrs.value }}</textarea>
4094                                        <# } else if ( 'select' === data.type ) { #>
4095                                                <# delete inputAttrs.type; #>
4096                                                <select
4097                                                        <# _.each( _.extend( inputAttrs ), function( value, key ) { #>
4098                                                                {{{ key }}}="{{ value }}"
4099                                                        <# }); #>
4100                                                        >
4101                                                        <# _.each( data.choices, function( val, key ) { #>
4102                                                                <#
4103                                                                var value, text;
4104                                                                if ( _.isObject( val ) ) {
4105                                                                        value = val.value;
4106                                                                        text = val.text;
4107                                                                } else {
4108                                                                        value = key;
4109                                                                        text = val;
4110                                                                }
4111                                                                #>
4112                                                                <option value="{{ value }}">{{ text }}</option>
4113                                                        <# } ); #>
4114                                                </select>
4115                                        <# } else { #>
4116                                                <input
4117                                                        <# _.each( _.extend( inputAttrs ), function( value, key ) { #>
4118                                                                {{{ key }}}="{{ value }}"
4119                                                        <# }); #>
4120                                                        >
4121                                        <# } #>
4122                        <# } #>
4123                </script>
4124
4125                <script type="text/html" id="tmpl-customize-notification">
4126                        <li class="notice notice-{{ data.type || 'info' }} {{ data.alt ? 'notice-alt' : '' }} {{ data.dismissible ? 'is-dismissible' : '' }} {{ data.containerClasses || '' }}" data-code="{{ data.code }}" data-type="{{ data.type }}">
4127                                <div class="notification-message">{{{ data.message || data.code }}}</div>
4128                                <# if ( data.dismissible ) { #>
4129                                        <button type="button" class="notice-dismiss"><span class="screen-reader-text"><?php _e( 'Dismiss' ); ?></span></button>
4130                                <# } #>
4131                        </li>
4132                </script>
4133
4134                <script type="text/html" id="tmpl-customize-changeset-locked-notification">
4135                        <li class="notice notice-{{ data.type || 'info' }} {{ data.containerClasses || '' }}" data-code="{{ data.code }}" data-type="{{ data.type }}">
4136                                <div class="notification-message customize-changeset-locked-message">
4137                                        <img class="customize-changeset-locked-avatar" src="{{ data.lockUser.avatar }}" alt="{{ data.lockUser.name }}">
4138                                        <p class="currently-editing">
4139                                                <# if ( data.message ) { #>
4140                                                        {{{ data.message }}}
4141                                                <# } else if ( data.allowOverride ) { #>
4142                                                        <?php
4143                                                        echo esc_html( sprintf( $l10n['locked_allow_override'], '{{ data.lockUser.name }}' ) );
4144                                                        ?>
4145                                                <# } else { #>
4146                                                        <?php
4147                                                        echo esc_html( sprintf( $l10n['locked'], '{{ data.lockUser.name }}' ) );
4148                                                        ?>
4149                                                <# } #>
4150                                        </p>
4151                                        <p class="notice notice-error notice-alt" hidden></p>
4152                                        <p class="action-buttons">
4153                                                <# if ( data.returnUrl !== data.previewUrl ) { #>
4154                                                        <a class="button customize-notice-go-back-button" href="{{ data.returnUrl }}"><?php _e( 'Go back' ); ?></a>
4155                                                <# } #>
4156                                                <a class="button customize-notice-preview-button" href="{{ data.frontendPreviewUrl }}"><?php _e( 'Preview' ); ?></a>
4157                                                <# if ( data.allowOverride ) { #>
4158                                                        <button class="button button-primary wp-tab-last customize-notice-take-over-button"><?php _e( 'Take over' ); ?></button>
4159                                                <# } #>
4160                                        </p>
4161                                </div>
4162                        </li>
4163                </script>
4164
4165                <script type="text/html" id="tmpl-customize-code-editor-lint-error-notification">
4166                        <li class="notice notice-{{ data.type || 'info' }} {{ data.alt ? 'notice-alt' : '' }} {{ data.dismissible ? 'is-dismissible' : '' }} {{ data.containerClasses || '' }}" data-code="{{ data.code }}" data-type="{{ data.type }}">
4167                                <div class="notification-message">{{{ data.message || data.code }}}</div>
4168
4169                                <p>
4170                                        <# var elementId = 'el-' + String( Math.random() ); #>
4171                                        <input id="{{ elementId }}" type="checkbox">
4172                                        <label for="{{ elementId }}"><?php _e( 'Update anyway, even though it might break your site?' ); ?></label>
4173                                </p>
4174                        </li>
4175                </script>
4176
4177                <?php
4178                /* The following template is obsolete in core but retained for plugins. */
4179                ?>
4180                <script type="text/html" id="tmpl-customize-control-notifications">
4181                        <ul>
4182                                <# _.each( data.notifications, function( notification ) { #>
4183                                        <li class="notice notice-{{ notification.type || 'info' }} {{ data.altNotice ? 'notice-alt' : '' }}" data-code="{{ notification.code }}" data-type="{{ notification.type }}">{{{ notification.message || notification.code }}}</li>
4184                                <# } ); #>
4185                        </ul>
4186                </script>
4187
4188                <script type="text/html" id="tmpl-customize-preview-link-control" >
4189                        <# var elementPrefix = _.uniqueId( 'el' ) + '-' #>
4190                        <p class="customize-control-title">
4191                                <?php esc_html_e( 'Share Preview Link' ); ?>
4192                        </p>
4193                        <p class="description customize-control-description"><?php esc_html_e( 'See how changes would look live on your website, and share the preview with people who can\'t access the Customizer.' ); ?></p>
4194                        <div class="customize-control-notifications-container"></div>
4195                        <div class="preview-link-wrapper">
4196                                <label for="{{ elementPrefix }}customize-preview-link-input" class="screen-reader-text"><?php esc_html_e( 'Preview Link' ); ?></label>
4197                                <a href="" target="">
4198                                        <span class="preview-control-element" data-component="url"></span>
4199                                        <span class="screen-reader-text"><?php _e( '(opens in a new window)' ); ?></span>
4200                                </a>
4201                                <input id="{{ elementPrefix }}customize-preview-link-input" readonly tabindex="-1" class="preview-control-element" data-component="input">
4202                                <button class="customize-copy-preview-link preview-control-element button button-secondary" data-component="button" data-copy-text="<?php esc_attr_e( 'Copy' ); ?>" data-copied-text="<?php esc_attr_e( 'Copied' ); ?>" ><?php esc_html_e( 'Copy' ); ?></button>
4203                        </div>
4204                </script>
4205                <script type="text/html" id="tmpl-customize-selected-changeset-status-control">
4206                        <# var inputId = _.uniqueId( 'customize-selected-changeset-status-control-input-' ); #>
4207                        <# var descriptionId = _.uniqueId( 'customize-selected-changeset-status-control-description-' ); #>
4208                        <# if ( data.label ) { #>
4209                                <label for="{{ inputId }}" class="customize-control-title">{{ data.label }}</label>
4210                        <# } #>
4211                        <# if ( data.description ) { #>
4212                                <span id="{{ descriptionId }}" class="description customize-control-description">{{{ data.description }}}</span>
4213                        <# } #>
4214                        <# _.each( data.choices, function( choice ) { #>
4215                                <# var choiceId = inputId + '-' + choice.status; #>
4216                                <span class="customize-inside-control-row">
4217                                        <input id="{{ choiceId }}" type="radio" value="{{ choice.status }}" name="{{ inputId }}" data-customize-setting-key-link="default">
4218                                        <label for="{{ choiceId }}">{{ choice.label }}</label>
4219                                </span>
4220                        <# } ); #>
4221                </script>
4222                <?php
4223        }
4224
4225        /**
4226         * Helper function to compare two objects by priority, ensuring sort stability via instance_number.
4227         *
4228         * @since 3.4.0
4229         * @deprecated 4.7.0 Use wp_list_sort()
4230         *
4231         * @param WP_Customize_Panel|WP_Customize_Section|WP_Customize_Control $a Object A.
4232         * @param WP_Customize_Panel|WP_Customize_Section|WP_Customize_Control $b Object B.
4233         * @return int
4234         */
4235        protected function _cmp_priority( $a, $b ) {
4236                _deprecated_function( __METHOD__, '4.7.0', 'wp_list_sort' );
4237
4238                if ( $a->priority === $b->priority ) {
4239                        return $a->instance_number - $b->instance_number;
4240                } else {
4241                        return $a->priority - $b->priority;
4242                }
4243        }
4244
4245        /**
4246         * Prepare panels, sections, and controls.
4247         *
4248         * For each, check if required related components exist,
4249         * whether the user has the necessary capabilities,
4250         * and sort by priority.
4251         *
4252         * @since 3.4.0
4253         */
4254        public function prepare_controls() {
4255
4256                $controls = array();
4257                $this->controls = wp_list_sort( $this->controls, array(
4258                        'priority'        => 'ASC',
4259                        'instance_number' => 'ASC',
4260                ), 'ASC', true );
4261
4262                foreach ( $this->controls as $id => $control ) {
4263                        if ( ! isset( $this->sections[ $control->section ] ) || ! $control->check_capabilities() ) {
4264                                continue;
4265                        }
4266
4267                        $this->sections[ $control->section ]->controls[] = $control;
4268                        $controls[ $id ] = $control;
4269                }
4270                $this->controls = $controls;
4271
4272                // Prepare sections.
4273                $this->sections = wp_list_sort( $this->sections, array(
4274                        'priority'        => 'ASC',
4275                        'instance_number' => 'ASC',
4276                ), 'ASC', true );
4277                $sections = array();
4278
4279                foreach ( $this->sections as $section ) {
4280                        if ( ! $section->check_capabilities() ) {
4281                                continue;
4282                        }
4283
4284
4285                        $section->controls = wp_list_sort( $section->controls, array(
4286                                'priority'        => 'ASC',
4287                                'instance_number' => 'ASC',
4288                        ) );
4289
4290                        if ( ! $section->panel ) {
4291                                // Top-level section.
4292                                $sections[ $section->id ] = $section;
4293                        } else {
4294                                // This section belongs to a panel.
4295                                if ( isset( $this->panels [ $section->panel ] ) ) {
4296                                        $this->panels[ $section->panel ]->sections[ $section->id ] = $section;
4297                                }
4298                        }
4299                }
4300                $this->sections = $sections;
4301
4302                // Prepare panels.
4303                $this->panels = wp_list_sort( $this->panels, array(
4304                        'priority'        => 'ASC',
4305                        'instance_number' => 'ASC',
4306                ), 'ASC', true );
4307                $panels = array();
4308
4309                foreach ( $this->panels as $panel ) {
4310                        if ( ! $panel->check_capabilities() ) {
4311                                continue;
4312                        }
4313
4314                        $panel->sections = wp_list_sort( $panel->sections, array(
4315                                'priority'        => 'ASC',
4316                                'instance_number' => 'ASC',
4317                        ), 'ASC', true );
4318                        $panels[ $panel->id ] = $panel;
4319                }
4320                $this->panels = $panels;
4321
4322                // Sort panels and top-level sections together.
4323                $this->containers = array_merge( $this->panels, $this->sections );
4324                $this->containers = wp_list_sort( $this->containers, array(
4325                        'priority'        => 'ASC',
4326                        'instance_number' => 'ASC',
4327                ), 'ASC', true );
4328        }
4329
4330        /**
4331         * Enqueue scripts for customize controls.
4332         *
4333         * @since 3.4.0
4334         */
4335        public function enqueue_control_scripts() {
4336                foreach ( $this->controls as $control ) {
4337                        $control->enqueue();
4338                }
4339
4340                if ( ! is_multisite() && ( current_user_can( 'install_themes' ) || current_user_can( 'update_themes' ) || current_user_can( 'delete_themes' ) ) ) {
4341                        wp_enqueue_script( 'updates' );
4342                        wp_localize_script( 'updates', '_wpUpdatesItemCounts', array(
4343                                'totals' => wp_get_update_data(),
4344                        ) );
4345                }
4346        }
4347
4348        /**
4349         * Determine whether the user agent is iOS.
4350         *
4351         * @since 4.4.0
4352         *
4353         * @return bool Whether the user agent is iOS.
4354         */
4355        public function is_ios() {
4356                return wp_is_mobile() && preg_match( '/iPad|iPod|iPhone/', $_SERVER['HTTP_USER_AGENT'] );
4357        }
4358
4359        /**
4360         * Get the template string for the Customizer pane document title.
4361         *
4362         * @since 4.4.0
4363         *
4364         * @return string The template string for the document title.
4365         */
4366        public function get_document_title_template() {
4367                if ( $this->is_theme_active() ) {
4368                        /* translators: %s: document title from the preview */
4369                        $document_title_tmpl = __( 'Customize: %s' );
4370                } else {
4371                        /* translators: %s: document title from the preview */
4372                        $document_title_tmpl = __( 'Live Preview: %s' );
4373                }
4374                $document_title_tmpl = html_entity_decode( $document_title_tmpl, ENT_QUOTES, 'UTF-8' ); // Because exported to JS and assigned to document.title.
4375                return $document_title_tmpl;
4376        }
4377
4378        /**
4379         * Set the initial URL to be previewed.
4380         *
4381         * URL is validated.
4382         *
4383         * @since 4.4.0
4384         *
4385         * @param string $preview_url URL to be previewed.
4386         */
4387        public function set_preview_url( $preview_url ) {
4388                $preview_url = esc_url_raw( $preview_url );
4389                $this->preview_url = wp_validate_redirect( $preview_url, home_url( '/' ) );
4390        }
4391
4392        /**
4393         * Get the initial URL to be previewed.
4394         *
4395         * @since 4.4.0
4396         *
4397         * @return string URL being previewed.
4398         */
4399        public function get_preview_url() {
4400                if ( empty( $this->preview_url ) ) {
4401                        $preview_url = home_url( '/' );
4402                } else {
4403                        $preview_url = $this->preview_url;
4404                }
4405                return $preview_url;
4406        }
4407
4408        /**
4409         * Determines whether the admin and the frontend are on different domains.
4410         *
4411         * @since 4.7.0
4412         *
4413         * @return bool Whether cross-domain.
4414         */
4415        public function is_cross_domain() {
4416                $admin_origin = wp_parse_url( admin_url() );
4417                $home_origin = wp_parse_url( home_url() );
4418                $cross_domain = ( strtolower( $admin_origin['host'] ) !== strtolower( $home_origin['host'] ) );
4419                return $cross_domain;
4420        }
4421
4422        /**
4423         * Get URLs allowed to be previewed.
4424         *
4425         * If the front end and the admin are served from the same domain, load the
4426         * preview over ssl if the Customizer is being loaded over ssl. This avoids
4427         * insecure content warnings. This is not attempted if the admin and front end
4428         * are on different domains to avoid the case where the front end doesn't have
4429         * ssl certs. Domain mapping plugins can allow other urls in these conditions
4430         * using the customize_allowed_urls filter.
4431         *
4432         * @since 4.7.0
4433         *
4434         * @returns array Allowed URLs.
4435         */
4436        public function get_allowed_urls() {
4437                $allowed_urls = array( home_url( '/' ) );
4438
4439                if ( is_ssl() && ! $this->is_cross_domain() ) {
4440                        $allowed_urls[] = home_url( '/', 'https' );
4441                }
4442
4443                /**
4444                 * Filters the list of URLs allowed to be clicked and followed in the Customizer preview.
4445                 *
4446                 * @since 3.4.0
4447                 *
4448                 * @param array $allowed_urls An array of allowed URLs.
4449                 */
4450                $allowed_urls = array_unique( apply_filters( 'customize_allowed_urls', $allowed_urls ) );
4451
4452                return $allowed_urls;
4453        }
4454
4455        /**
4456         * Get messenger channel.
4457         *
4458         * @since 4.7.0
4459         *
4460         * @return string Messenger channel.
4461         */
4462        public function get_messenger_channel() {
4463                return $this->messenger_channel;
4464        }
4465
4466        /**
4467         * Set URL to link the user to when closing the Customizer.
4468         *
4469         * URL is validated.
4470         *
4471         * @since 4.4.0
4472         *
4473         * @param string $return_url URL for return link.
4474         */
4475        public function set_return_url( $return_url ) {
4476                $return_url = esc_url_raw( $return_url );
4477                $return_url = remove_query_arg( wp_removable_query_args(), $return_url );
4478                $return_url = wp_validate_redirect( $return_url );
4479                $this->return_url = $return_url;
4480        }
4481
4482        /**
4483         * Get URL to link the user to when closing the Customizer.
4484         *
4485         * @since 4.4.0
4486         *
4487         * @return string URL for link to close Customizer.
4488         */
4489        public function get_return_url() {
4490                $referer = wp_get_referer();
4491                $excluded_referer_basenames = array( 'customize.php', 'wp-login.php' );
4492
4493                if ( $this->return_url ) {
4494                        $return_url = $this->return_url;
4495                } else if ( $referer && ! in_array( basename( parse_url( $referer, PHP_URL_PATH ) ), $excluded_referer_basenames, true ) ) {
4496                        $return_url = $referer;
4497                } else if ( $this->preview_url ) {
4498                        $return_url = $this->preview_url;
4499                } else {
4500                        $return_url = home_url( '/' );
4501                }
4502                return $return_url;
4503        }
4504
4505        /**
4506         * Set the autofocused constructs.
4507         *
4508         * @since 4.4.0
4509         *
4510         * @param array $autofocus {
4511         *     Mapping of 'panel', 'section', 'control' to the ID which should be autofocused.
4512         *
4513         *     @type string [$control]  ID for control to be autofocused.
4514         *     @type string [$section]  ID for section to be autofocused.
4515         *     @type string [$panel]    ID for panel to be autofocused.
4516         * }
4517         */
4518        public function set_autofocus( $autofocus ) {
4519                $this->autofocus = array_filter( wp_array_slice_assoc( $autofocus, array( 'panel', 'section', 'control' ) ), 'is_string' );
4520        }
4521
4522        /**
4523         * Get the autofocused constructs.
4524         *
4525         * @since 4.4.0
4526         *
4527         * @return array {
4528         *     Mapping of 'panel', 'section', 'control' to the ID which should be autofocused.
4529         *
4530         *     @type string [$control]  ID for control to be autofocused.
4531         *     @type string [$section]  ID for section to be autofocused.
4532         *     @type string [$panel]    ID for panel to be autofocused.
4533         * }
4534         */
4535        public function get_autofocus() {
4536                return $this->autofocus;
4537        }
4538
4539        /**
4540         * Get nonces for the Customizer.
4541         *
4542         * @since 4.5.0
4543         *
4544         * @return array Nonces.
4545         */
4546        public function get_nonces() {
4547                $nonces = array(
4548                        'save' => wp_create_nonce( 'save-customize_' . $this->get_stylesheet() ),
4549                        'preview' => wp_create_nonce( 'preview-customize_' . $this->get_stylesheet() ),
4550                        'switch_themes' => wp_create_nonce( 'switch_themes' ),
4551                        'dismiss_autosave_or_lock' => wp_create_nonce( 'customize_dismiss_autosave_or_lock' ),
4552                        'override_lock' => wp_create_nonce( 'customize_override_changeset_lock' ),
4553                        'trash' => wp_create_nonce( 'trash_customize_changeset' ),
4554                );
4555
4556                /**
4557                 * Filters nonces for Customizer.
4558                 *
4559                 * @since 4.2.0
4560                 *
4561                 * @param array                $nonces Array of refreshed nonces for save and
4562                 *                                     preview actions.
4563                 * @param WP_Customize_Manager $this   WP_Customize_Manager instance.
4564                 */
4565                $nonces = apply_filters( 'customize_refresh_nonces', $nonces, $this );
4566
4567                return $nonces;
4568        }
4569
4570        /**
4571         * Print JavaScript settings for parent window.
4572         *
4573         * @since 4.4.0
4574         */
4575        public function customize_pane_settings() {
4576
4577                $login_url = add_query_arg( array(
4578                        'interim-login' => 1,
4579                        'customize-login' => 1,
4580                ), wp_login_url() );
4581
4582                // Ensure dirty flags are set for modified settings.
4583                foreach ( array_keys( $this->unsanitized_post_values() ) as $setting_id ) {
4584                        $setting = $this->get_setting( $setting_id );
4585                        if ( $setting ) {
4586                                $setting->dirty = true;
4587                        }
4588                }
4589
4590                $autosave_revision_post = null;
4591                $autosave_autodraft_post = null;
4592                $changeset_post_id = $this->changeset_post_id();
4593                if ( ! $this->saved_starter_content_changeset && ! $this->autosaved() ) {
4594                        if ( $changeset_post_id ) {
4595                                if ( is_user_logged_in() ) {
4596                                        $autosave_revision_post = wp_get_post_autosave( $changeset_post_id, get_current_user_id() );
4597                                }
4598                        } else {
4599                                $autosave_autodraft_posts = $this->get_changeset_posts( array(
4600                                        'posts_per_page' => 1,
4601                                        'post_status' => 'auto-draft',
4602                                        'exclude_restore_dismissed' => true,
4603                                ) );
4604                                if ( ! empty( $autosave_autodraft_posts ) ) {
4605                                        $autosave_autodraft_post = array_shift( $autosave_autodraft_posts );
4606                                }
4607                        }
4608                }
4609
4610                $current_user_can_publish = current_user_can( get_post_type_object( 'customize_changeset' )->cap->publish_posts );
4611
4612                // @todo Include all of the status labels here from script-loader.php, and then allow it to be filtered.
4613                $status_choices = array();
4614                if ( $current_user_can_publish ) {
4615                        $status_choices[] = array(
4616                                'status' => 'publish',
4617                                'label' => __( 'Publish' ),
4618                        );
4619                }
4620                $status_choices[] = array(
4621                        'status' => 'draft',
4622                        'label' => __( 'Save Draft' ),
4623                );
4624                if ( $current_user_can_publish ) {
4625                        $status_choices[] = array(
4626                                'status' => 'future',
4627                                'label' => _x( 'Schedule', 'customizer changeset action/button label' ),
4628                        );
4629                }
4630
4631                // Prepare Customizer settings to pass to JavaScript.
4632                $changeset_post = null;
4633                if ( $changeset_post_id ) {
4634                        $changeset_post = get_post( $changeset_post_id );
4635                }
4636
4637                // Determine initial date to be at present or future, not past.
4638                $current_time = current_time( 'mysql', false );
4639                $initial_date = $current_time;
4640                if ( $changeset_post ) {
4641                        $initial_date = get_the_time( 'Y-m-d H:i:s', $changeset_post->ID );
4642                        if ( $initial_date < $current_time ) {
4643                                $initial_date = $current_time;
4644                        }
4645                }
4646
4647                $lock_user_id = false;
4648                if ( $this->changeset_post_id() ) {
4649                        $lock_user_id = wp_check_post_lock( $this->changeset_post_id() );
4650                }
4651
4652                $settings = array(
4653                        'changeset' => array(
4654                                'uuid' => $this->changeset_uuid(),
4655                                'branching' => $this->branching(),
4656                                'autosaved' => $this->autosaved(),
4657                                'hasAutosaveRevision' => ! empty( $autosave_revision_post ),
4658                                'latestAutoDraftUuid' => $autosave_autodraft_post ? $autosave_autodraft_post->post_name : null,
4659                                'status' => $changeset_post ? $changeset_post->post_status : '',
4660                                'currentUserCanPublish' => $current_user_can_publish,
4661                                'publishDate' => $initial_date,
4662                                'statusChoices' => $status_choices,
4663                                'lockUser' => $lock_user_id ? $this->get_lock_user_data(