]> scripts.mit.edu Git - autoinstalls/wordpress.git/blob - wp-includes/class-wp-customize-manager.php
WordPress 4.7.1
[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 ) ) {
3415                         $return_url = $referer;
3416                 } else if ( $this->preview_url ) {
3417                         $return_url = $this->preview_url;
3418                 } else {
3419                         $return_url = home_url( '/' );
3420                 }
3421                 return $return_url;
3422         }
3423
3424         /**
3425          * Set the autofocused constructs.
3426          *
3427          * @since 4.4.0
3428          * @access public
3429          *
3430          * @param array $autofocus {
3431          *     Mapping of 'panel', 'section', 'control' to the ID which should be autofocused.
3432          *
3433          *     @type string [$control]  ID for control to be autofocused.
3434          *     @type string [$section]  ID for section to be autofocused.
3435          *     @type string [$panel]    ID for panel to be autofocused.
3436          * }
3437          */
3438         public function set_autofocus( $autofocus ) {
3439                 $this->autofocus = array_filter( wp_array_slice_assoc( $autofocus, array( 'panel', 'section', 'control' ) ), 'is_string' );
3440         }
3441
3442         /**
3443          * Get the autofocused constructs.
3444          *
3445          * @since 4.4.0
3446          * @access public
3447          *
3448          * @return array {
3449          *     Mapping of 'panel', 'section', 'control' to the ID which should be autofocused.
3450          *
3451          *     @type string [$control]  ID for control to be autofocused.
3452          *     @type string [$section]  ID for section to be autofocused.
3453          *     @type string [$panel]    ID for panel to be autofocused.
3454          * }
3455          */
3456         public function get_autofocus() {
3457                 return $this->autofocus;
3458         }
3459
3460         /**
3461          * Get nonces for the Customizer.
3462          *
3463          * @since 4.5.0
3464          * @return array Nonces.
3465          */
3466         public function get_nonces() {
3467                 $nonces = array(
3468                         'save' => wp_create_nonce( 'save-customize_' . $this->get_stylesheet() ),
3469                         'preview' => wp_create_nonce( 'preview-customize_' . $this->get_stylesheet() ),
3470                 );
3471
3472                 /**
3473                  * Filters nonces for Customizer.
3474                  *
3475                  * @since 4.2.0
3476                  *
3477                  * @param array                $nonces Array of refreshed nonces for save and
3478                  *                                     preview actions.
3479                  * @param WP_Customize_Manager $this   WP_Customize_Manager instance.
3480                  */
3481                 $nonces = apply_filters( 'customize_refresh_nonces', $nonces, $this );
3482
3483                 return $nonces;
3484         }
3485
3486         /**
3487          * Print JavaScript settings for parent window.
3488          *
3489          * @since 4.4.0
3490          */
3491         public function customize_pane_settings() {
3492
3493                 $login_url = add_query_arg( array(
3494                         'interim-login' => 1,
3495                         'customize-login' => 1,
3496                 ), wp_login_url() );
3497
3498                 // Ensure dirty flags are set for modified settings.
3499                 foreach ( array_keys( $this->unsanitized_post_values() ) as $setting_id ) {
3500                         $setting = $this->get_setting( $setting_id );
3501                         if ( $setting ) {
3502                                 $setting->dirty = true;
3503                         }
3504                 }
3505
3506                 // Prepare Customizer settings to pass to JavaScript.
3507                 $settings = array(
3508                         'changeset' => array(
3509                                 'uuid' => $this->changeset_uuid(),
3510                                 'status' => $this->changeset_post_id() ? get_post_status( $this->changeset_post_id() ) : '',
3511                         ),
3512                         'timeouts' => array(
3513                                 'windowRefresh' => 250,
3514                                 'changesetAutoSave' => AUTOSAVE_INTERVAL * 1000,
3515                                 'keepAliveCheck' => 2500,
3516                                 'reflowPaneContents' => 100,
3517                                 'previewFrameSensitivity' => 2000,
3518                         ),
3519                         'theme'    => array(
3520                                 'stylesheet' => $this->get_stylesheet(),
3521                                 'active'     => $this->is_theme_active(),
3522                         ),
3523                         'url'      => array(
3524                                 'preview'       => esc_url_raw( $this->get_preview_url() ),
3525                                 'parent'        => esc_url_raw( admin_url() ),
3526                                 'activated'     => esc_url_raw( home_url( '/' ) ),
3527                                 'ajax'          => esc_url_raw( admin_url( 'admin-ajax.php', 'relative' ) ),
3528                                 'allowed'       => array_map( 'esc_url_raw', $this->get_allowed_urls() ),
3529                                 'isCrossDomain' => $this->is_cross_domain(),
3530                                 'home'          => esc_url_raw( home_url( '/' ) ),
3531                                 'login'         => esc_url_raw( $login_url ),
3532                         ),
3533                         'browser'  => array(
3534                                 'mobile' => wp_is_mobile(),
3535                                 'ios'    => $this->is_ios(),
3536                         ),
3537                         'panels'   => array(),
3538                         'sections' => array(),
3539                         'nonce'    => $this->get_nonces(),
3540                         'autofocus' => $this->get_autofocus(),
3541                         'documentTitleTmpl' => $this->get_document_title_template(),
3542                         'previewableDevices' => $this->get_previewable_devices(),
3543                 );
3544
3545                 // Prepare Customize Section objects to pass to JavaScript.
3546                 foreach ( $this->sections() as $id => $section ) {
3547                         if ( $section->check_capabilities() ) {
3548                                 $settings['sections'][ $id ] = $section->json();
3549                         }
3550                 }
3551
3552                 // Prepare Customize Panel objects to pass to JavaScript.
3553                 foreach ( $this->panels() as $panel_id => $panel ) {
3554                         if ( $panel->check_capabilities() ) {
3555                                 $settings['panels'][ $panel_id ] = $panel->json();
3556                                 foreach ( $panel->sections as $section_id => $section ) {
3557                                         if ( $section->check_capabilities() ) {
3558                                                 $settings['sections'][ $section_id ] = $section->json();
3559                                         }
3560                                 }
3561                         }
3562                 }
3563
3564                 ?>
3565                 <script type="text/javascript">
3566                         var _wpCustomizeSettings = <?php echo wp_json_encode( $settings ); ?>;
3567                         _wpCustomizeSettings.controls = {};
3568                         _wpCustomizeSettings.settings = {};
3569                         <?php
3570
3571                         // Serialize settings one by one to improve memory usage.
3572                         echo "(function ( s ){\n";
3573                         foreach ( $this->settings() as $setting ) {
3574                                 if ( $setting->check_capabilities() ) {
3575                                         printf(
3576                                                 "s[%s] = %s;\n",
3577                                                 wp_json_encode( $setting->id ),
3578                                                 wp_json_encode( $setting->json() )
3579                                         );
3580                                 }
3581                         }
3582                         echo "})( _wpCustomizeSettings.settings );\n";
3583
3584                         // Serialize controls one by one to improve memory usage.
3585                         echo "(function ( c ){\n";
3586                         foreach ( $this->controls() as $control ) {
3587                                 if ( $control->check_capabilities() ) {
3588                                         printf(
3589                                                 "c[%s] = %s;\n",
3590                                                 wp_json_encode( $control->id ),
3591                                                 wp_json_encode( $control->json() )
3592                                         );
3593                                 }
3594                         }
3595                         echo "})( _wpCustomizeSettings.controls );\n";
3596                 ?>
3597                 </script>
3598                 <?php
3599         }
3600
3601         /**
3602          * Returns a list of devices to allow previewing.
3603          *
3604          * @access public
3605          * @since 4.5.0
3606          *
3607          * @return array List of devices with labels and default setting.
3608          */
3609         public function get_previewable_devices() {
3610                 $devices = array(
3611                         'desktop' => array(
3612                                 'label' => __( 'Enter desktop preview mode' ),
3613                                 'default' => true,
3614                         ),
3615                         'tablet' => array(
3616                                 'label' => __( 'Enter tablet preview mode' ),
3617                         ),
3618                         'mobile' => array(
3619                                 'label' => __( 'Enter mobile preview mode' ),
3620                         ),
3621                 );
3622
3623                 /**
3624                  * Filters the available devices to allow previewing in the Customizer.
3625                  *
3626                  * @since 4.5.0
3627                  *
3628                  * @see WP_Customize_Manager::get_previewable_devices()
3629                  *
3630                  * @param array $devices List of devices with labels and default setting.
3631                  */
3632                 $devices = apply_filters( 'customize_previewable_devices', $devices );
3633
3634                 return $devices;
3635         }
3636
3637         /**
3638          * Register some default controls.
3639          *
3640          * @since 3.4.0
3641          */
3642         public function register_controls() {
3643
3644                 /* Panel, Section, and Control Types */
3645                 $this->register_panel_type( 'WP_Customize_Panel' );
3646                 $this->register_section_type( 'WP_Customize_Section' );
3647                 $this->register_section_type( 'WP_Customize_Sidebar_Section' );
3648                 $this->register_control_type( 'WP_Customize_Color_Control' );
3649                 $this->register_control_type( 'WP_Customize_Media_Control' );
3650                 $this->register_control_type( 'WP_Customize_Upload_Control' );
3651                 $this->register_control_type( 'WP_Customize_Image_Control' );
3652                 $this->register_control_type( 'WP_Customize_Background_Image_Control' );
3653                 $this->register_control_type( 'WP_Customize_Background_Position_Control' );
3654                 $this->register_control_type( 'WP_Customize_Cropped_Image_Control' );
3655                 $this->register_control_type( 'WP_Customize_Site_Icon_Control' );
3656                 $this->register_control_type( 'WP_Customize_Theme_Control' );
3657
3658                 /* Themes */
3659
3660                 $this->add_section( new WP_Customize_Themes_Section( $this, 'themes', array(
3661                         'title'      => $this->theme()->display( 'Name' ),
3662                         'capability' => 'switch_themes',
3663                         'priority'   => 0,
3664                 ) ) );
3665
3666                 // Themes Setting (unused - the theme is considerably more fundamental to the Customizer experience).
3667                 $this->add_setting( new WP_Customize_Filter_Setting( $this, 'active_theme', array(
3668                         'capability' => 'switch_themes',
3669                 ) ) );
3670
3671                 require_once( ABSPATH . 'wp-admin/includes/theme.php' );
3672
3673                 // Theme Controls.
3674
3675                 // Add a control for the active/original theme.
3676                 if ( ! $this->is_theme_active() ) {
3677                         $themes = wp_prepare_themes_for_js( array( wp_get_theme( $this->original_stylesheet ) ) );
3678                         $active_theme = current( $themes );
3679                         $active_theme['isActiveTheme'] = true;
3680                         $this->add_control( new WP_Customize_Theme_Control( $this, $active_theme['id'], array(
3681                                 'theme'    => $active_theme,
3682                                 'section'  => 'themes',
3683                                 'settings' => 'active_theme',
3684                         ) ) );
3685                 }
3686
3687                 $themes = wp_prepare_themes_for_js();
3688                 foreach ( $themes as $theme ) {
3689                         if ( $theme['active'] || $theme['id'] === $this->original_stylesheet ) {
3690                                 continue;
3691                         }
3692
3693                         $theme_id = 'theme_' . $theme['id'];
3694                         $theme['isActiveTheme'] = false;
3695                         $this->add_control( new WP_Customize_Theme_Control( $this, $theme_id, array(
3696                                 'theme'    => $theme,
3697                                 'section'  => 'themes',
3698                                 'settings' => 'active_theme',
3699                         ) ) );
3700                 }
3701
3702                 /* Site Identity */
3703
3704                 $this->add_section( 'title_tagline', array(
3705                         'title'    => __( 'Site Identity' ),
3706                         'priority' => 20,
3707                 ) );
3708
3709                 $this->add_setting( 'blogname', array(
3710                         'default'    => get_option( 'blogname' ),
3711                         'type'       => 'option',
3712                         'capability' => 'manage_options',
3713                 ) );
3714
3715                 $this->add_control( 'blogname', array(
3716                         'label'      => __( 'Site Title' ),
3717                         'section'    => 'title_tagline',
3718                 ) );
3719
3720                 $this->add_setting( 'blogdescription', array(
3721                         'default'    => get_option( 'blogdescription' ),
3722                         'type'       => 'option',
3723                         'capability' => 'manage_options',
3724                 ) );
3725
3726                 $this->add_control( 'blogdescription', array(
3727                         'label'      => __( 'Tagline' ),
3728                         'section'    => 'title_tagline',
3729                 ) );
3730
3731                 // Add a setting to hide header text if the theme doesn't support custom headers.
3732                 if ( ! current_theme_supports( 'custom-header', 'header-text' ) ) {
3733                         $this->add_setting( 'header_text', array(
3734                                 'theme_supports'    => array( 'custom-logo', 'header-text' ),
3735                                 'default'           => 1,
3736                                 'sanitize_callback' => 'absint',
3737                         ) );
3738
3739                         $this->add_control( 'header_text', array(
3740                                 'label'    => __( 'Display Site Title and Tagline' ),
3741                                 'section'  => 'title_tagline',
3742                                 'settings' => 'header_text',
3743                                 'type'     => 'checkbox',
3744                         ) );
3745                 }
3746
3747                 $this->add_setting( 'site_icon', array(
3748                         'type'       => 'option',
3749                         'capability' => 'manage_options',
3750                         'transport'  => 'postMessage', // Previewed with JS in the Customizer controls window.
3751                 ) );
3752
3753                 $this->add_control( new WP_Customize_Site_Icon_Control( $this, 'site_icon', array(
3754                         'label'       => __( 'Site Icon' ),
3755                         'description' => sprintf(
3756                                 /* translators: %s: site icon size in pixels */
3757                                 __( 'The Site Icon is used as a browser and app icon for your site. Icons must be square, and at least %s pixels wide and tall.' ),
3758                                 '<strong>512</strong>'
3759                         ),
3760                         'section'     => 'title_tagline',
3761                         'priority'    => 60,
3762                         'height'      => 512,
3763                         'width'       => 512,
3764                 ) ) );
3765
3766                 $this->add_setting( 'custom_logo', array(
3767                         'theme_supports' => array( 'custom-logo' ),
3768                         'transport'      => 'postMessage',
3769                 ) );
3770
3771                 $custom_logo_args = get_theme_support( 'custom-logo' );
3772                 $this->add_control( new WP_Customize_Cropped_Image_Control( $this, 'custom_logo', array(
3773                         'label'         => __( 'Logo' ),
3774                         'section'       => 'title_tagline',
3775                         'priority'      => 8,
3776                         'height'        => $custom_logo_args[0]['height'],
3777                         'width'         => $custom_logo_args[0]['width'],
3778                         'flex_height'   => $custom_logo_args[0]['flex-height'],
3779                         'flex_width'    => $custom_logo_args[0]['flex-width'],
3780                         'button_labels' => array(
3781                                 'select'       => __( 'Select logo' ),
3782                                 'change'       => __( 'Change logo' ),
3783                                 'remove'       => __( 'Remove' ),
3784                                 'default'      => __( 'Default' ),
3785                                 'placeholder'  => __( 'No logo selected' ),
3786                                 'frame_title'  => __( 'Select logo' ),
3787                                 'frame_button' => __( 'Choose logo' ),
3788                         ),
3789                 ) ) );
3790
3791                 $this->selective_refresh->add_partial( 'custom_logo', array(
3792                         'settings'            => array( 'custom_logo' ),
3793                         'selector'            => '.custom-logo-link',
3794                         'render_callback'     => array( $this, '_render_custom_logo_partial' ),
3795                         'container_inclusive' => true,
3796                 ) );
3797
3798                 /* Colors */
3799
3800                 $this->add_section( 'colors', array(
3801                         'title'          => __( 'Colors' ),
3802                         'priority'       => 40,
3803                 ) );
3804
3805                 $this->add_setting( 'header_textcolor', array(
3806                         'theme_supports' => array( 'custom-header', 'header-text' ),
3807                         'default'        => get_theme_support( 'custom-header', 'default-text-color' ),
3808
3809                         'sanitize_callback'    => array( $this, '_sanitize_header_textcolor' ),
3810                         'sanitize_js_callback' => 'maybe_hash_hex_color',
3811                 ) );
3812
3813                 // Input type: checkbox
3814                 // With custom value
3815                 $this->add_control( 'display_header_text', array(
3816                         'settings' => 'header_textcolor',
3817                         'label'    => __( 'Display Site Title and Tagline' ),
3818                         'section'  => 'title_tagline',
3819                         'type'     => 'checkbox',
3820                         'priority' => 40,
3821                 ) );
3822
3823                 $this->add_control( new WP_Customize_Color_Control( $this, 'header_textcolor', array(
3824                         'label'   => __( 'Header Text Color' ),
3825                         'section' => 'colors',
3826                 ) ) );
3827
3828                 // Input type: Color
3829                 // With sanitize_callback
3830                 $this->add_setting( 'background_color', array(
3831                         'default'        => get_theme_support( 'custom-background', 'default-color' ),
3832                         'theme_supports' => 'custom-background',
3833
3834                         'sanitize_callback'    => 'sanitize_hex_color_no_hash',
3835                         'sanitize_js_callback' => 'maybe_hash_hex_color',
3836                 ) );
3837
3838                 $this->add_control( new WP_Customize_Color_Control( $this, 'background_color', array(
3839                         'label'   => __( 'Background Color' ),
3840                         'section' => 'colors',
3841                 ) ) );
3842
3843                 /* Custom Header */
3844
3845                 if ( current_theme_supports( 'custom-header', 'video' ) ) {
3846                         $title = __( 'Header Media' );
3847                         $description = '<p>' . __( 'If you add a video, the image will be used as a fallback while the video loads.' ) . '</p>';
3848
3849                         // @todo Customizer sections should support having notifications just like controls do. See <https://core.trac.wordpress.org/ticket/38794>.
3850                         $description .= '<div class="customize-control-notifications-container header-video-not-currently-previewable" style="display: none"><ul>';
3851                         $description .= '<li class="notice notice-info">' . __( 'This theme doesn\'t support video headers on this page. Navigate to the front page or another page that supports video headers.' ) . '</li>';
3852                         $description .= '</ul></div>';
3853                         $width = absint( get_theme_support( 'custom-header', 'width' ) );
3854                         $height = absint( get_theme_support( 'custom-header', 'height' ) );
3855                         if ( $width && $height ) {
3856                                 $control_description = sprintf(
3857                                         /* translators: 1: .mp4, 2: header size in pixels */
3858                                         __( 'Upload your video in %1$s format and minimize its file size for best results. Your theme recommends dimensions of %2$s pixels.' ),
3859                                         '<code>.mp4</code>',
3860                                         sprintf( '<strong>%s &times; %s</strong>', $width, $height )
3861                                 );
3862                         } elseif ( $width ) {
3863                                 $control_description = sprintf(
3864                                         /* translators: 1: .mp4, 2: header width in pixels */
3865                                         __( 'Upload your video in %1$s format and minimize its file size for best results. Your theme recommends a width of %2$s pixels.' ),
3866                                         '<code>.mp4</code>',
3867                                         sprintf( '<strong>%s</strong>', $width )
3868                                 );
3869                         } else {
3870                                 $control_description = sprintf(
3871                                         /* translators: 1: .mp4, 2: header height in pixels */
3872                                         __( 'Upload your video in %1$s format and minimize its file size for best results. Your theme recommends a height of %2$s pixels.' ),
3873                                         '<code>.mp4</code>',
3874                                         sprintf( '<strong>%s</strong>', $height )
3875                                 );
3876                         }
3877                 } else {
3878                         $title = __( 'Header Image' );
3879                         $description = '';
3880                         $control_description = '';
3881                 }
3882
3883                 $this->add_section( 'header_image', array(
3884                         'title'          => $title,
3885                         'description'    => $description,
3886                         'theme_supports' => 'custom-header',
3887                         'priority'       => 60,
3888                 ) );
3889
3890                 $this->add_setting( 'header_video', array(
3891                         'theme_supports'    => array( 'custom-header', 'video' ),
3892                         'transport'         => 'postMessage',
3893                         'sanitize_callback' => 'absint',
3894                         'validate_callback' => array( $this, '_validate_header_video' ),
3895                 ) );
3896
3897                 $this->add_setting( 'external_header_video', array(
3898                         'theme_supports'    => array( 'custom-header', 'video' ),
3899                         'transport'         => 'postMessage',
3900                         'sanitize_callback' => array( $this, '_sanitize_external_header_video' ),
3901                         'validate_callback' => array( $this, '_validate_external_header_video' ),
3902                 ) );
3903
3904                 $this->add_setting( new WP_Customize_Filter_Setting( $this, 'header_image', array(
3905                         'default'        => sprintf( get_theme_support( 'custom-header', 'default-image' ), get_template_directory_uri(), get_stylesheet_directory_uri() ),
3906                         'theme_supports' => 'custom-header',
3907                 ) ) );
3908
3909                 $this->add_setting( new WP_Customize_Header_Image_Setting( $this, 'header_image_data', array(
3910                         'theme_supports' => 'custom-header',
3911                 ) ) );
3912
3913                 /*
3914                  * Switch image settings to postMessage when video support is enabled since
3915                  * it entails that the_custom_header_markup() will be used, and thus selective
3916                  * refresh can be utilized.
3917                  */
3918                 if ( current_theme_supports( 'custom-header', 'video' ) ) {
3919                         $this->get_setting( 'header_image' )->transport = 'postMessage';
3920                         $this->get_setting( 'header_image_data' )->transport = 'postMessage';
3921                 }
3922
3923                 $this->add_control( new WP_Customize_Media_Control( $this, 'header_video', array(
3924                         'theme_supports' => array( 'custom-header', 'video' ),
3925                         'label'          => __( 'Header Video' ),
3926                         'description'    => $control_description,
3927                         'section'        => 'header_image',
3928                         'mime_type'      => 'video',
3929                         // @todo These button_labels can be removed once WP_Customize_Media_Control provides mime_type-specific labels automatically. See <https://core.trac.wordpress.org/ticket/38796>.
3930                         'button_labels'  => array(
3931                                 'select'       => __( 'Select Video' ),
3932                                 'change'       => __( 'Change Video' ),
3933                                 'placeholder'  => __( 'No video selected' ),
3934                                 'frame_title'  => __( 'Select Video' ),
3935                                 'frame_button' => __( 'Choose Video' ),
3936                         ),
3937                         'active_callback' => 'is_header_video_active',
3938                 ) ) );
3939
3940                 $this->add_control( 'external_header_video', array(
3941                         'theme_supports' => array( 'custom-header', 'video' ),
3942                         'type'           => 'url',
3943                         'description'    => __( 'Or, enter a YouTube URL:' ),
3944                         'section'        => 'header_image',
3945                         'active_callback'=> 'is_front_page',
3946                 ) );
3947
3948                 $this->add_control( new WP_Customize_Header_Image_Control( $this ) );
3949
3950                 $this->selective_refresh->add_partial( 'custom_header', array(
3951                         'selector'            => '#wp-custom-header',
3952                         'render_callback'     => 'the_custom_header_markup',
3953                         'settings'            => array( 'header_video', 'external_header_video', 'header_image' ), // The image is used as a video fallback here.
3954                         'container_inclusive' => true,
3955                 ) );
3956
3957                 /* Custom Background */
3958
3959                 $this->add_section( 'background_image', array(
3960                         'title'          => __( 'Background Image' ),
3961                         'theme_supports' => 'custom-background',
3962                         'priority'       => 80,
3963                 ) );
3964
3965                 $this->add_setting( 'background_image', array(
3966                         'default'        => get_theme_support( 'custom-background', 'default-image' ),
3967                         'theme_supports' => 'custom-background',
3968                         'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
3969                 ) );
3970
3971                 $this->add_setting( new WP_Customize_Background_Image_Setting( $this, 'background_image_thumb', array(
3972                         'theme_supports' => 'custom-background',
3973                         'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
3974                 ) ) );
3975
3976                 $this->add_control( new WP_Customize_Background_Image_Control( $this ) );
3977
3978                 $this->add_setting( 'background_preset', array(
3979                         'default'        => get_theme_support( 'custom-background', 'default-preset' ),
3980                         'theme_supports' => 'custom-background',
3981                         'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
3982                 ) );
3983
3984                 $this->add_control( 'background_preset', array(
3985                         'label'      => _x( 'Preset', 'Background Preset' ),
3986                         'section'    => 'background_image',
3987                         'type'       => 'select',
3988                         'choices'    => array(
3989                                 'default' => _x( 'Default', 'Default Preset' ),
3990                                 'fill'    => __( 'Fill Screen' ),
3991                                 'fit'     => __( 'Fit to Screen' ),
3992                                 'repeat'  => _x( 'Repeat', 'Repeat Image' ),
3993                                 'custom'  => _x( 'Custom', 'Custom Preset' ),
3994                         ),
3995                 ) );
3996
3997                 $this->add_setting( 'background_position_x', array(
3998                         'default'        => get_theme_support( 'custom-background', 'default-position-x' ),
3999                         'theme_supports' => 'custom-background',
4000                         'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
4001                 ) );
4002
4003                 $this->add_setting( 'background_position_y', array(
4004                         'default'        => get_theme_support( 'custom-background', 'default-position-y' ),
4005                         'theme_supports' => 'custom-background',
4006                         'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
4007                 ) );
4008
4009                 $this->add_control( new WP_Customize_Background_Position_Control( $this, 'background_position', array(
4010                         'label'    => __( 'Image Position' ),
4011                         'section'  => 'background_image',
4012                         'settings' => array(
4013                                 'x' => 'background_position_x',
4014                                 'y' => 'background_position_y',
4015                         ),
4016                 ) ) );
4017
4018                 $this->add_setting( 'background_size', array(
4019                         'default'        => get_theme_support( 'custom-background', 'default-size' ),
4020                         'theme_supports' => 'custom-background',
4021                         'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
4022                 ) );
4023
4024                 $this->add_control( 'background_size', array(
4025                         'label'      => __( 'Image Size' ),
4026                         'section'    => 'background_image',
4027                         'type'       => 'select',
4028                         'choices'    => array(
4029                                 'auto'    => __( 'Original' ),
4030                                 'contain' => __( 'Fit to Screen' ),
4031                                 'cover'   => __( 'Fill Screen' ),
4032                         ),
4033                 ) );
4034
4035                 $this->add_setting( 'background_repeat', array(
4036                         'default'           => get_theme_support( 'custom-background', 'default-repeat' ),
4037                         'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
4038                         'theme_supports'    => 'custom-background',
4039                 ) );
4040
4041                 $this->add_control( 'background_repeat', array(
4042                         'label'    => __( 'Repeat Background Image' ),
4043                         'section'  => 'background_image',
4044                         'type'     => 'checkbox',
4045                 ) );
4046
4047                 $this->add_setting( 'background_attachment', array(
4048                         'default'           => get_theme_support( 'custom-background', 'default-attachment' ),
4049                         'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
4050                         'theme_supports'    => 'custom-background',
4051                 ) );
4052
4053                 $this->add_control( 'background_attachment', array(
4054                         'label'    => __( 'Scroll with Page' ),
4055                         'section'  => 'background_image',
4056                         'type'     => 'checkbox',
4057                 ) );
4058
4059
4060                 // If the theme is using the default background callback, we can update
4061                 // the background CSS using postMessage.
4062                 if ( get_theme_support( 'custom-background', 'wp-head-callback' ) === '_custom_background_cb' ) {
4063                         foreach ( array( 'color', 'image', 'preset', 'position_x', 'position_y', 'size', 'repeat', 'attachment' ) as $prop ) {
4064                                 $this->get_setting( 'background_' . $prop )->transport = 'postMessage';
4065                         }
4066                 }
4067
4068                 /*
4069                  * Static Front Page
4070                  * See also https://core.trac.wordpress.org/ticket/19627 which introduces the the static-front-page theme_support.
4071                  * The following replicates behavior from options-reading.php.
4072                  */
4073
4074                 $this->add_section( 'static_front_page', array(
4075                         'title' => __( 'Static Front Page' ),
4076                         'priority' => 120,
4077                         'description' => __( 'Your theme supports a static front page.' ),
4078                         'active_callback' => array( $this, 'has_published_pages' ),
4079                 ) );
4080
4081                 $this->add_setting( 'show_on_front', array(
4082                         'default' => get_option( 'show_on_front' ),
4083                         'capability' => 'manage_options',
4084                         'type' => 'option',
4085                 ) );
4086
4087                 $this->add_control( 'show_on_front', array(
4088                         'label' => __( 'Front page displays' ),
4089                         'section' => 'static_front_page',
4090                         'type' => 'radio',
4091                         'choices' => array(
4092                                 'posts' => __( 'Your latest posts' ),
4093                                 'page'  => __( 'A static page' ),
4094                         ),
4095                 ) );
4096
4097                 $this->add_setting( 'page_on_front', array(
4098                         'type'       => 'option',
4099                         'capability' => 'manage_options',
4100                 ) );
4101
4102                 $this->add_control( 'page_on_front', array(
4103                         'label' => __( 'Front page' ),
4104                         'section' => 'static_front_page',
4105                         'type' => 'dropdown-pages',
4106                         'allow_addition' => true,
4107                 ) );
4108
4109                 $this->add_setting( 'page_for_posts', array(
4110                         'type' => 'option',
4111                         'capability' => 'manage_options',
4112                 ) );
4113
4114                 $this->add_control( 'page_for_posts', array(
4115                         'label' => __( 'Posts page' ),
4116                         'section' => 'static_front_page',
4117                         'type' => 'dropdown-pages',
4118                         'allow_addition' => true,
4119                 ) );
4120
4121                 /* Custom CSS */
4122                 $this->add_section( 'custom_css', array(
4123                         'title'              => __( 'Additional CSS' ),
4124                         'priority'           => 200,
4125                         'description_hidden' => true,
4126                         'description'        => sprintf( '%s<br /><a href="%s" class="external-link" target="_blank">%s<span class="screen-reader-text">%s</span></a>',
4127                                 __( 'CSS allows you to customize the appearance and layout of your site with code. Separate CSS is saved for each of your themes. In the editing area the Tab key enters a tab character. To move below this area by pressing Tab, press the Esc key followed by the Tab key.' ),
4128                                 esc_url( __( 'https://codex.wordpress.org/CSS' ) ),
4129                                 __( 'Learn more about CSS' ),
4130                                 __( '(link opens in a new window)' )
4131                         ),
4132                 ) );
4133
4134                 $custom_css_setting = new WP_Customize_Custom_CSS_Setting( $this, sprintf( 'custom_css[%s]', get_stylesheet() ), array(
4135                         'capability' => 'edit_css',
4136                         'default' => sprintf( "/*\n%s\n*/", __( "You can add your own CSS here.\n\nClick the help icon above to learn more." ) ),
4137                 ) );
4138                 $this->add_setting( $custom_css_setting );
4139
4140                 $this->add_control( 'custom_css', array(
4141                         'type'     => 'textarea',
4142                         'section'  => 'custom_css',
4143                         'settings' => array( 'default' => $custom_css_setting->id ),
4144                         'input_attrs' => array(
4145                                 'class' => 'code', // Ensures contents displayed as LTR instead of RTL.
4146                         ),
4147                 ) );
4148         }
4149
4150         /**
4151          * Return whether there are published pages.
4152          *
4153          * Used as active callback for static front page section and controls.
4154          *
4155          * @access private
4156          * @since 4.7.0
4157          *
4158          * @returns bool Whether there are published (or to be published) pages.
4159          */
4160         public function has_published_pages() {
4161
4162                 $setting = $this->get_setting( 'nav_menus_created_posts' );
4163                 if ( $setting ) {
4164                         foreach ( $setting->value() as $post_id ) {
4165                                 if ( 'page' === get_post_type( $post_id ) ) {
4166                                         return true;
4167                                 }
4168                         }
4169                 }
4170                 return 0 !== count( get_pages() );
4171         }
4172
4173         /**
4174          * Add settings from the POST data that were not added with code, e.g. dynamically-created settings for Widgets
4175          *
4176          * @since 4.2.0
4177          * @access public
4178          *
4179          * @see add_dynamic_settings()
4180          */
4181         public function register_dynamic_settings() {
4182                 $setting_ids = array_keys( $this->unsanitized_post_values() );
4183                 $this->add_dynamic_settings( $setting_ids );
4184         }
4185
4186         /**
4187          * Callback for validating the header_textcolor value.
4188          *
4189          * Accepts 'blank', and otherwise uses sanitize_hex_color_no_hash().
4190          * Returns default text color if hex color is empty.
4191          *
4192          * @since 3.4.0
4193          *
4194          * @param string $color
4195          * @return mixed
4196          */
4197         public function _sanitize_header_textcolor( $color ) {
4198                 if ( 'blank' === $color )
4199                         return 'blank';
4200
4201                 $color = sanitize_hex_color_no_hash( $color );
4202                 if ( empty( $color ) )
4203                         $color = get_theme_support( 'custom-header', 'default-text-color' );
4204
4205                 return $color;
4206         }
4207
4208         /**
4209          * Callback for validating a background setting value.
4210          *
4211          * @since 4.7.0
4212          * @access private
4213          *
4214          * @param string $value Repeat value.
4215          * @param WP_Customize_Setting $setting Setting.
4216          * @return string|WP_Error Background value or validation error.
4217          */
4218         public function _sanitize_background_setting( $value, $setting ) {
4219                 if ( 'background_repeat' === $setting->id ) {
4220                         if ( ! in_array( $value, array( 'repeat-x', 'repeat-y', 'repeat', 'no-repeat' ) ) ) {
4221                                 return new WP_Error( 'invalid_value', __( 'Invalid value for background repeat.' ) );
4222                         }
4223                 } elseif ( 'background_attachment' === $setting->id ) {
4224                         if ( ! in_array( $value, array( 'fixed', 'scroll' ) ) ) {
4225                                 return new WP_Error( 'invalid_value', __( 'Invalid value for background attachment.' ) );
4226                         }
4227                 } elseif ( 'background_position_x' === $setting->id ) {
4228                         if ( ! in_array( $value, array( 'left', 'center', 'right' ), true ) ) {
4229                                 return new WP_Error( 'invalid_value', __( 'Invalid value for background position X.' ) );
4230                         }
4231                 } elseif ( 'background_position_y' === $setting->id ) {
4232                         if ( ! in_array( $value, array( 'top', 'center', 'bottom' ), true ) ) {
4233                                 return new WP_Error( 'invalid_value', __( 'Invalid value for background position Y.' ) );
4234                         }
4235                 } elseif ( 'background_size' === $setting->id ) {
4236                         if ( ! in_array( $value, array( 'auto', 'contain', 'cover' ), true ) ) {
4237                                 return new WP_Error( 'invalid_value', __( 'Invalid value for background size.' ) );
4238                         }
4239                 } elseif ( 'background_preset' === $setting->id ) {
4240                         if ( ! in_array( $value, array( 'default', 'fill', 'fit', 'repeat', 'custom' ), true ) ) {
4241                                 return new WP_Error( 'invalid_value', __( 'Invalid value for background size.' ) );
4242                         }
4243                 } elseif ( 'background_image' === $setting->id || 'background_image_thumb' === $setting->id ) {
4244                         $value = empty( $value ) ? '' : esc_url_raw( $value );
4245                 } else {
4246                         return new WP_Error( 'unrecognized_setting', __( 'Unrecognized background setting.' ) );
4247                 }
4248                 return $value;
4249         }
4250
4251         /**
4252          * Export header video settings to facilitate selective refresh.
4253          *
4254          * @since 4.7.0
4255          *
4256          * @param array $response Response.
4257          * @param WP_Customize_Selective_Refresh $selective_refresh Selective refresh component.
4258          * @param array $partials Array of partials.
4259          * @return array
4260          */
4261         public function export_header_video_settings( $response, $selective_refresh, $partials ) {
4262                 if ( isset( $partials['custom_header'] ) ) {
4263                         $response['custom_header_settings'] = get_header_video_settings();
4264                 }
4265
4266                 return $response;
4267         }
4268
4269         /**
4270          * Callback for validating the header_video value.
4271          *
4272          * Ensures that the selected video is less than 8MB and provides an error message.
4273          *
4274          * @since 4.7.0
4275          *
4276          * @param WP_Error $validity
4277          * @param mixed $value
4278          * @return mixed
4279          */
4280         public function _validate_header_video( $validity, $value ) {
4281                 $video = get_attached_file( absint( $value ) );
4282                 if ( $video ) {
4283                         $size = filesize( $video );
4284                         if ( 8 < $size / pow( 1024, 2 ) ) { // Check whether the size is larger than 8MB.
4285                                 $validity->add( 'size_too_large',
4286                                         __( 'This video file is too large to use as a header video. Try a shorter video or optimize the compression settings and re-upload a file that is less than 8MB. Or, upload your video to YouTube and link it with the option below.' )
4287                                 );
4288                         }
4289                         if ( '.mp4' !== substr( $video, -4 ) && '.mov' !== substr( $video, -4 ) ) { // Check for .mp4 or .mov format, which (assuming h.264 encoding) are the only cross-browser-supported formats.
4290                                 $validity->add( 'invalid_file_type', sprintf(
4291                                         /* translators: 1: .mp4, 2: .mov */
4292                                         __( 'Only %1$s or %2$s files may be used for header video. Please convert your video file and try again, or, upload your video to YouTube and link it with the option below.' ),
4293                                         '<code>.mp4</code>',
4294                                         '<code>.mov</code>'
4295                                 ) );
4296                         }
4297                 }
4298                 return $validity;
4299         }
4300
4301         /**
4302          * Callback for validating the external_header_video value.
4303          *
4304          * Ensures that the provided URL is supported.
4305          *
4306          * @since 4.7.0
4307          *
4308          * @param WP_Error $validity
4309          * @param mixed $value
4310          * @return mixed
4311          */
4312         public function _validate_external_header_video( $validity, $value ) {
4313                 $video = esc_url_raw( $value );
4314                 if ( $video ) {
4315                         if ( ! preg_match( '#^https?://(?:www\.)?(?:youtube\.com/watch|youtu\.be/)#', $video ) ) {
4316                                 $validity->add( 'invalid_url', __( 'Please enter a valid YouTube URL.' ) );
4317                         }
4318                 }
4319                 return $validity;
4320         }
4321
4322         /**
4323          * Callback for sanitizing the external_header_video value.
4324          *
4325          * @since 4.7.1
4326          *
4327          * @param string $value URL.
4328          * @return string Sanitized URL.
4329          */
4330         public function _sanitize_external_header_video( $value ) {
4331                 return esc_url_raw( trim( $value ) );
4332         }
4333
4334         /**
4335          * Callback for rendering the custom logo, used in the custom_logo partial.
4336          *
4337          * This method exists because the partial object and context data are passed
4338          * into a partial's render_callback so we cannot use get_custom_logo() as
4339          * the render_callback directly since it expects a blog ID as the first
4340          * argument. When WP no longer supports PHP 5.3, this method can be removed
4341          * in favor of an anonymous function.
4342          *
4343          * @see WP_Customize_Manager::register_controls()
4344          *
4345          * @since 4.5.0
4346          * @access private
4347          *
4348          * @return string Custom logo.
4349          */
4350         public function _render_custom_logo_partial() {
4351                 return get_custom_logo();
4352         }
4353 }