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