]> scripts.mit.edu Git - autoinstalls/wordpress.git/blob - wp-includes/class-wp-customize-manager.php
WordPress 4.7
[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_term_meta_cache' => false,
802                 ) );
803                 if ( ! empty( $changeset_post_query->posts ) ) {
804                         // Note: 'fields'=>'ids' is not being used in order to cache the post object as it will be needed.
805                         $changeset_post_id = $changeset_post_query->posts[0]->ID;
806                         wp_cache_set( $this->_changeset_uuid, $changeset_post_id, $cache_group );
807                         return $changeset_post_id;
808                 }
809
810                 return null;
811         }
812
813         /**
814          * Get the changeset post id for the loaded changeset.
815          *
816          * @since 4.7.0
817          * @access public
818          *
819          * @return int|null Post ID on success or null if there is no post yet saved.
820          */
821         public function changeset_post_id() {
822                 if ( ! isset( $this->_changeset_post_id ) ) {
823                         $post_id = $this->find_changeset_post_id( $this->_changeset_uuid );
824                         if ( ! $post_id ) {
825                                 $post_id = false;
826                         }
827                         $this->_changeset_post_id = $post_id;
828                 }
829                 if ( false === $this->_changeset_post_id ) {
830                         return null;
831                 }
832                 return $this->_changeset_post_id;
833         }
834
835         /**
836          * Get the data stored in a changeset post.
837          *
838          * @since 4.7.0
839          * @access protected
840          *
841          * @param int $post_id Changeset post ID.
842          * @return array|WP_Error Changeset data or WP_Error on error.
843          */
844         protected function get_changeset_post_data( $post_id ) {
845                 if ( ! $post_id ) {
846                         return new WP_Error( 'empty_post_id' );
847                 }
848                 $changeset_post = get_post( $post_id );
849                 if ( ! $changeset_post ) {
850                         return new WP_Error( 'missing_post' );
851                 }
852                 if ( 'customize_changeset' !== $changeset_post->post_type ) {
853                         return new WP_Error( 'wrong_post_type' );
854                 }
855                 $changeset_data = json_decode( $changeset_post->post_content, true );
856                 if ( function_exists( 'json_last_error' ) && json_last_error() ) {
857                         return new WP_Error( 'json_parse_error', '', json_last_error() );
858                 }
859                 if ( ! is_array( $changeset_data ) ) {
860                         return new WP_Error( 'expected_array' );
861                 }
862                 return $changeset_data;
863         }
864
865         /**
866          * Get changeset data.
867          *
868          * @since 4.7.0
869          * @access public
870          *
871          * @return array Changeset data.
872          */
873         public function changeset_data() {
874                 if ( isset( $this->_changeset_data ) ) {
875                         return $this->_changeset_data;
876                 }
877                 $changeset_post_id = $this->changeset_post_id();
878                 if ( ! $changeset_post_id ) {
879                         $this->_changeset_data = array();
880                 } else {
881                         $data = $this->get_changeset_post_data( $changeset_post_id );
882                         if ( ! is_wp_error( $data ) ) {
883                                 $this->_changeset_data = $data;
884                         } else {
885                                 $this->_changeset_data = array();
886                         }
887                 }
888                 return $this->_changeset_data;
889         }
890
891         /**
892          * Starter content setting IDs.
893          *
894          * @since 4.7.0
895          * @access private
896          * @var array
897          */
898         protected $pending_starter_content_settings_ids = array();
899
900         /**
901          * Import theme starter content into the customized state.
902          *
903          * @since 4.7.0
904          * @access public
905          *
906          * @param array $starter_content Starter content. Defaults to `get_theme_starter_content()`.
907          */
908         function import_theme_starter_content( $starter_content = array() ) {
909                 if ( empty( $starter_content ) ) {
910                         $starter_content = get_theme_starter_content();
911                 }
912
913                 $changeset_data = array();
914                 if ( $this->changeset_post_id() ) {
915                         $changeset_data = $this->get_changeset_post_data( $this->changeset_post_id() );
916                 }
917
918                 $sidebars_widgets = isset( $starter_content['widgets'] ) && ! empty( $this->widgets ) ? $starter_content['widgets'] : array();
919                 $attachments = isset( $starter_content['attachments'] ) && ! empty( $this->nav_menus ) ? $starter_content['attachments'] : array();
920                 $posts = isset( $starter_content['posts'] ) && ! empty( $this->nav_menus ) ? $starter_content['posts'] : array();
921                 $options = isset( $starter_content['options'] ) ? $starter_content['options'] : array();
922                 $nav_menus = isset( $starter_content['nav_menus'] ) && ! empty( $this->nav_menus ) ? $starter_content['nav_menus'] : array();
923                 $theme_mods = isset( $starter_content['theme_mods'] ) ? $starter_content['theme_mods'] : array();
924
925                 // Widgets.
926                 $max_widget_numbers = array();
927                 foreach ( $sidebars_widgets as $sidebar_id => $widgets ) {
928                         $sidebar_widget_ids = array();
929                         foreach ( $widgets as $widget ) {
930                                 list( $id_base, $instance ) = $widget;
931
932                                 if ( ! isset( $max_widget_numbers[ $id_base ] ) ) {
933
934                                         // When $settings is an array-like object, get an intrinsic array for use with array_keys().
935                                         $settings = get_option( "widget_{$id_base}", array() );
936                                         if ( $settings instanceof ArrayObject || $settings instanceof ArrayIterator ) {
937                                                 $settings = $settings->getArrayCopy();
938                                         }
939
940                                         // Find the max widget number for this type.
941                                         $widget_numbers = array_keys( $settings );
942                                         if ( count( $widget_numbers ) > 0 ) {
943                                                 $widget_numbers[] = 1;
944                                                 $max_widget_numbers[ $id_base ] = call_user_func_array( 'max', $widget_numbers );
945                                         } else {
946                                                 $max_widget_numbers[ $id_base ] = 1;
947                                         }
948                                 }
949                                 $max_widget_numbers[ $id_base ] += 1;
950
951                                 $widget_id = sprintf( '%s-%d', $id_base, $max_widget_numbers[ $id_base ] );
952                                 $setting_id = sprintf( 'widget_%s[%d]', $id_base, $max_widget_numbers[ $id_base ] );
953
954                                 $setting_value = $this->widgets->sanitize_widget_js_instance( $instance );
955                                 if ( empty( $changeset_data[ $setting_id ] ) || ! empty( $changeset_data[ $setting_id ]['starter_content'] ) ) {
956                                         $this->set_post_value( $setting_id, $setting_value );
957                                         $this->pending_starter_content_settings_ids[] = $setting_id;
958                                 }
959                                 $sidebar_widget_ids[] = $widget_id;
960                         }
961
962                         $setting_id = sprintf( 'sidebars_widgets[%s]', $sidebar_id );
963                         if ( empty( $changeset_data[ $setting_id ] ) || ! empty( $changeset_data[ $setting_id ]['starter_content'] ) ) {
964                                 $this->set_post_value( $setting_id, $sidebar_widget_ids );
965                                 $this->pending_starter_content_settings_ids[] = $setting_id;
966                         }
967                 }
968
969                 $starter_content_auto_draft_post_ids = array();
970                 if ( ! empty( $changeset_data['nav_menus_created_posts']['value'] ) ) {
971                         $starter_content_auto_draft_post_ids = array_merge( $starter_content_auto_draft_post_ids, $changeset_data['nav_menus_created_posts']['value'] );
972                 }
973
974                 // Make an index of all the posts needed and what their slugs are.
975                 $needed_posts = array();
976                 $attachments = $this->prepare_starter_content_attachments( $attachments );
977                 foreach ( $attachments as $attachment ) {
978                         $key = 'attachment:' . $attachment['post_name'];
979                         $needed_posts[ $key ] = true;
980                 }
981                 foreach ( array_keys( $posts ) as $post_symbol ) {
982                         if ( empty( $posts[ $post_symbol ]['post_name'] ) && empty( $posts[ $post_symbol ]['post_title'] ) ) {
983                                 unset( $posts[ $post_symbol ] );
984                                 continue;
985                         }
986                         if ( empty( $posts[ $post_symbol ]['post_name'] ) ) {
987                                 $posts[ $post_symbol ]['post_name'] = sanitize_title( $posts[ $post_symbol ]['post_title'] );
988                         }
989                         if ( empty( $posts[ $post_symbol ]['post_type'] ) ) {
990                                 $posts[ $post_symbol ]['post_type'] = 'post';
991                         }
992                         $needed_posts[ $posts[ $post_symbol ]['post_type'] . ':' . $posts[ $post_symbol ]['post_name'] ] = true;
993                 }
994                 $all_post_slugs = array_merge(
995                         wp_list_pluck( $attachments, 'post_name' ),
996                         wp_list_pluck( $posts, 'post_name' )
997                 );
998
999                 // Re-use auto-draft starter content posts referenced in the current customized state.
1000                 $existing_starter_content_posts = array();
1001                 if ( ! empty( $starter_content_auto_draft_post_ids ) ) {
1002                         $existing_posts_query = new WP_Query( array(
1003                                 'post__in' => $starter_content_auto_draft_post_ids,
1004                                 'post_status' => 'auto-draft',
1005                                 'post_type' => 'any',
1006                                 'posts_per_page' => -1,
1007                         ) );
1008                         foreach ( $existing_posts_query->posts as $existing_post ) {
1009                                 $post_name = $existing_post->post_name;
1010                                 if ( empty( $post_name ) ) {
1011                                         $post_name = get_post_meta( $existing_post->ID, '_customize_draft_post_name', true );
1012                                 }
1013                                 $existing_starter_content_posts[ $existing_post->post_type . ':' . $post_name ] = $existing_post;
1014                         }
1015                 }
1016
1017                 // Re-use non-auto-draft posts.
1018                 if ( ! empty( $all_post_slugs ) ) {
1019                         $existing_posts_query = new WP_Query( array(
1020                                 'post_name__in' => $all_post_slugs,
1021                                 'post_status' => array_diff( get_post_stati(), array( 'auto-draft' ) ),
1022                                 'post_type' => 'any',
1023                                 'posts_per_page' => -1,
1024                         ) );
1025                         foreach ( $existing_posts_query->posts as $existing_post ) {
1026                                 $key = $existing_post->post_type . ':' . $existing_post->post_name;
1027                                 if ( isset( $needed_posts[ $key ] ) && ! isset( $existing_starter_content_posts[ $key ] ) ) {
1028                                         $existing_starter_content_posts[ $key ] = $existing_post;
1029                                 }
1030                         }
1031                 }
1032
1033                 // Attachments are technically posts but handled differently.
1034                 if ( ! empty( $attachments ) ) {
1035
1036                         $attachment_ids = array();
1037
1038                         foreach ( $attachments as $symbol => $attachment ) {
1039                                 $file_array = array(
1040                                         'name' => $attachment['file_name'],
1041                                 );
1042                                 $file_path = $attachment['file_path'];
1043                                 $attachment_id = null;
1044                                 $attached_file = null;
1045                                 if ( isset( $existing_starter_content_posts[ 'attachment:' . $attachment['post_name'] ] ) ) {
1046                                         $attachment_post = $existing_starter_content_posts[ 'attachment:' . $attachment['post_name'] ];
1047                                         $attachment_id = $attachment_post->ID;
1048                                         $attached_file = get_attached_file( $attachment_id );
1049                                         if ( empty( $attached_file ) || ! file_exists( $attached_file ) ) {
1050                                                 $attachment_id = null;
1051                                                 $attached_file = null;
1052                                         } elseif ( $this->get_stylesheet() !== get_post_meta( $attachment_post->ID, '_starter_content_theme', true ) ) {
1053
1054                                                 // Re-generate attachment metadata since it was previously generated for a different theme.
1055                                                 $metadata = wp_generate_attachment_metadata( $attachment_post->ID, $attached_file );
1056                                                 wp_update_attachment_metadata( $attachment_id, $metadata );
1057                                                 update_post_meta( $attachment_id, '_starter_content_theme', $this->get_stylesheet() );
1058                                         }
1059                                 }
1060
1061                                 // Insert the attachment auto-draft because it doesn't yet exist or the attached file is gone.
1062                                 if ( ! $attachment_id ) {
1063
1064                                         // Copy file to temp location so that original file won't get deleted from theme after sideloading.
1065                                         $temp_file_name = wp_tempnam( basename( $file_path ) );
1066                                         if ( $temp_file_name && copy( $file_path, $temp_file_name ) ) {
1067                                                 $file_array['tmp_name'] = $temp_file_name;
1068                                         }
1069                                         if ( empty( $file_array['tmp_name'] ) ) {
1070                                                 continue;
1071                                         }
1072
1073                                         $attachment_post_data = array_merge(
1074                                                 wp_array_slice_assoc( $attachment, array( 'post_title', 'post_content', 'post_excerpt' ) ),
1075                                                 array(
1076                                                         'post_status' => 'auto-draft', // So attachment will be garbage collected in a week if changeset is never published.
1077                                                 )
1078                                         );
1079
1080                                         // In PHP < 5.6 filesize() returns 0 for the temp files unless we clear the file status cache.
1081                                         // Technically, PHP < 5.6.0 || < 5.5.13 || < 5.4.29 but no need to be so targeted.
1082                                         // See https://bugs.php.net/bug.php?id=65701
1083                                         if ( version_compare( PHP_VERSION, '5.6', '<' ) ) {
1084                                                 clearstatcache();
1085                                         }
1086
1087                                         $attachment_id = media_handle_sideload( $file_array, 0, null, $attachment_post_data );
1088                                         if ( is_wp_error( $attachment_id ) ) {
1089                                                 continue;
1090                                         }
1091                                         update_post_meta( $attachment_id, '_starter_content_theme', $this->get_stylesheet() );
1092                                         update_post_meta( $attachment_id, '_customize_draft_post_name', $attachment['post_name'] );
1093                                 }
1094
1095                                 $attachment_ids[ $symbol ] = $attachment_id;
1096                         }
1097                         $starter_content_auto_draft_post_ids = array_merge( $starter_content_auto_draft_post_ids, array_values( $attachment_ids ) );
1098                 }
1099
1100                 // Posts & pages.
1101                 if ( ! empty( $posts ) ) {
1102                         foreach ( array_keys( $posts ) as $post_symbol ) {
1103                                 if ( empty( $posts[ $post_symbol ]['post_type'] ) || empty( $posts[ $post_symbol ]['post_name'] ) ) {
1104                                         continue;
1105                                 }
1106                                 $post_type = $posts[ $post_symbol ]['post_type'];
1107                                 if ( ! empty( $posts[ $post_symbol ]['post_name'] ) ) {
1108                                         $post_name = $posts[ $post_symbol ]['post_name'];
1109                                 } elseif ( ! empty( $posts[ $post_symbol ]['post_title'] ) ) {
1110                                         $post_name = sanitize_title( $posts[ $post_symbol ]['post_title'] );
1111                                 } else {
1112                                         continue;
1113                                 }
1114
1115                                 // Use existing auto-draft post if one already exists with the same type and name.
1116                                 if ( isset( $existing_starter_content_posts[ $post_type . ':' . $post_name ] ) ) {
1117                                         $posts[ $post_symbol ]['ID'] = $existing_starter_content_posts[ $post_type . ':' . $post_name ]->ID;
1118                                         continue;
1119                                 }
1120
1121                                 // Translate the featured image symbol.
1122                                 if ( ! empty( $posts[ $post_symbol ]['thumbnail'] )
1123                                         && preg_match( '/^{{(?P<symbol>.+)}}$/', $posts[ $post_symbol ]['thumbnail'], $matches )
1124                                         && isset( $attachment_ids[ $matches['symbol'] ] ) ) {
1125                                         $posts[ $post_symbol ]['meta_input']['_thumbnail_id'] = $attachment_ids[ $matches['symbol'] ];
1126                                 }
1127
1128                                 if ( ! empty( $posts[ $post_symbol ]['template'] ) ) {
1129                                         $posts[ $post_symbol ]['meta_input']['_wp_page_template'] = $posts[ $post_symbol ]['template'];
1130                                 }
1131
1132                                 $r = $this->nav_menus->insert_auto_draft_post( $posts[ $post_symbol ] );
1133                                 if ( $r instanceof WP_Post ) {
1134                                         $posts[ $post_symbol ]['ID'] = $r->ID;
1135                                 }
1136                         }
1137
1138                         $starter_content_auto_draft_post_ids = array_merge( $starter_content_auto_draft_post_ids, wp_list_pluck( $posts, 'ID' ) );
1139                 }
1140
1141                 // The nav_menus_created_posts setting is why nav_menus component is dependency for adding posts.
1142                 if ( ! empty( $this->nav_menus ) && ! empty( $starter_content_auto_draft_post_ids ) ) {
1143                         $setting_id = 'nav_menus_created_posts';
1144                         $this->set_post_value( $setting_id, array_unique( array_values( $starter_content_auto_draft_post_ids ) ) );
1145                         $this->pending_starter_content_settings_ids[] = $setting_id;
1146                 }
1147
1148                 // Nav menus.
1149                 $placeholder_id = -1;
1150                 $reused_nav_menu_setting_ids = array();
1151                 foreach ( $nav_menus as $nav_menu_location => $nav_menu ) {
1152
1153                         $nav_menu_term_id = null;
1154                         $nav_menu_setting_id = null;
1155                         $matches = array();
1156
1157                         // Look for an existing placeholder menu with starter content to re-use.
1158                         foreach ( $changeset_data as $setting_id => $setting_params ) {
1159                                 $can_reuse = (
1160                                         ! empty( $setting_params['starter_content'] )
1161                                         &&
1162                                         ! in_array( $setting_id, $reused_nav_menu_setting_ids, true )
1163                                         &&
1164                                         preg_match( '#^nav_menu\[(?P<nav_menu_id>-?\d+)\]$#', $setting_id, $matches )
1165                                 );
1166                                 if ( $can_reuse ) {
1167                                         $nav_menu_term_id = intval( $matches['nav_menu_id'] );
1168                                         $nav_menu_setting_id = $setting_id;
1169                                         $reused_nav_menu_setting_ids[] = $setting_id;
1170                                         break;
1171                                 }
1172                         }
1173
1174                         if ( ! $nav_menu_term_id ) {
1175                                 while ( isset( $changeset_data[ sprintf( 'nav_menu[%d]', $placeholder_id ) ] ) ) {
1176                                         $placeholder_id--;
1177                                 }
1178                                 $nav_menu_term_id = $placeholder_id;
1179                                 $nav_menu_setting_id = sprintf( 'nav_menu[%d]', $placeholder_id );
1180                         }
1181
1182                         $this->set_post_value( $nav_menu_setting_id, array(
1183                                 'name' => isset( $nav_menu['name'] ) ? $nav_menu['name'] : $nav_menu_location,
1184                         ) );
1185                         $this->pending_starter_content_settings_ids[] = $nav_menu_setting_id;
1186
1187                         // @todo Add support for menu_item_parent.
1188                         $position = 0;
1189                         foreach ( $nav_menu['items'] as $nav_menu_item ) {
1190                                 $nav_menu_item_setting_id = sprintf( 'nav_menu_item[%d]', $placeholder_id-- );
1191                                 if ( ! isset( $nav_menu_item['position'] ) ) {
1192                                         $nav_menu_item['position'] = $position++;
1193                                 }
1194                                 $nav_menu_item['nav_menu_term_id'] = $nav_menu_term_id;
1195
1196                                 if ( isset( $nav_menu_item['object_id'] ) ) {
1197                                         if ( 'post_type' === $nav_menu_item['type'] && preg_match( '/^{{(?P<symbol>.+)}}$/', $nav_menu_item['object_id'], $matches ) && isset( $posts[ $matches['symbol'] ] ) ) {
1198                                                 $nav_menu_item['object_id'] = $posts[ $matches['symbol'] ]['ID'];
1199                                                 if ( empty( $nav_menu_item['title'] ) ) {
1200                                                         $original_object = get_post( $nav_menu_item['object_id'] );
1201                                                         $nav_menu_item['title'] = $original_object->post_title;
1202                                                 }
1203                                         } else {
1204                                                 continue;
1205                                         }
1206                                 } else {
1207                                         $nav_menu_item['object_id'] = 0;
1208                                 }
1209
1210                                 if ( empty( $changeset_data[ $nav_menu_item_setting_id ] ) || ! empty( $changeset_data[ $nav_menu_item_setting_id ]['starter_content'] ) ) {
1211                                         $this->set_post_value( $nav_menu_item_setting_id, $nav_menu_item );
1212                                         $this->pending_starter_content_settings_ids[] = $nav_menu_item_setting_id;
1213                                 }
1214                         }
1215
1216                         $setting_id = sprintf( 'nav_menu_locations[%s]', $nav_menu_location );
1217                         if ( empty( $changeset_data[ $setting_id ] ) || ! empty( $changeset_data[ $setting_id ]['starter_content'] ) ) {
1218                                 $this->set_post_value( $setting_id, $nav_menu_term_id );
1219                                 $this->pending_starter_content_settings_ids[] = $setting_id;
1220                         }
1221                 }
1222
1223                 // Options.
1224                 foreach ( $options as $name => $value ) {
1225                         if ( preg_match( '/^{{(?P<symbol>.+)}}$/', $value, $matches ) ) {
1226                                 if ( isset( $posts[ $matches['symbol'] ] ) ) {
1227                                         $value = $posts[ $matches['symbol'] ]['ID'];
1228                                 } elseif ( isset( $attachment_ids[ $matches['symbol'] ] ) ) {
1229                                         $value = $attachment_ids[ $matches['symbol'] ];
1230                                 } else {
1231                                         continue;
1232                                 }
1233                         }
1234
1235                         if ( empty( $changeset_data[ $name ] ) || ! empty( $changeset_data[ $name ]['starter_content'] ) ) {
1236                                 $this->set_post_value( $name, $value );
1237                                 $this->pending_starter_content_settings_ids[] = $name;
1238                         }
1239                 }
1240
1241                 // Theme mods.
1242                 foreach ( $theme_mods as $name => $value ) {
1243                         if ( preg_match( '/^{{(?P<symbol>.+)}}$/', $value, $matches ) ) {
1244                                 if ( isset( $posts[ $matches['symbol'] ] ) ) {
1245                                         $value = $posts[ $matches['symbol'] ]['ID'];
1246                                 } elseif ( isset( $attachment_ids[ $matches['symbol'] ] ) ) {
1247                                         $value = $attachment_ids[ $matches['symbol'] ];
1248                                 } else {
1249                                         continue;
1250                                 }
1251                         }
1252
1253                         // Handle header image as special case since setting has a legacy format.
1254                         if ( 'header_image' === $name ) {
1255                                 $name = 'header_image_data';
1256                                 $metadata = wp_get_attachment_metadata( $value );
1257                                 if ( empty( $metadata ) ) {
1258                                         continue;
1259                                 }
1260                                 $value = array(
1261                                         'attachment_id' => $value,
1262                                         'url' => wp_get_attachment_url( $value ),
1263                                         'height' => $metadata['height'],
1264                                         'width' => $metadata['width'],
1265                                 );
1266                         } elseif ( 'background_image' === $name ) {
1267                                 $value = wp_get_attachment_url( $value );
1268                         }
1269
1270                         if ( empty( $changeset_data[ $name ] ) || ! empty( $changeset_data[ $name ]['starter_content'] ) ) {
1271                                 $this->set_post_value( $name, $value );
1272                                 $this->pending_starter_content_settings_ids[] = $name;
1273                         }
1274                 }
1275
1276                 if ( ! empty( $this->pending_starter_content_settings_ids ) ) {
1277                         if ( did_action( 'customize_register' ) ) {
1278                                 $this->_save_starter_content_changeset();
1279                         } else {
1280                                 add_action( 'customize_register', array( $this, '_save_starter_content_changeset' ), 1000 );
1281                         }
1282                 }
1283         }
1284
1285         /**
1286          * Prepare starter content attachments.
1287          *
1288          * Ensure that the attachments are valid and that they have slugs and file name/path.
1289          *
1290          * @since 4.7.0
1291          * @access private
1292          *
1293          * @param array $attachments Attachments.
1294          * @return array Prepared attachments.
1295          */
1296         protected function prepare_starter_content_attachments( $attachments ) {
1297                 $prepared_attachments = array();
1298                 if ( empty( $attachments ) ) {
1299                         return $prepared_attachments;
1300                 }
1301
1302                 // Such is The WordPress Way.
1303                 require_once( ABSPATH . 'wp-admin/includes/file.php' );
1304                 require_once( ABSPATH . 'wp-admin/includes/media.php' );
1305                 require_once( ABSPATH . 'wp-admin/includes/image.php' );
1306
1307                 foreach ( $attachments as $symbol => $attachment ) {
1308
1309                         // A file is required and URLs to files are not currently allowed.
1310                         if ( empty( $attachment['file'] ) || preg_match( '#^https?://$#', $attachment['file'] ) ) {
1311                                 continue;
1312                         }
1313
1314                         $file_path = null;
1315                         if ( file_exists( $attachment['file'] ) ) {
1316                                 $file_path = $attachment['file']; // Could be absolute path to file in plugin.
1317                         } elseif ( is_child_theme() && file_exists( get_stylesheet_directory() . '/' . $attachment['file'] ) ) {
1318                                 $file_path = get_stylesheet_directory() . '/' . $attachment['file'];
1319                         } elseif ( file_exists( get_template_directory() . '/' . $attachment['file'] ) ) {
1320                                 $file_path = get_template_directory() . '/' . $attachment['file'];
1321                         } else {
1322                                 continue;
1323                         }
1324                         $file_name = basename( $attachment['file'] );
1325
1326                         // Skip file types that are not recognized.
1327                         $checked_filetype = wp_check_filetype( $file_name );
1328                         if ( empty( $checked_filetype['type'] ) ) {
1329                                 continue;
1330                         }
1331
1332                         // Ensure post_name is set since not automatically derived from post_title for new auto-draft posts.
1333                         if ( empty( $attachment['post_name'] ) ) {
1334                                 if ( ! empty( $attachment['post_title'] ) ) {
1335                                         $attachment['post_name'] = sanitize_title( $attachment['post_title'] );
1336                                 } else {
1337                                         $attachment['post_name'] = sanitize_title( preg_replace( '/\.\w+$/', '', $file_name ) );
1338                                 }
1339                         }
1340
1341                         $attachment['file_name'] = $file_name;
1342                         $attachment['file_path'] = $file_path;
1343                         $prepared_attachments[ $symbol ] = $attachment;
1344                 }
1345                 return $prepared_attachments;
1346         }
1347
1348         /**
1349          * Save starter content changeset.
1350          *
1351          * @since 4.7.0
1352          * @access private
1353          */
1354         public function _save_starter_content_changeset() {
1355
1356                 if ( empty( $this->pending_starter_content_settings_ids ) ) {
1357                         return;
1358                 }
1359
1360                 $this->save_changeset_post( array(
1361                         'data' => array_fill_keys( $this->pending_starter_content_settings_ids, array( 'starter_content' => true ) ),
1362                         'starter_content' => true,
1363                 ) );
1364
1365                 $this->pending_starter_content_settings_ids = array();
1366         }
1367
1368         /**
1369          * Get dirty pre-sanitized setting values in the current customized state.
1370          *
1371          * The returned array consists of a merge of three sources:
1372          * 1. If the theme is not currently active, then the base array is any stashed
1373          *    theme mods that were modified previously but never published.
1374          * 2. The values from the current changeset, if it exists.
1375          * 3. If the user can customize, the values parsed from the incoming
1376          *    `$_POST['customized']` JSON data.
1377          * 4. Any programmatically-set post values via `WP_Customize_Manager::set_post_value()`.
1378          *
1379          * The name "unsanitized_post_values" is a carry-over from when the customized
1380          * state was exclusively sourced from `$_POST['customized']`. Nevertheless,
1381          * the value returned will come from the current changeset post and from the
1382          * incoming post data.
1383          *
1384          * @since 4.1.1
1385          * @since 4.7.0 Added $args param and merging with changeset values and stashed theme mods.
1386          *
1387          * @param array $args {
1388          *     Args.
1389          *
1390          *     @type bool $exclude_changeset Whether the changeset values should also be excluded. Defaults to false.
1391          *     @type bool $exclude_post_data Whether the post input values should also be excluded. Defaults to false when lacking the customize capability.
1392          * }
1393          * @return array
1394          */
1395         public function unsanitized_post_values( $args = array() ) {
1396                 $args = array_merge(
1397                         array(
1398                                 'exclude_changeset' => false,
1399                                 'exclude_post_data' => ! current_user_can( 'customize' ),
1400                         ),
1401                         $args
1402                 );
1403
1404                 $values = array();
1405
1406                 // Let default values be from the stashed theme mods if doing a theme switch and if no changeset is present.
1407                 if ( ! $this->is_theme_active() ) {
1408                         $stashed_theme_mods = get_option( 'customize_stashed_theme_mods' );
1409                         $stylesheet = $this->get_stylesheet();
1410                         if ( isset( $stashed_theme_mods[ $stylesheet ] ) ) {
1411                                 $values = array_merge( $values, wp_list_pluck( $stashed_theme_mods[ $stylesheet ], 'value' ) );
1412                         }
1413                 }
1414
1415                 if ( ! $args['exclude_changeset'] ) {
1416                         foreach ( $this->changeset_data() as $setting_id => $setting_params ) {
1417                                 if ( ! array_key_exists( 'value', $setting_params ) ) {
1418                                         continue;
1419                                 }
1420                                 if ( isset( $setting_params['type'] ) && 'theme_mod' === $setting_params['type'] ) {
1421
1422                                         // Ensure that theme mods values are only used if they were saved under the current theme.
1423                                         $namespace_pattern = '/^(?P<stylesheet>.+?)::(?P<setting_id>.+)$/';
1424                                         if ( preg_match( $namespace_pattern, $setting_id, $matches ) && $this->get_stylesheet() === $matches['stylesheet'] ) {
1425                                                 $values[ $matches['setting_id'] ] = $setting_params['value'];
1426                                         }
1427                                 } else {
1428                                         $values[ $setting_id ] = $setting_params['value'];
1429                                 }
1430                         }
1431                 }
1432
1433                 if ( ! $args['exclude_post_data'] ) {
1434                         if ( ! isset( $this->_post_values ) ) {
1435                                 if ( isset( $_POST['customized'] ) ) {
1436                                         $post_values = json_decode( wp_unslash( $_POST['customized'] ), true );
1437                                 } else {
1438                                         $post_values = array();
1439                                 }
1440                                 if ( is_array( $post_values ) ) {
1441                                         $this->_post_values = $post_values;
1442                                 } else {
1443                                         $this->_post_values = array();
1444                                 }
1445                         }
1446                         $values = array_merge( $values, $this->_post_values );
1447                 }
1448                 return $values;
1449         }
1450
1451         /**
1452          * Returns the sanitized value for a given setting from the current customized state.
1453          *
1454          * The name "post_value" is a carry-over from when the customized state was exclusively
1455          * sourced from `$_POST['customized']`. Nevertheless, the value returned will come
1456          * from the current changeset post and from the incoming post data.
1457          *
1458          * @since 3.4.0
1459          * @since 4.1.1 Introduced the `$default` parameter.
1460          * @since 4.6.0 `$default` is now returned early when the setting post value is invalid.
1461          * @access public
1462          *
1463          * @see WP_REST_Server::dispatch()
1464          * @see WP_Rest_Request::sanitize_params()
1465          * @see WP_Rest_Request::has_valid_params()
1466          *
1467          * @param WP_Customize_Setting $setting A WP_Customize_Setting derived object.
1468          * @param mixed                $default Value returned $setting has no post value (added in 4.2.0)
1469          *                                      or the post value is invalid (added in 4.6.0).
1470          * @return string|mixed $post_value Sanitized value or the $default provided.
1471          */
1472         public function post_value( $setting, $default = null ) {
1473                 $post_values = $this->unsanitized_post_values();
1474                 if ( ! array_key_exists( $setting->id, $post_values ) ) {
1475                         return $default;
1476                 }
1477                 $value = $post_values[ $setting->id ];
1478                 $valid = $setting->validate( $value );
1479                 if ( is_wp_error( $valid ) ) {
1480                         return $default;
1481                 }
1482                 $value = $setting->sanitize( $value );
1483                 if ( is_null( $value ) || is_wp_error( $value ) ) {
1484                         return $default;
1485                 }
1486                 return $value;
1487         }
1488
1489         /**
1490          * Override a setting's value in the current customized state.
1491          *
1492          * The name "post_value" is a carry-over from when the customized state was
1493          * exclusively sourced from `$_POST['customized']`.
1494          *
1495          * @since 4.2.0
1496          * @access public
1497          *
1498          * @param string $setting_id ID for the WP_Customize_Setting instance.
1499          * @param mixed  $value      Post value.
1500          */
1501         public function set_post_value( $setting_id, $value ) {
1502                 $this->unsanitized_post_values(); // Populate _post_values from $_POST['customized'].
1503                 $this->_post_values[ $setting_id ] = $value;
1504
1505                 /**
1506                  * Announce when a specific setting's unsanitized post value has been set.
1507                  *
1508                  * Fires when the WP_Customize_Manager::set_post_value() method is called.
1509                  *
1510                  * The dynamic portion of the hook name, `$setting_id`, refers to the setting ID.
1511                  *
1512                  * @since 4.4.0
1513                  *
1514                  * @param mixed                $value Unsanitized setting post value.
1515                  * @param WP_Customize_Manager $this  WP_Customize_Manager instance.
1516                  */
1517                 do_action( "customize_post_value_set_{$setting_id}", $value, $this );
1518
1519                 /**
1520                  * Announce when any setting's unsanitized post value has been set.
1521                  *
1522                  * Fires when the WP_Customize_Manager::set_post_value() method is called.
1523                  *
1524                  * This is useful for `WP_Customize_Setting` instances to watch
1525                  * in order to update a cached previewed value.
1526                  *
1527                  * @since 4.4.0
1528                  *
1529                  * @param string               $setting_id Setting ID.
1530                  * @param mixed                $value      Unsanitized setting post value.
1531                  * @param WP_Customize_Manager $this       WP_Customize_Manager instance.
1532                  */
1533                 do_action( 'customize_post_value_set', $setting_id, $value, $this );
1534         }
1535
1536         /**
1537          * Print JavaScript settings.
1538          *
1539          * @since 3.4.0
1540          */
1541         public function customize_preview_init() {
1542
1543                 /*
1544                  * Now that Customizer previews are loaded into iframes via GET requests
1545                  * and natural URLs with transaction UUIDs added, we need to ensure that
1546                  * the responses are never cached by proxies. In practice, this will not
1547                  * be needed if the user is logged-in anyway. But if anonymous access is
1548                  * allowed then the auth cookies would not be sent and WordPress would
1549                  * not send no-cache headers by default.
1550                  */
1551                 if ( ! headers_sent() ) {
1552                         nocache_headers();
1553                         header( 'X-Robots: noindex, nofollow, noarchive' );
1554                 }
1555                 add_action( 'wp_head', 'wp_no_robots' );
1556                 add_filter( 'wp_headers', array( $this, 'filter_iframe_security_headers' ) );
1557
1558                 /*
1559                  * If preview is being served inside the customizer preview iframe, and
1560                  * if the user doesn't have customize capability, then it is assumed
1561                  * that the user's session has expired and they need to re-authenticate.
1562                  */
1563                 if ( $this->messenger_channel && ! current_user_can( 'customize' ) ) {
1564                         $this->wp_die( -1, __( 'Unauthorized. You may remove the customize_messenger_channel param to preview as frontend.' ) );
1565                         return;
1566                 }
1567
1568                 $this->prepare_controls();
1569
1570                 add_filter( 'wp_redirect', array( $this, 'add_state_query_params' ) );
1571
1572                 wp_enqueue_script( 'customize-preview' );
1573                 add_action( 'wp_head', array( $this, 'customize_preview_loading_style' ) );
1574                 add_action( 'wp_head', array( $this, 'remove_frameless_preview_messenger_channel' ) );
1575                 add_action( 'wp_footer', array( $this, 'customize_preview_settings' ), 20 );
1576                 add_filter( 'get_edit_post_link', '__return_empty_string' );
1577
1578                 /**
1579                  * Fires once the Customizer preview has initialized and JavaScript
1580                  * settings have been printed.
1581                  *
1582                  * @since 3.4.0
1583                  *
1584                  * @param WP_Customize_Manager $this WP_Customize_Manager instance.
1585                  */
1586                 do_action( 'customize_preview_init', $this );
1587         }
1588
1589         /**
1590          * Filter the X-Frame-Options and Content-Security-Policy headers to ensure frontend can load in customizer.
1591          *
1592          * @since 4.7.0
1593          * @access public
1594          *
1595          * @param array $headers Headers.
1596          * @return array Headers.
1597          */
1598         public function filter_iframe_security_headers( $headers ) {
1599                 $customize_url = admin_url( 'customize.php' );
1600                 $headers['X-Frame-Options'] = 'ALLOW-FROM ' . $customize_url;
1601                 $headers['Content-Security-Policy'] = 'frame-ancestors ' . preg_replace( '#^(\w+://[^/]+).+?$#', '$1', $customize_url );
1602                 return $headers;
1603         }
1604
1605         /**
1606          * Add customize state query params to a given URL if preview is allowed.
1607          *
1608          * @since 4.7.0
1609          * @access public
1610          * @see wp_redirect()
1611          * @see WP_Customize_Manager::get_allowed_url()
1612          *
1613          * @param string $url URL.
1614          * @return string URL.
1615          */
1616         public function add_state_query_params( $url ) {
1617                 $parsed_original_url = wp_parse_url( $url );
1618                 $is_allowed = false;
1619                 foreach ( $this->get_allowed_urls() as $allowed_url ) {
1620                         $parsed_allowed_url = wp_parse_url( $allowed_url );
1621                         $is_allowed = (
1622                                 $parsed_allowed_url['scheme'] === $parsed_original_url['scheme']
1623                                 &&
1624                                 $parsed_allowed_url['host'] === $parsed_original_url['host']
1625                                 &&
1626                                 0 === strpos( $parsed_original_url['path'], $parsed_allowed_url['path'] )
1627                         );
1628                         if ( $is_allowed ) {
1629                                 break;
1630                         }
1631                 }
1632
1633                 if ( $is_allowed ) {
1634                         $query_params = array(
1635                                 'customize_changeset_uuid' => $this->changeset_uuid(),
1636                         );
1637                         if ( ! $this->is_theme_active() ) {
1638                                 $query_params['customize_theme'] = $this->get_stylesheet();
1639                         }
1640                         if ( $this->messenger_channel ) {
1641                                 $query_params['customize_messenger_channel'] = $this->messenger_channel;
1642                         }
1643                         $url = add_query_arg( $query_params, $url );
1644                 }
1645
1646                 return $url;
1647         }
1648
1649         /**
1650          * Prevent sending a 404 status when returning the response for the customize
1651          * preview, since it causes the jQuery Ajax to fail. Send 200 instead.
1652          *
1653          * @since 4.0.0
1654          * @deprecated 4.7.0
1655          * @access public
1656          */
1657         public function customize_preview_override_404_status() {
1658                 _deprecated_function( __METHOD__, '4.7.0' );
1659         }
1660
1661         /**
1662          * Print base element for preview frame.
1663          *
1664          * @since 3.4.0
1665          * @deprecated 4.7.0
1666          */
1667         public function customize_preview_base() {
1668                 _deprecated_function( __METHOD__, '4.7.0' );
1669         }
1670
1671         /**
1672          * Print a workaround to handle HTML5 tags in IE < 9.
1673          *
1674          * @since 3.4.0
1675          * @deprecated 4.7.0 Customizer no longer supports IE8, so all supported browsers recognize HTML5.
1676          */
1677         public function customize_preview_html5() {
1678                 _deprecated_function( __FUNCTION__, '4.7.0' );
1679         }
1680
1681         /**
1682          * Print CSS for loading indicators for the Customizer preview.
1683          *
1684          * @since 4.2.0
1685          * @access public
1686          */
1687         public function customize_preview_loading_style() {
1688                 ?><style>
1689                         body.wp-customizer-unloading {
1690                                 opacity: 0.25;
1691                                 cursor: progress !important;
1692                                 -webkit-transition: opacity 0.5s;
1693                                 transition: opacity 0.5s;
1694                         }
1695                         body.wp-customizer-unloading * {
1696                                 pointer-events: none !important;
1697                         }
1698                         form.customize-unpreviewable,
1699                         form.customize-unpreviewable input,
1700                         form.customize-unpreviewable select,
1701                         form.customize-unpreviewable button,
1702                         a.customize-unpreviewable,
1703                         area.customize-unpreviewable {
1704                                 cursor: not-allowed !important;
1705                         }
1706                 </style><?php
1707         }
1708
1709         /**
1710          * Remove customize_messenger_channel query parameter from the preview window when it is not in an iframe.
1711          *
1712          * This ensures that the admin bar will be shown. It also ensures that link navigation will
1713          * work as expected since the parent frame is not being sent the URL to navigate to.
1714          *
1715          * @since 4.7.0
1716          * @access public
1717          */
1718         public function remove_frameless_preview_messenger_channel() {
1719                 if ( ! $this->messenger_channel ) {
1720                         return;
1721                 }
1722                 ?>
1723                 <script>
1724                 ( function() {
1725                         var urlParser, oldQueryParams, newQueryParams, i;
1726                         if ( parent !== window ) {
1727                                 return;
1728                         }
1729                         urlParser = document.createElement( 'a' );
1730                         urlParser.href = location.href;
1731                         oldQueryParams = urlParser.search.substr( 1 ).split( /&/ );
1732                         newQueryParams = [];
1733                         for ( i = 0; i < oldQueryParams.length; i += 1 ) {
1734                                 if ( ! /^customize_messenger_channel=/.test( oldQueryParams[ i ] ) ) {
1735                                         newQueryParams.push( oldQueryParams[ i ] );
1736                                 }
1737                         }
1738                         urlParser.search = newQueryParams.join( '&' );
1739                         if ( urlParser.search !== location.search ) {
1740                                 location.replace( urlParser.href );
1741                         }
1742                 } )();
1743                 </script>
1744                 <?php
1745         }
1746
1747         /**
1748          * Print JavaScript settings for preview frame.
1749          *
1750          * @since 3.4.0
1751          */
1752         public function customize_preview_settings() {
1753                 $post_values = $this->unsanitized_post_values( array( 'exclude_changeset' => true ) );
1754                 $setting_validities = $this->validate_setting_values( $post_values );
1755                 $exported_setting_validities = array_map( array( $this, 'prepare_setting_validity_for_js' ), $setting_validities );
1756
1757                 // Note that the REQUEST_URI is not passed into home_url() since this breaks subdirectory installs.
1758                 $self_url = empty( $_SERVER['REQUEST_URI'] ) ? home_url( '/' ) : esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) );
1759                 $state_query_params = array(
1760                         'customize_theme',
1761                         'customize_changeset_uuid',
1762                         'customize_messenger_channel',
1763                 );
1764                 $self_url = remove_query_arg( $state_query_params, $self_url );
1765
1766                 $allowed_urls = $this->get_allowed_urls();
1767                 $allowed_hosts = array();
1768                 foreach ( $allowed_urls as $allowed_url ) {
1769                         $parsed = wp_parse_url( $allowed_url );
1770                         if ( empty( $parsed['host'] ) ) {
1771                                 continue;
1772                         }
1773                         $host = $parsed['host'];
1774                         if ( ! empty( $parsed['port'] ) ) {
1775                                 $host .= ':' . $parsed['port'];
1776                         }
1777                         $allowed_hosts[] = $host;
1778                 }
1779                 $settings = array(
1780                         'changeset' => array(
1781                                 'uuid' => $this->_changeset_uuid,
1782                         ),
1783                         'timeouts' => array(
1784                                 'selectiveRefresh' => 250,
1785                                 'keepAliveSend' => 1000,
1786                         ),
1787                         'theme' => array(
1788                                 'stylesheet' => $this->get_stylesheet(),
1789                                 'active'     => $this->is_theme_active(),
1790                         ),
1791                         'url' => array(
1792                                 'self' => $self_url,
1793                                 'allowed' => array_map( 'esc_url_raw', $this->get_allowed_urls() ),
1794                                 'allowedHosts' => array_unique( $allowed_hosts ),
1795                                 'isCrossDomain' => $this->is_cross_domain(),
1796                         ),
1797                         'channel' => $this->messenger_channel,
1798                         'activePanels' => array(),
1799                         'activeSections' => array(),
1800                         'activeControls' => array(),
1801                         'settingValidities' => $exported_setting_validities,
1802                         'nonce' => current_user_can( 'customize' ) ? $this->get_nonces() : array(),
1803                         'l10n' => array(
1804                                 'shiftClickToEdit' => __( 'Shift-click to edit this element.' ),
1805                                 'linkUnpreviewable' => __( 'This link is not live-previewable.' ),
1806                                 'formUnpreviewable' => __( 'This form is not live-previewable.' ),
1807                         ),
1808                         '_dirty' => array_keys( $post_values ),
1809                 );
1810
1811                 foreach ( $this->panels as $panel_id => $panel ) {
1812                         if ( $panel->check_capabilities() ) {
1813                                 $settings['activePanels'][ $panel_id ] = $panel->active();
1814                                 foreach ( $panel->sections as $section_id => $section ) {
1815                                         if ( $section->check_capabilities() ) {
1816                                                 $settings['activeSections'][ $section_id ] = $section->active();
1817                                         }
1818                                 }
1819                         }
1820                 }
1821                 foreach ( $this->sections as $id => $section ) {
1822                         if ( $section->check_capabilities() ) {
1823                                 $settings['activeSections'][ $id ] = $section->active();
1824                         }
1825                 }
1826                 foreach ( $this->controls as $id => $control ) {
1827                         if ( $control->check_capabilities() ) {
1828                                 $settings['activeControls'][ $id ] = $control->active();
1829                         }
1830                 }
1831
1832                 ?>
1833                 <script type="text/javascript">
1834                         var _wpCustomizeSettings = <?php echo wp_json_encode( $settings ); ?>;
1835                         _wpCustomizeSettings.values = {};
1836                         (function( v ) {
1837                                 <?php
1838                                 /*
1839                                  * Serialize settings separately from the initial _wpCustomizeSettings
1840                                  * serialization in order to avoid a peak memory usage spike.
1841                                  * @todo We may not even need to export the values at all since the pane syncs them anyway.
1842                                  */
1843                                 foreach ( $this->settings as $id => $setting ) {
1844                                         if ( $setting->check_capabilities() ) {
1845                                                 printf(
1846                                                         "v[%s] = %s;\n",
1847                                                         wp_json_encode( $id ),
1848                                                         wp_json_encode( $setting->js_value() )
1849                                                 );
1850                                         }
1851                                 }
1852                                 ?>
1853                         })( _wpCustomizeSettings.values );
1854                 </script>
1855                 <?php
1856         }
1857
1858         /**
1859          * Prints a signature so we can ensure the Customizer was properly executed.
1860          *
1861          * @since 3.4.0
1862          * @deprecated 4.7.0
1863          */
1864         public function customize_preview_signature() {
1865                 _deprecated_function( __METHOD__, '4.7.0' );
1866         }
1867
1868         /**
1869          * Removes the signature in case we experience a case where the Customizer was not properly executed.
1870          *
1871          * @since 3.4.0
1872          * @deprecated 4.7.0
1873          *
1874          * @param mixed $return Value passed through for {@see 'wp_die_handler'} filter.
1875          * @return mixed Value passed through for {@see 'wp_die_handler'} filter.
1876          */
1877         public function remove_preview_signature( $return = null ) {
1878                 _deprecated_function( __METHOD__, '4.7.0' );
1879
1880                 return $return;
1881         }
1882
1883         /**
1884          * Is it a theme preview?
1885          *
1886          * @since 3.4.0
1887          *
1888          * @return bool True if it's a preview, false if not.
1889          */
1890         public function is_preview() {
1891                 return (bool) $this->previewing;
1892         }
1893
1894         /**
1895          * Retrieve the template name of the previewed theme.
1896          *
1897          * @since 3.4.0
1898          *
1899          * @return string Template name.
1900          */
1901         public function get_template() {
1902                 return $this->theme()->get_template();
1903         }
1904
1905         /**
1906          * Retrieve the stylesheet name of the previewed theme.
1907          *
1908          * @since 3.4.0
1909          *
1910          * @return string Stylesheet name.
1911          */
1912         public function get_stylesheet() {
1913                 return $this->theme()->get_stylesheet();
1914         }
1915
1916         /**
1917          * Retrieve the template root of the previewed theme.
1918          *
1919          * @since 3.4.0
1920          *
1921          * @return string Theme root.
1922          */
1923         public function get_template_root() {
1924                 return get_raw_theme_root( $this->get_template(), true );
1925         }
1926
1927         /**
1928          * Retrieve the stylesheet root of the previewed theme.
1929          *
1930          * @since 3.4.0
1931          *
1932          * @return string Theme root.
1933          */
1934         public function get_stylesheet_root() {
1935                 return get_raw_theme_root( $this->get_stylesheet(), true );
1936         }
1937
1938         /**
1939          * Filters the current theme and return the name of the previewed theme.
1940          *
1941          * @since 3.4.0
1942          *
1943          * @param $current_theme {@internal Parameter is not used}
1944          * @return string Theme name.
1945          */
1946         public function current_theme( $current_theme ) {
1947                 return $this->theme()->display('Name');
1948         }
1949
1950         /**
1951          * Validates setting values.
1952          *
1953          * Validation is skipped for unregistered settings or for values that are
1954          * already null since they will be skipped anyway. Sanitization is applied
1955          * to values that pass validation, and values that become null or `WP_Error`
1956          * after sanitizing are marked invalid.
1957          *
1958          * @since 4.6.0
1959          * @access public
1960          *
1961          * @see WP_REST_Request::has_valid_params()
1962          * @see WP_Customize_Setting::validate()
1963          *
1964          * @param array $setting_values Mapping of setting IDs to values to validate and sanitize.
1965          * @param array $options {
1966          *     Options.
1967          *
1968          *     @type bool $validate_existence  Whether a setting's existence will be checked.
1969          *     @type bool $validate_capability Whether the setting capability will be checked.
1970          * }
1971          * @return array Mapping of setting IDs to return value of validate method calls, either `true` or `WP_Error`.
1972          */
1973         public function validate_setting_values( $setting_values, $options = array() ) {
1974                 $options = wp_parse_args( $options, array(
1975                         'validate_capability' => false,
1976                         'validate_existence' => false,
1977                 ) );
1978
1979                 $validities = array();
1980                 foreach ( $setting_values as $setting_id => $unsanitized_value ) {
1981                         $setting = $this->get_setting( $setting_id );
1982                         if ( ! $setting ) {
1983                                 if ( $options['validate_existence'] ) {
1984                                         $validities[ $setting_id ] = new WP_Error( 'unrecognized', __( 'Setting does not exist or is unrecognized.' ) );
1985                                 }
1986                                 continue;
1987                         }
1988                         if ( $options['validate_capability'] && ! current_user_can( $setting->capability ) ) {
1989                                 $validity = new WP_Error( 'unauthorized', __( 'Unauthorized to modify setting due to capability.' ) );
1990                         } else {
1991                                 if ( is_null( $unsanitized_value ) ) {
1992                                         continue;
1993                                 }
1994                                 $validity = $setting->validate( $unsanitized_value );
1995                         }
1996                         if ( ! is_wp_error( $validity ) ) {
1997                                 /** This filter is documented in wp-includes/class-wp-customize-setting.php */
1998                                 $late_validity = apply_filters( "customize_validate_{$setting->id}", new WP_Error(), $unsanitized_value, $setting );
1999                                 if ( ! empty( $late_validity->errors ) ) {
2000                                         $validity = $late_validity;
2001                                 }
2002                         }
2003                         if ( ! is_wp_error( $validity ) ) {
2004                                 $value = $setting->sanitize( $unsanitized_value );
2005                                 if ( is_null( $value ) ) {
2006                                         $validity = false;
2007                                 } elseif ( is_wp_error( $value ) ) {
2008                                         $validity = $value;
2009                                 }
2010                         }
2011                         if ( false === $validity ) {
2012                                 $validity = new WP_Error( 'invalid_value', __( 'Invalid value.' ) );
2013                         }
2014                         $validities[ $setting_id ] = $validity;
2015                 }
2016                 return $validities;
2017         }
2018
2019         /**
2020          * Prepares setting validity for exporting to the client (JS).
2021          *
2022          * Converts `WP_Error` instance into array suitable for passing into the
2023          * `wp.customize.Notification` JS model.
2024          *
2025          * @since 4.6.0
2026          * @access public
2027          *
2028          * @param true|WP_Error $validity Setting validity.
2029          * @return true|array If `$validity` was a WP_Error, the error codes will be array-mapped
2030          *                    to their respective `message` and `data` to pass into the
2031          *                    `wp.customize.Notification` JS model.
2032          */
2033         public function prepare_setting_validity_for_js( $validity ) {
2034                 if ( is_wp_error( $validity ) ) {
2035                         $notification = array();
2036                         foreach ( $validity->errors as $error_code => $error_messages ) {
2037                                 $notification[ $error_code ] = array(
2038                                         'message' => join( ' ', $error_messages ),
2039                                         'data' => $validity->get_error_data( $error_code ),
2040                                 );
2041                         }
2042                         return $notification;
2043                 } else {
2044                         return true;
2045                 }
2046         }
2047
2048         /**
2049          * Handle customize_save WP Ajax request to save/update a changeset.
2050          *
2051          * @since 3.4.0
2052          * @since 4.7.0 The semantics of this method have changed to update a changeset, optionally to also change the status and other attributes.
2053          */
2054         public function save() {
2055                 if ( ! is_user_logged_in() ) {
2056                         wp_send_json_error( 'unauthenticated' );
2057                 }
2058
2059                 if ( ! $this->is_preview() ) {
2060                         wp_send_json_error( 'not_preview' );
2061                 }
2062
2063                 $action = 'save-customize_' . $this->get_stylesheet();
2064                 if ( ! check_ajax_referer( $action, 'nonce', false ) ) {
2065                         wp_send_json_error( 'invalid_nonce' );
2066                 }
2067
2068                 $changeset_post_id = $this->changeset_post_id();
2069                 if ( empty( $changeset_post_id ) ) {
2070                         if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->create_posts ) ) {
2071                                 wp_send_json_error( 'cannot_create_changeset_post' );
2072                         }
2073                 } else {
2074                         if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->edit_post, $changeset_post_id ) ) {
2075                                 wp_send_json_error( 'cannot_edit_changeset_post' );
2076                         }
2077                 }
2078
2079                 if ( ! empty( $_POST['customize_changeset_data'] ) ) {
2080                         $input_changeset_data = json_decode( wp_unslash( $_POST['customize_changeset_data'] ), true );
2081                         if ( ! is_array( $input_changeset_data ) ) {
2082                                 wp_send_json_error( 'invalid_customize_changeset_data' );
2083                         }
2084                 } else {
2085                         $input_changeset_data = array();
2086                 }
2087
2088                 // Validate title.
2089                 $changeset_title = null;
2090                 if ( isset( $_POST['customize_changeset_title'] ) ) {
2091                         $changeset_title = sanitize_text_field( wp_unslash( $_POST['customize_changeset_title'] ) );
2092                 }
2093
2094                 // Validate changeset status param.
2095                 $is_publish = null;
2096                 $changeset_status = null;
2097                 if ( isset( $_POST['customize_changeset_status'] ) ) {
2098                         $changeset_status = wp_unslash( $_POST['customize_changeset_status'] );
2099                         if ( ! get_post_status_object( $changeset_status ) || ! in_array( $changeset_status, array( 'draft', 'pending', 'publish', 'future' ), true ) ) {
2100                                 wp_send_json_error( 'bad_customize_changeset_status', 400 );
2101                         }
2102                         $is_publish = ( 'publish' === $changeset_status || 'future' === $changeset_status );
2103                         if ( $is_publish && ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->publish_posts ) ) {
2104                                 wp_send_json_error( 'changeset_publish_unauthorized', 403 );
2105                         }
2106                 }
2107
2108                 /*
2109                  * Validate changeset date param. Date is assumed to be in local time for
2110                  * the WP if in MySQL format (YYYY-MM-DD HH:MM:SS). Otherwise, the date
2111                  * is parsed with strtotime() so that ISO date format may be supplied
2112                  * or a string like "+10 minutes".
2113                  */
2114                 $changeset_date_gmt = null;
2115                 if ( isset( $_POST['customize_changeset_date'] ) ) {
2116                         $changeset_date = wp_unslash( $_POST['customize_changeset_date'] );
2117                         if ( preg_match( '/^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d$/', $changeset_date ) ) {
2118                                 $mm = substr( $changeset_date, 5, 2 );
2119                                 $jj = substr( $changeset_date, 8, 2 );
2120                                 $aa = substr( $changeset_date, 0, 4 );
2121                                 $valid_date = wp_checkdate( $mm, $jj, $aa, $changeset_date );
2122                                 if ( ! $valid_date ) {
2123                                         wp_send_json_error( 'bad_customize_changeset_date', 400 );
2124                                 }
2125                                 $changeset_date_gmt = get_gmt_from_date( $changeset_date );
2126                         } else {
2127                                 $timestamp = strtotime( $changeset_date );
2128                                 if ( ! $timestamp ) {
2129                                         wp_send_json_error( 'bad_customize_changeset_date', 400 );
2130                                 }
2131                                 $changeset_date_gmt = gmdate( 'Y-m-d H:i:s', $timestamp );
2132                         }
2133                 }
2134
2135                 $r = $this->save_changeset_post( array(
2136                         'status' => $changeset_status,
2137                         'title' => $changeset_title,
2138                         'date_gmt' => $changeset_date_gmt,
2139                         'data' => $input_changeset_data,
2140                 ) );
2141                 if ( is_wp_error( $r ) ) {
2142                         $response = array(
2143                                 'message' => $r->get_error_message(),
2144                                 'code' => $r->get_error_code(),
2145                         );
2146                         if ( is_array( $r->get_error_data() ) ) {
2147                                 $response = array_merge( $response, $r->get_error_data() );
2148                         } else {
2149                                 $response['data'] = $r->get_error_data();
2150                         }
2151                 } else {
2152                         $response = $r;
2153
2154                         // Note that if the changeset status was publish, then it will get set to trash if revisions are not supported.
2155                         $response['changeset_status'] = get_post_status( $this->changeset_post_id() );
2156                         if ( $is_publish && 'trash' === $response['changeset_status'] ) {
2157                                 $response['changeset_status'] = 'publish';
2158                         }
2159
2160                         if ( 'publish' === $response['changeset_status'] ) {
2161                                 $response['next_changeset_uuid'] = wp_generate_uuid4();
2162                         }
2163                 }
2164
2165                 if ( isset( $response['setting_validities'] ) ) {
2166                         $response['setting_validities'] = array_map( array( $this, 'prepare_setting_validity_for_js' ), $response['setting_validities'] );
2167                 }
2168
2169                 /**
2170                  * Filters response data for a successful customize_save Ajax request.
2171                  *
2172                  * This filter does not apply if there was a nonce or authentication failure.
2173                  *
2174                  * @since 4.2.0
2175                  *
2176                  * @param array                $response Additional information passed back to the 'saved'
2177                  *                                       event on `wp.customize`.
2178                  * @param WP_Customize_Manager $this     WP_Customize_Manager instance.
2179                  */
2180                 $response = apply_filters( 'customize_save_response', $response, $this );
2181
2182                 if ( is_wp_error( $r ) ) {
2183                         wp_send_json_error( $response );
2184                 } else {
2185                         wp_send_json_success( $response );
2186                 }
2187         }
2188
2189         /**
2190          * Save the post for the loaded changeset.
2191          *
2192          * @since 4.7.0
2193          * @access public
2194          *
2195          * @param array $args {
2196          *     Args for changeset post.
2197          *
2198          *     @type array  $data            Optional additional changeset data. Values will be merged on top of any existing post values.
2199          *     @type string $status          Post status. Optional. If supplied, the save will be transactional and a post revision will be allowed.
2200          *     @type string $title           Post title. Optional.
2201          *     @type string $date_gmt        Date in GMT. Optional.
2202          *     @type int    $user_id         ID for user who is saving the changeset. Optional, defaults to the current user ID.
2203          *     @type bool   $starter_content Whether the data is starter content. If false (default), then $starter_content will be cleared for any $data being saved.
2204          * }
2205          *
2206          * @return array|WP_Error Returns array on success and WP_Error with array data on error.
2207          */
2208         function save_changeset_post( $args = array() ) {
2209
2210                 $args = array_merge(
2211                         array(
2212                                 'status' => null,
2213                                 'title' => null,
2214                                 'data' => array(),
2215                                 'date_gmt' => null,
2216                                 'user_id' => get_current_user_id(),
2217                                 'starter_content' => false,
2218                         ),
2219                         $args
2220                 );
2221
2222                 $changeset_post_id = $this->changeset_post_id();
2223                 $existing_changeset_data = array();
2224                 if ( $changeset_post_id ) {
2225                         $existing_status = get_post_status( $changeset_post_id );
2226                         if ( 'publish' === $existing_status || 'trash' === $existing_status ) {
2227                                 return new WP_Error( 'changeset_already_published' );
2228                         }
2229
2230                         $existing_changeset_data = $this->get_changeset_post_data( $changeset_post_id );
2231                 }
2232
2233                 // Fail if attempting to publish but publish hook is missing.
2234                 if ( 'publish' === $args['status'] && false === has_action( 'transition_post_status', '_wp_customize_publish_changeset' ) ) {
2235                         return new WP_Error( 'missing_publish_callback' );
2236                 }
2237
2238                 // Validate date.
2239                 $now = gmdate( 'Y-m-d H:i:59' );
2240                 if ( $args['date_gmt'] ) {
2241                         $is_future_dated = ( mysql2date( 'U', $args['date_gmt'], false ) > mysql2date( 'U', $now, false ) );
2242                         if ( ! $is_future_dated ) {
2243                                 return new WP_Error( 'not_future_date' ); // Only future dates are allowed.
2244                         }
2245
2246                         if ( ! $this->is_theme_active() && ( 'future' === $args['status'] || $is_future_dated ) ) {
2247                                 return new WP_Error( 'cannot_schedule_theme_switches' ); // This should be allowed in the future, when theme is a regular setting.
2248                         }
2249                         $will_remain_auto_draft = ( ! $args['status'] && ( ! $changeset_post_id || 'auto-draft' === get_post_status( $changeset_post_id ) ) );
2250                         if ( $will_remain_auto_draft ) {
2251                                 return new WP_Error( 'cannot_supply_date_for_auto_draft_changeset' );
2252                         }
2253                 } elseif ( $changeset_post_id && 'future' === $args['status'] ) {
2254
2255                         // Fail if the new status is future but the existing post's date is not in the future.
2256                         $changeset_post = get_post( $changeset_post_id );
2257                         if ( mysql2date( 'U', $changeset_post->post_date_gmt, false ) <= mysql2date( 'U', $now, false ) ) {
2258                                 return new WP_Error( 'not_future_date' );
2259                         }
2260                 }
2261
2262                 // The request was made via wp.customize.previewer.save().
2263                 $update_transactionally = (bool) $args['status'];
2264                 $allow_revision = (bool) $args['status'];
2265
2266                 // Amend post values with any supplied data.
2267                 foreach ( $args['data'] as $setting_id => $setting_params ) {
2268                         if ( array_key_exists( 'value', $setting_params ) ) {
2269                                 $this->set_post_value( $setting_id, $setting_params['value'] ); // Add to post values so that they can be validated and sanitized.
2270                         }
2271                 }
2272
2273                 // Note that in addition to post data, this will include any stashed theme mods.
2274                 $post_values = $this->unsanitized_post_values( array(
2275                         'exclude_changeset' => true,
2276                         'exclude_post_data' => false,
2277                 ) );
2278                 $this->add_dynamic_settings( array_keys( $post_values ) ); // Ensure settings get created even if they lack an input value.
2279
2280                 /*
2281                  * Get list of IDs for settings that have values different from what is currently
2282                  * saved in the changeset. By skipping any values that are already the same, the
2283                  * subset of changed settings can be passed into validate_setting_values to prevent
2284                  * an underprivileged modifying a single setting for which they have the capability
2285                  * from being blocked from saving. This also prevents a user from touching of the
2286                  * previous saved settings and overriding the associated user_id if they made no change.
2287                  */
2288                 $changed_setting_ids = array();
2289                 foreach ( $post_values as $setting_id => $setting_value ) {
2290                         $setting = $this->get_setting( $setting_id );
2291
2292                         if ( $setting && 'theme_mod' === $setting->type ) {
2293                                 $prefixed_setting_id = $this->get_stylesheet() . '::' . $setting->id;
2294                         } else {
2295                                 $prefixed_setting_id = $setting_id;
2296                         }
2297
2298                         $is_value_changed = (
2299                                 ! isset( $existing_changeset_data[ $prefixed_setting_id ] )
2300                                 ||
2301                                 ! array_key_exists( 'value', $existing_changeset_data[ $prefixed_setting_id ] )
2302                                 ||
2303                                 $existing_changeset_data[ $prefixed_setting_id ]['value'] !== $setting_value
2304                         );
2305                         if ( $is_value_changed ) {
2306                                 $changed_setting_ids[] = $setting_id;
2307                         }
2308                 }
2309
2310                 /**
2311                  * Fires before save validation happens.
2312                  *
2313                  * Plugins can add just-in-time {@see 'customize_validate_{$this->ID}'} filters
2314                  * at this point to catch any settings registered after `customize_register`.
2315                  * The dynamic portion of the hook name, `$this->ID` refers to the setting ID.
2316                  *
2317                  * @since 4.6.0
2318                  *
2319                  * @param WP_Customize_Manager $this WP_Customize_Manager instance.
2320                  */
2321                 do_action( 'customize_save_validation_before', $this );
2322
2323                 // Validate settings.
2324                 $validated_values = array_merge(
2325                         array_fill_keys( array_keys( $args['data'] ), null ), // Make sure existence/capability checks are done on value-less setting updates.
2326                         $post_values
2327                 );
2328                 $setting_validities = $this->validate_setting_values( $validated_values, array(
2329                         'validate_capability' => true,
2330                         'validate_existence' => true,
2331                 ) );
2332                 $invalid_setting_count = count( array_filter( $setting_validities, 'is_wp_error' ) );
2333
2334                 /*
2335                  * Short-circuit if there are invalid settings the update is transactional.
2336                  * A changeset update is transactional when a status is supplied in the request.
2337                  */
2338                 if ( $update_transactionally && $invalid_setting_count > 0 ) {
2339                         $response = array(
2340                                 'setting_validities' => $setting_validities,
2341                                 'message' => sprintf( _n( 'There is %s invalid setting.', 'There are %s invalid settings.', $invalid_setting_count ), number_format_i18n( $invalid_setting_count ) ),
2342                         );
2343                         return new WP_Error( 'transaction_fail', '', $response );
2344                 }
2345
2346                 // Obtain/merge data for changeset.
2347                 $original_changeset_data = $this->get_changeset_post_data( $changeset_post_id );
2348                 $data = $original_changeset_data;
2349                 if ( is_wp_error( $data ) ) {
2350                         $data = array();
2351                 }
2352
2353                 // Ensure that all post values are included in the changeset data.
2354                 foreach ( $post_values as $setting_id => $post_value ) {
2355                         if ( ! isset( $args['data'][ $setting_id ] ) ) {
2356                                 $args['data'][ $setting_id ] = array();
2357                         }
2358                         if ( ! isset( $args['data'][ $setting_id ]['value'] ) ) {
2359                                 $args['data'][ $setting_id ]['value'] = $post_value;
2360                         }
2361                 }
2362
2363                 foreach ( $args['data'] as $setting_id => $setting_params ) {
2364                         $setting = $this->get_setting( $setting_id );
2365                         if ( ! $setting || ! $setting->check_capabilities() ) {
2366                                 continue;
2367                         }
2368
2369                         // Skip updating changeset for invalid setting values.
2370                         if ( isset( $setting_validities[ $setting_id ] ) && is_wp_error( $setting_validities[ $setting_id ] ) ) {
2371                                 continue;
2372                         }
2373
2374                         $changeset_setting_id = $setting_id;
2375                         if ( 'theme_mod' === $setting->type ) {
2376                                 $changeset_setting_id = sprintf( '%s::%s', $this->get_stylesheet(), $setting_id );
2377                         }
2378
2379                         if ( null === $setting_params ) {
2380                                 // Remove setting from changeset entirely.
2381                                 unset( $data[ $changeset_setting_id ] );
2382                         } else {
2383
2384                                 if ( ! isset( $data[ $changeset_setting_id ] ) ) {
2385                                         $data[ $changeset_setting_id ] = array();
2386                                 }
2387
2388                                 // Merge any additional setting params that have been supplied with the existing params.
2389                                 $merged_setting_params = array_merge( $data[ $changeset_setting_id ], $setting_params );
2390
2391                                 // Skip updating setting params if unchanged (ensuring the user_id is not overwritten).
2392                                 if ( $data[ $changeset_setting_id ] === $merged_setting_params ) {
2393                                         continue;
2394                                 }
2395
2396                                 $data[ $changeset_setting_id ] = array_merge(
2397                                         $merged_setting_params,
2398                                         array(
2399                                                 'type' => $setting->type,
2400                                                 'user_id' => $args['user_id'],
2401                                         )
2402                                 );
2403
2404                                 // Clear starter_content flag in data if changeset is not explicitly being updated for starter content.
2405                                 if ( empty( $args['starter_content'] ) ) {
2406                                         unset( $data[ $changeset_setting_id ]['starter_content'] );
2407                                 }
2408                         }
2409                 }
2410
2411                 $filter_context = array(
2412                         'uuid' => $this->changeset_uuid(),
2413                         'title' => $args['title'],
2414                         'status' => $args['status'],
2415                         'date_gmt' => $args['date_gmt'],
2416                         'post_id' => $changeset_post_id,
2417                         'previous_data' => is_wp_error( $original_changeset_data ) ? array() : $original_changeset_data,
2418                         'manager' => $this,
2419                 );
2420
2421                 /**
2422                  * Filters the settings' data that will be persisted into the changeset.
2423                  *
2424                  * Plugins may amend additional data (such as additional meta for settings) into the changeset with this filter.
2425                  *
2426                  * @since 4.7.0
2427                  *
2428                  * @param array $data Updated changeset data, mapping setting IDs to arrays containing a $value item and optionally other metadata.
2429                  * @param array $context {
2430                  *     Filter context.
2431                  *
2432                  *     @type string               $uuid          Changeset UUID.
2433                  *     @type string               $title         Requested title for the changeset post.
2434                  *     @type string               $status        Requested status for the changeset post.
2435                  *     @type string               $date_gmt      Requested date for the changeset post in MySQL format and GMT timezone.
2436                  *     @type int|false            $post_id       Post ID for the changeset, or false if it doesn't exist yet.
2437                  *     @type array                $previous_data Previous data contained in the changeset.
2438                  *     @type WP_Customize_Manager $manager       Manager instance.
2439                  * }
2440                  */
2441                 $data = apply_filters( 'customize_changeset_save_data', $data, $filter_context );
2442
2443                 // Switch theme if publishing changes now.
2444                 if ( 'publish' === $args['status'] && ! $this->is_theme_active() ) {
2445                         // Temporarily stop previewing the theme to allow switch_themes() to operate properly.
2446                         $this->stop_previewing_theme();
2447                         switch_theme( $this->get_stylesheet() );
2448                         update_option( 'theme_switched_via_customizer', true );
2449                         $this->start_previewing_theme();
2450                 }
2451
2452                 // Gather the data for wp_insert_post()/wp_update_post().
2453                 $json_options = 0;
2454                 if ( defined( 'JSON_UNESCAPED_SLASHES' ) ) {
2455                         $json_options |= JSON_UNESCAPED_SLASHES; // Introduced in PHP 5.4. This is only to improve readability as slashes needn't be escaped in storage.
2456                 }
2457                 $json_options |= JSON_PRETTY_PRINT; // Also introduced in PHP 5.4, but WP defines constant for back compat. See WP Trac #30139.
2458                 $post_array = array(
2459                         'post_content' => wp_json_encode( $data, $json_options ),
2460                 );
2461                 if ( $args['title'] ) {
2462                         $post_array['post_title'] = $args['title'];
2463                 }
2464                 if ( $changeset_post_id ) {
2465                         $post_array['ID'] = $changeset_post_id;
2466                 } else {
2467                         $post_array['post_type'] = 'customize_changeset';
2468                         $post_array['post_name'] = $this->changeset_uuid();
2469                         $post_array['post_status'] = 'auto-draft';
2470                 }
2471                 if ( $args['status'] ) {
2472                         $post_array['post_status'] = $args['status'];
2473                 }
2474
2475                 // Reset post date to now if we are publishing, otherwise pass post_date_gmt and translate for post_date.
2476                 if ( 'publish' === $args['status'] ) {
2477                         $post_array['post_date_gmt'] = '0000-00-00 00:00:00';
2478                         $post_array['post_date'] = '0000-00-00 00:00:00';
2479                 } elseif ( $args['date_gmt'] ) {
2480                         $post_array['post_date_gmt'] = $args['date_gmt'];
2481                         $post_array['post_date'] = get_date_from_gmt( $args['date_gmt'] );
2482                 }
2483
2484                 $this->store_changeset_revision = $allow_revision;
2485                 add_filter( 'wp_save_post_revision_post_has_changed', array( $this, '_filter_revision_post_has_changed' ), 5, 3 );
2486
2487                 // Update the changeset post. The publish_customize_changeset action will cause the settings in the changeset to be saved via WP_Customize_Setting::save().
2488                 $has_kses = ( false !== has_filter( 'content_save_pre', 'wp_filter_post_kses' ) );
2489                 if ( $has_kses ) {
2490                         kses_remove_filters(); // Prevent KSES from corrupting JSON in post_content.
2491                 }
2492
2493                 // Note that updating a post with publish status will trigger WP_Customize_Manager::publish_changeset_values().
2494                 if ( $changeset_post_id ) {
2495                         $post_array['edit_date'] = true; // Prevent date clearing.
2496                         $r = wp_update_post( wp_slash( $post_array ), true );
2497                 } else {
2498                         $r = wp_insert_post( wp_slash( $post_array ), true );
2499                         if ( ! is_wp_error( $r ) ) {
2500                                 $this->_changeset_post_id = $r; // Update cached post ID for the loaded changeset.
2501                         }
2502                 }
2503                 if ( $has_kses ) {
2504                         kses_init_filters();
2505                 }
2506                 $this->_changeset_data = null; // Reset so WP_Customize_Manager::changeset_data() will re-populate with updated contents.
2507
2508                 remove_filter( 'wp_save_post_revision_post_has_changed', array( $this, '_filter_revision_post_has_changed' ) );
2509
2510                 $response = array(
2511                         'setting_validities' => $setting_validities,
2512                 );
2513
2514                 if ( is_wp_error( $r ) ) {
2515                         $response['changeset_post_save_failure'] = $r->get_error_code();
2516                         return new WP_Error( 'changeset_post_save_failure', '', $response );
2517                 }
2518
2519                 return $response;
2520         }
2521
2522         /**
2523          * Whether a changeset revision should be made.
2524          *
2525          * @since 4.7.0
2526          * @access private
2527          * @var bool
2528          */
2529         protected $store_changeset_revision;
2530
2531         /**
2532          * Filters whether a changeset has changed to create a new revision.
2533          *
2534          * Note that this will not be called while a changeset post remains in auto-draft status.
2535          *
2536          * @since 4.7.0
2537          * @access private
2538          *
2539          * @param bool    $post_has_changed Whether the post has changed.
2540          * @param WP_Post $last_revision    The last revision post object.
2541          * @param WP_Post $post             The post object.
2542          *
2543          * @return bool Whether a revision should be made.
2544          */
2545         public function _filter_revision_post_has_changed( $post_has_changed, $last_revision, $post ) {
2546                 unset( $last_revision );
2547                 if ( 'customize_changeset' === $post->post_type ) {
2548                         $post_has_changed = $this->store_changeset_revision;
2549                 }
2550                 return $post_has_changed;
2551         }
2552
2553         /**
2554          * Publish changeset values.
2555          *
2556          * This will the values contained in a changeset, even changesets that do not
2557          * correspond to current manager instance. This is called by
2558          * `_wp_customize_publish_changeset()` when a customize_changeset post is
2559          * transitioned to the `publish` status. As such, this method should not be
2560          * called directly and instead `wp_publish_post()` should be used.
2561          *
2562          * Please note that if the settings in the changeset are for a non-activated
2563          * theme, the theme must first be switched to (via `switch_theme()`) before
2564          * invoking this method.
2565          *
2566          * @since 4.7.0
2567          * @access private
2568          * @see _wp_customize_publish_changeset()
2569          *
2570          * @param int $changeset_post_id ID for customize_changeset post. Defaults to the changeset for the current manager instance.
2571          * @return true|WP_Error True or error info.
2572          */
2573         public function _publish_changeset_values( $changeset_post_id ) {
2574                 $publishing_changeset_data = $this->get_changeset_post_data( $changeset_post_id );
2575                 if ( is_wp_error( $publishing_changeset_data ) ) {
2576                         return $publishing_changeset_data;
2577                 }
2578
2579                 $changeset_post = get_post( $changeset_post_id );
2580
2581                 /*
2582                  * Temporarily override the changeset context so that it will be read
2583                  * in calls to unsanitized_post_values() and so that it will be available
2584                  * on the $wp_customize object passed to hooks during the save logic.
2585                  */
2586                 $previous_changeset_post_id = $this->_changeset_post_id;
2587                 $this->_changeset_post_id   = $changeset_post_id;
2588                 $previous_changeset_uuid    = $this->_changeset_uuid;
2589                 $this->_changeset_uuid      = $changeset_post->post_name;
2590                 $previous_changeset_data    = $this->_changeset_data;
2591                 $this->_changeset_data      = $publishing_changeset_data;
2592
2593                 // Parse changeset data to identify theme mod settings and user IDs associated with settings to be saved.
2594                 $setting_user_ids = array();
2595                 $theme_mod_settings = array();
2596                 $namespace_pattern = '/^(?P<stylesheet>.+?)::(?P<setting_id>.+)$/';
2597                 $matches = array();
2598                 foreach ( $this->_changeset_data as $raw_setting_id => $setting_params ) {
2599                         $actual_setting_id = null;
2600                         $is_theme_mod_setting = (
2601                                 isset( $setting_params['value'] )
2602                                 &&
2603                                 isset( $setting_params['type'] )
2604                                 &&
2605                                 'theme_mod' === $setting_params['type']
2606                                 &&
2607                                 preg_match( $namespace_pattern, $raw_setting_id, $matches )
2608                         );
2609                         if ( $is_theme_mod_setting ) {
2610                                 if ( ! isset( $theme_mod_settings[ $matches['stylesheet'] ] ) ) {
2611                                         $theme_mod_settings[ $matches['stylesheet'] ] = array();
2612                                 }
2613                                 $theme_mod_settings[ $matches['stylesheet'] ][ $matches['setting_id'] ] = $setting_params;
2614
2615                                 if ( $this->get_stylesheet() === $matches['stylesheet'] ) {
2616                                         $actual_setting_id = $matches['setting_id'];
2617                                 }
2618                         } else {
2619                                 $actual_setting_id = $raw_setting_id;
2620                         }
2621
2622                         // Keep track of the user IDs for settings actually for this theme.
2623                         if ( $actual_setting_id && isset( $setting_params['user_id'] ) ) {
2624                                 $setting_user_ids[ $actual_setting_id ] = $setting_params['user_id'];
2625                         }
2626                 }
2627
2628                 $changeset_setting_values = $this->unsanitized_post_values( array(
2629                         'exclude_post_data' => true,
2630                         'exclude_changeset' => false,
2631                 ) );
2632                 $changeset_setting_ids = array_keys( $changeset_setting_values );
2633                 $this->add_dynamic_settings( $changeset_setting_ids );
2634
2635                 /**
2636                  * Fires once the theme has switched in the Customizer, but before settings
2637                  * have been saved.
2638                  *
2639                  * @since 3.4.0
2640                  *
2641                  * @param WP_Customize_Manager $manager WP_Customize_Manager instance.
2642                  */
2643                 do_action( 'customize_save', $this );
2644
2645                 /*
2646                  * Ensure that all settings will allow themselves to be saved. Note that
2647                  * this is safe because the setting would have checked the capability
2648                  * when the setting value was written into the changeset. So this is why
2649                  * an additional capability check is not required here.
2650                  */
2651                 $original_setting_capabilities = array();
2652                 foreach ( $changeset_setting_ids as $setting_id ) {
2653                         $setting = $this->get_setting( $setting_id );
2654                         if ( $setting && ! isset( $setting_user_ids[ $setting_id ] ) ) {
2655                                 $original_setting_capabilities[ $setting->id ] = $setting->capability;
2656                                 $setting->capability = 'exist';
2657                         }
2658                 }
2659
2660                 $original_user_id = get_current_user_id();
2661                 foreach ( $changeset_setting_ids as $setting_id ) {
2662                         $setting = $this->get_setting( $setting_id );
2663                         if ( $setting ) {
2664                                 /*
2665                                  * Set the current user to match the user who saved the value into
2666                                  * the changeset so that any filters that apply during the save
2667                                  * process will respect the original user's capabilities. This
2668                                  * will ensure, for example, that KSES won't strip unsafe HTML
2669                                  * when a scheduled changeset publishes via WP Cron.
2670                                  */
2671                                 if ( isset( $setting_user_ids[ $setting_id ] ) ) {
2672                                         wp_set_current_user( $setting_user_ids[ $setting_id ] );
2673                                 } else {
2674                                         wp_set_current_user( $original_user_id );
2675                                 }
2676
2677                                 $setting->save();
2678                         }
2679                 }
2680                 wp_set_current_user( $original_user_id );
2681
2682                 // Update the stashed theme mod settings, removing the active theme's stashed settings, if activated.
2683                 if ( did_action( 'switch_theme' ) ) {
2684                         $other_theme_mod_settings = $theme_mod_settings;
2685                         unset( $other_theme_mod_settings[ $this->get_stylesheet() ] );
2686                         $this->update_stashed_theme_mod_settings( $other_theme_mod_settings );
2687                 }
2688
2689                 /**
2690                  * Fires after Customize settings have been saved.
2691                  *
2692                  * @since 3.6.0
2693                  *
2694                  * @param WP_Customize_Manager $manager WP_Customize_Manager instance.
2695                  */
2696                 do_action( 'customize_save_after', $this );
2697
2698                 // Restore original capabilities.
2699                 foreach ( $original_setting_capabilities as $setting_id => $capability ) {
2700                         $setting = $this->get_setting( $setting_id );
2701                         if ( $setting ) {
2702                                 $setting->capability = $capability;
2703                         }
2704                 }
2705
2706                 // Restore original changeset data.
2707                 $this->_changeset_data    = $previous_changeset_data;
2708                 $this->_changeset_post_id = $previous_changeset_post_id;
2709                 $this->_changeset_uuid    = $previous_changeset_uuid;
2710
2711                 return true;
2712         }
2713
2714         /**
2715          * Update stashed theme mod settings.
2716          *
2717          * @since 4.7.0
2718          * @access private
2719          *
2720          * @param array $inactive_theme_mod_settings Mapping of stylesheet to arrays of theme mod settings.
2721          * @return array|false Returns array of updated stashed theme mods or false if the update failed or there were no changes.
2722          */
2723         protected function update_stashed_theme_mod_settings( $inactive_theme_mod_settings ) {
2724                 $stashed_theme_mod_settings = get_option( 'customize_stashed_theme_mods' );
2725                 if ( empty( $stashed_theme_mod_settings ) ) {
2726                         $stashed_theme_mod_settings = array();
2727                 }
2728
2729                 // Delete any stashed theme mods for the active theme since since they would have been loaded and saved upon activation.
2730                 unset( $stashed_theme_mod_settings[ $this->get_stylesheet() ] );
2731
2732                 // Merge inactive theme mods with the stashed theme mod settings.
2733                 foreach ( $inactive_theme_mod_settings as $stylesheet => $theme_mod_settings ) {
2734                         if ( ! isset( $stashed_theme_mod_settings[ $stylesheet ] ) ) {
2735                                 $stashed_theme_mod_settings[ $stylesheet ] = array();
2736                         }
2737
2738                         $stashed_theme_mod_settings[ $stylesheet ] = array_merge(
2739                                 $stashed_theme_mod_settings[ $stylesheet ],
2740                                 $theme_mod_settings
2741                         );
2742                 }
2743
2744                 $autoload = false;
2745                 $result = update_option( 'customize_stashed_theme_mods', $stashed_theme_mod_settings, $autoload );
2746                 if ( ! $result ) {
2747                         return false;
2748                 }
2749                 return $stashed_theme_mod_settings;
2750         }
2751
2752         /**
2753          * Refresh nonces for the current preview.
2754          *
2755          * @since 4.2.0
2756          */
2757         public function refresh_nonces() {
2758                 if ( ! $this->is_preview() ) {
2759                         wp_send_json_error( 'not_preview' );
2760                 }
2761
2762                 wp_send_json_success( $this->get_nonces() );
2763         }
2764
2765         /**
2766          * Add a customize setting.
2767          *
2768          * @since 3.4.0
2769          * @since 4.5.0 Return added WP_Customize_Setting instance.
2770          * @access public
2771          *
2772          * @param WP_Customize_Setting|string $id   Customize Setting object, or ID.
2773          * @param array                       $args Setting arguments; passed to WP_Customize_Setting
2774          *                                          constructor.
2775          * @return WP_Customize_Setting             The instance of the setting that was added.
2776          */
2777         public function add_setting( $id, $args = array() ) {
2778                 if ( $id instanceof WP_Customize_Setting ) {
2779                         $setting = $id;
2780                 } else {
2781                         $class = 'WP_Customize_Setting';
2782
2783                         /** This filter is documented in wp-includes/class-wp-customize-manager.php */
2784                         $args = apply_filters( 'customize_dynamic_setting_args', $args, $id );
2785
2786                         /** This filter is documented in wp-includes/class-wp-customize-manager.php */
2787                         $class = apply_filters( 'customize_dynamic_setting_class', $class, $id, $args );
2788
2789                         $setting = new $class( $this, $id, $args );
2790                 }
2791
2792                 $this->settings[ $setting->id ] = $setting;
2793                 return $setting;
2794         }
2795
2796         /**
2797          * Register any dynamically-created settings, such as those from $_POST['customized']
2798          * that have no corresponding setting created.
2799          *
2800          * This is a mechanism to "wake up" settings that have been dynamically created
2801          * on the front end and have been sent to WordPress in `$_POST['customized']`. When WP
2802          * loads, the dynamically-created settings then will get created and previewed
2803          * even though they are not directly created statically with code.
2804          *
2805          * @since 4.2.0
2806          * @access public
2807          *
2808          * @param array $setting_ids The setting IDs to add.
2809          * @return array The WP_Customize_Setting objects added.
2810          */
2811         public function add_dynamic_settings( $setting_ids ) {
2812                 $new_settings = array();
2813                 foreach ( $setting_ids as $setting_id ) {
2814                         // Skip settings already created
2815                         if ( $this->get_setting( $setting_id ) ) {
2816                                 continue;
2817                         }
2818
2819                         $setting_args = false;
2820                         $setting_class = 'WP_Customize_Setting';
2821
2822                         /**
2823                          * Filters a dynamic setting's constructor args.
2824                          *
2825                          * For a dynamic setting to be registered, this filter must be employed
2826                          * to override the default false value with an array of args to pass to
2827                          * the WP_Customize_Setting constructor.
2828                          *
2829                          * @since 4.2.0
2830                          *
2831                          * @param false|array $setting_args The arguments to the WP_Customize_Setting constructor.
2832                          * @param string      $setting_id   ID for dynamic setting, usually coming from `$_POST['customized']`.
2833                          */
2834                         $setting_args = apply_filters( 'customize_dynamic_setting_args', $setting_args, $setting_id );
2835                         if ( false === $setting_args ) {
2836                                 continue;
2837                         }
2838
2839                         /**
2840                          * Allow non-statically created settings to be constructed with custom WP_Customize_Setting subclass.
2841                          *
2842                          * @since 4.2.0
2843                          *
2844                          * @param string $setting_class WP_Customize_Setting or a subclass.
2845                          * @param string $setting_id    ID for dynamic setting, usually coming from `$_POST['customized']`.
2846                          * @param array  $setting_args  WP_Customize_Setting or a subclass.
2847                          */
2848                         $setting_class = apply_filters( 'customize_dynamic_setting_class', $setting_class, $setting_id, $setting_args );
2849
2850                         $setting = new $setting_class( $this, $setting_id, $setting_args );
2851
2852                         $this->add_setting( $setting );
2853                         $new_settings[] = $setting;
2854                 }
2855                 return $new_settings;
2856         }
2857
2858         /**
2859          * Retrieve a customize setting.
2860          *
2861          * @since 3.4.0
2862          *
2863          * @param string $id Customize Setting ID.
2864          * @return WP_Customize_Setting|void The setting, if set.
2865          */
2866         public function get_setting( $id ) {
2867                 if ( isset( $this->settings[ $id ] ) ) {
2868                         return $this->settings[ $id ];
2869                 }
2870         }
2871
2872         /**
2873          * Remove a customize setting.
2874          *
2875          * @since 3.4.0
2876          *
2877          * @param string $id Customize Setting ID.
2878          */
2879         public function remove_setting( $id ) {
2880                 unset( $this->settings[ $id ] );
2881         }
2882
2883         /**
2884          * Add a customize panel.
2885          *
2886          * @since 4.0.0
2887          * @since 4.5.0 Return added WP_Customize_Panel instance.
2888          * @access public
2889          *
2890          * @param WP_Customize_Panel|string $id   Customize Panel object, or Panel ID.
2891          * @param array                     $args Optional. Panel arguments. Default empty array.
2892          *
2893          * @return WP_Customize_Panel             The instance of the panel that was added.
2894          */
2895         public function add_panel( $id, $args = array() ) {
2896                 if ( $id instanceof WP_Customize_Panel ) {
2897                         $panel = $id;
2898                 } else {
2899                         $panel = new WP_Customize_Panel( $this, $id, $args );
2900                 }
2901
2902                 $this->panels[ $panel->id ] = $panel;
2903                 return $panel;
2904         }
2905
2906         /**
2907          * Retrieve a customize panel.
2908          *
2909          * @since 4.0.0
2910          * @access public
2911          *
2912          * @param string $id Panel ID to get.
2913          * @return WP_Customize_Panel|void Requested panel instance, if set.
2914          */
2915         public function get_panel( $id ) {
2916                 if ( isset( $this->panels[ $id ] ) ) {
2917                         return $this->panels[ $id ];
2918                 }
2919         }
2920
2921         /**
2922          * Remove a customize panel.
2923          *
2924          * @since 4.0.0
2925          * @access public
2926          *
2927          * @param string $id Panel ID to remove.
2928          */
2929         public function remove_panel( $id ) {
2930                 // Removing core components this way is _doing_it_wrong().
2931                 if ( in_array( $id, $this->components, true ) ) {
2932                         /* translators: 1: panel id, 2: link to 'customize_loaded_components' filter reference */
2933                         $message = sprintf( __( 'Removing %1$s manually will cause PHP warnings. Use the %2$s filter instead.' ),
2934                                 $id,
2935                                 '<a href="' . esc_url( 'https://developer.wordpress.org/reference/hooks/customize_loaded_components/' ) . '"><code>customize_loaded_components</code></a>'
2936                         );
2937
2938                         _doing_it_wrong( __METHOD__, $message, '4.5.0' );
2939                 }
2940                 unset( $this->panels[ $id ] );
2941         }
2942
2943         /**
2944          * Register a customize panel type.
2945          *
2946          * Registered types are eligible to be rendered via JS and created dynamically.
2947          *
2948          * @since 4.3.0
2949          * @access public
2950          *
2951          * @see WP_Customize_Panel
2952          *
2953          * @param string $panel Name of a custom panel which is a subclass of WP_Customize_Panel.
2954          */
2955         public function register_panel_type( $panel ) {
2956                 $this->registered_panel_types[] = $panel;
2957         }
2958
2959         /**
2960          * Render JS templates for all registered panel types.
2961          *
2962          * @since 4.3.0
2963          * @access public
2964          */
2965         public function render_panel_templates() {
2966                 foreach ( $this->registered_panel_types as $panel_type ) {
2967                         $panel = new $panel_type( $this, 'temp', array() );
2968                         $panel->print_template();
2969                 }
2970         }
2971
2972         /**
2973          * Add a customize section.
2974          *
2975          * @since 3.4.0
2976          * @since 4.5.0 Return added WP_Customize_Section instance.
2977          * @access public
2978          *
2979          * @param WP_Customize_Section|string $id   Customize Section object, or Section ID.
2980          * @param array                       $args Section arguments.
2981          *
2982          * @return WP_Customize_Section             The instance of the section that was added.
2983          */
2984         public function add_section( $id, $args = array() ) {
2985                 if ( $id instanceof WP_Customize_Section ) {
2986                         $section = $id;
2987                 } else {
2988                         $section = new WP_Customize_Section( $this, $id, $args );
2989                 }
2990
2991                 $this->sections[ $section->id ] = $section;
2992                 return $section;
2993         }
2994
2995         /**
2996          * Retrieve a customize section.
2997          *
2998          * @since 3.4.0
2999          *
3000          * @param string $id Section ID.
3001          * @return WP_Customize_Section|void The section, if set.
3002          */
3003         public function get_section( $id ) {
3004                 if ( isset( $this->sections[ $id ] ) )
3005                         return $this->sections[ $id ];
3006         }
3007
3008         /**
3009          * Remove a customize section.
3010          *
3011          * @since 3.4.0
3012          *
3013          * @param string $id Section ID.
3014          */
3015         public function remove_section( $id ) {
3016                 unset( $this->sections[ $id ] );
3017         }
3018
3019         /**
3020          * Register a customize section type.
3021          *
3022          * Registered types are eligible to be rendered via JS and created dynamically.
3023          *
3024          * @since 4.3.0
3025          * @access public
3026          *
3027          * @see WP_Customize_Section
3028          *
3029          * @param string $section Name of a custom section which is a subclass of WP_Customize_Section.
3030          */
3031         public function register_section_type( $section ) {
3032                 $this->registered_section_types[] = $section;
3033         }
3034
3035         /**
3036          * Render JS templates for all registered section types.
3037          *
3038          * @since 4.3.0
3039          * @access public
3040          */
3041         public function render_section_templates() {
3042                 foreach ( $this->registered_section_types as $section_type ) {
3043                         $section = new $section_type( $this, 'temp', array() );
3044                         $section->print_template();
3045                 }
3046         }
3047
3048         /**
3049          * Add a customize control.
3050          *
3051          * @since 3.4.0
3052          * @since 4.5.0 Return added WP_Customize_Control instance.
3053          * @access public
3054          *
3055          * @param WP_Customize_Control|string $id   Customize Control object, or ID.
3056          * @param array                       $args Control arguments; passed to WP_Customize_Control
3057          *                                          constructor.
3058          * @return WP_Customize_Control             The instance of the control that was added.
3059          */
3060         public function add_control( $id, $args = array() ) {
3061                 if ( $id instanceof WP_Customize_Control ) {
3062                         $control = $id;
3063                 } else {
3064                         $control = new WP_Customize_Control( $this, $id, $args );
3065                 }
3066
3067                 $this->controls[ $control->id ] = $control;
3068                 return $control;
3069         }
3070
3071         /**
3072          * Retrieve a customize control.
3073          *
3074          * @since 3.4.0
3075          *
3076          * @param string $id ID of the control.
3077          * @return WP_Customize_Control|void The control object, if set.
3078          */
3079         public function get_control( $id ) {
3080                 if ( isset( $this->controls[ $id ] ) )
3081                         return $this->controls[ $id ];
3082         }
3083
3084         /**
3085          * Remove a customize control.
3086          *
3087          * @since 3.4.0
3088          *
3089          * @param string $id ID of the control.
3090          */
3091         public function remove_control( $id ) {
3092                 unset( $this->controls[ $id ] );
3093         }
3094
3095         /**
3096          * Register a customize control type.
3097          *
3098          * Registered types are eligible to be rendered via JS and created dynamically.
3099          *
3100          * @since 4.1.0
3101          * @access public
3102          *
3103          * @param string $control Name of a custom control which is a subclass of
3104          *                        WP_Customize_Control.
3105          */
3106         public function register_control_type( $control ) {
3107                 $this->registered_control_types[] = $control;
3108         }
3109
3110         /**
3111          * Render JS templates for all registered control types.
3112          *
3113          * @since 4.1.0
3114          * @access public
3115          */
3116         public function render_control_templates() {
3117                 foreach ( $this->registered_control_types as $control_type ) {
3118                         $control = new $control_type( $this, 'temp', array(
3119                                 'settings' => array(),
3120                         ) );
3121                         $control->print_template();
3122                 }
3123                 ?>
3124                 <script type="text/html" id="tmpl-customize-control-notifications">
3125                         <ul>
3126                                 <# _.each( data.notifications, function( notification ) { #>
3127                                         <li class="notice notice-{{ notification.type || 'info' }} {{ data.altNotice ? 'notice-alt' : '' }}" data-code="{{ notification.code }}" data-type="{{ notification.type }}">{{{ notification.message || notification.code }}}</li>
3128                                 <# } ); #>
3129                         </ul>
3130                 </script>
3131                 <?php
3132         }
3133
3134         /**
3135          * Helper function to compare two objects by priority, ensuring sort stability via instance_number.
3136          *
3137          * @since 3.4.0
3138          * @deprecated 4.7.0 Use wp_list_sort()
3139          *
3140          * @param WP_Customize_Panel|WP_Customize_Section|WP_Customize_Control $a Object A.
3141          * @param WP_Customize_Panel|WP_Customize_Section|WP_Customize_Control $b Object B.
3142          * @return int
3143          */
3144         protected function _cmp_priority( $a, $b ) {
3145                 _deprecated_function( __METHOD__, '4.7.0', 'wp_list_sort' );
3146
3147                 if ( $a->priority === $b->priority ) {
3148                         return $a->instance_number - $b->instance_number;
3149                 } else {
3150                         return $a->priority - $b->priority;
3151                 }
3152         }
3153
3154         /**
3155          * Prepare panels, sections, and controls.
3156          *
3157          * For each, check if required related components exist,
3158          * whether the user has the necessary capabilities,
3159          * and sort by priority.
3160          *
3161          * @since 3.4.0
3162          */
3163         public function prepare_controls() {
3164
3165                 $controls = array();
3166                 $this->controls = wp_list_sort( $this->controls, array(
3167                         'priority'        => 'ASC',
3168                         'instance_number' => 'ASC',
3169                 ), 'ASC', true );
3170
3171                 foreach ( $this->controls as $id => $control ) {
3172                         if ( ! isset( $this->sections[ $control->section ] ) || ! $control->check_capabilities() ) {
3173                                 continue;
3174                         }
3175
3176                         $this->sections[ $control->section ]->controls[] = $control;
3177                         $controls[ $id ] = $control;
3178                 }
3179                 $this->controls = $controls;
3180
3181                 // Prepare sections.
3182                 $this->sections = wp_list_sort( $this->sections, array(
3183                         'priority'        => 'ASC',
3184                         'instance_number' => 'ASC',
3185                 ), 'ASC', true );
3186                 $sections = array();
3187
3188                 foreach ( $this->sections as $section ) {
3189                         if ( ! $section->check_capabilities() ) {
3190                                 continue;
3191                         }
3192
3193
3194                         $section->controls = wp_list_sort( $section->controls, array(
3195                                 'priority'        => 'ASC',
3196                                 'instance_number' => 'ASC',
3197                         ) );
3198
3199                         if ( ! $section->panel ) {
3200                                 // Top-level section.
3201                                 $sections[ $section->id ] = $section;
3202                         } else {
3203                                 // This section belongs to a panel.
3204                                 if ( isset( $this->panels [ $section->panel ] ) ) {
3205                                         $this->panels[ $section->panel ]->sections[ $section->id ] = $section;
3206                                 }
3207                         }
3208                 }
3209                 $this->sections = $sections;
3210
3211                 // Prepare panels.
3212                 $this->panels = wp_list_sort( $this->panels, array(
3213                         'priority'        => 'ASC',
3214                         'instance_number' => 'ASC',
3215                 ), 'ASC', true );
3216                 $panels = array();
3217
3218                 foreach ( $this->panels as $panel ) {
3219                         if ( ! $panel->check_capabilities() ) {
3220                                 continue;
3221                         }
3222
3223                         $panel->sections = wp_list_sort( $panel->sections, array(
3224                                 'priority'        => 'ASC',
3225                                 'instance_number' => 'ASC',
3226                         ), 'ASC', true );
3227                         $panels[ $panel->id ] = $panel;
3228                 }
3229                 $this->panels = $panels;
3230
3231                 // Sort panels and top-level sections together.
3232                 $this->containers = array_merge( $this->panels, $this->sections );
3233                 $this->containers = wp_list_sort( $this->containers, array(
3234                         'priority'        => 'ASC',
3235                         'instance_number' => 'ASC',
3236                 ), 'ASC', true );
3237         }
3238
3239         /**
3240          * Enqueue scripts for customize controls.
3241          *
3242          * @since 3.4.0
3243          */
3244         public function enqueue_control_scripts() {
3245                 foreach ( $this->controls as $control ) {
3246                         $control->enqueue();
3247                 }
3248         }
3249
3250         /**
3251          * Determine whether the user agent is iOS.
3252          *
3253          * @since 4.4.0
3254          * @access public
3255          *
3256          * @return bool Whether the user agent is iOS.
3257          */
3258         public function is_ios() {
3259                 return wp_is_mobile() && preg_match( '/iPad|iPod|iPhone/', $_SERVER['HTTP_USER_AGENT'] );
3260         }
3261
3262         /**
3263          * Get the template string for the Customizer pane document title.
3264          *
3265          * @since 4.4.0
3266          * @access public
3267          *
3268          * @return string The template string for the document title.
3269          */
3270         public function get_document_title_template() {
3271                 if ( $this->is_theme_active() ) {
3272                         /* translators: %s: document title from the preview */
3273                         $document_title_tmpl = __( 'Customize: %s' );
3274                 } else {
3275                         /* translators: %s: document title from the preview */
3276                         $document_title_tmpl = __( 'Live Preview: %s' );
3277                 }
3278                 $document_title_tmpl = html_entity_decode( $document_title_tmpl, ENT_QUOTES, 'UTF-8' ); // Because exported to JS and assigned to document.title.
3279                 return $document_title_tmpl;
3280         }
3281
3282         /**
3283          * Set the initial URL to be previewed.
3284          *
3285          * URL is validated.
3286          *
3287          * @since 4.4.0
3288          * @access public
3289          *
3290          * @param string $preview_url URL to be previewed.
3291          */
3292         public function set_preview_url( $preview_url ) {
3293                 $preview_url = esc_url_raw( $preview_url );
3294                 $this->preview_url = wp_validate_redirect( $preview_url, home_url( '/' ) );
3295         }
3296
3297         /**
3298          * Get the initial URL to be previewed.
3299          *
3300          * @since 4.4.0
3301          * @access public
3302          *
3303          * @return string URL being previewed.
3304          */
3305         public function get_preview_url() {
3306                 if ( empty( $this->preview_url ) ) {
3307                         $preview_url = home_url( '/' );
3308                 } else {
3309                         $preview_url = $this->preview_url;
3310                 }
3311                 return $preview_url;
3312         }
3313
3314         /**
3315          * Determines whether the admin and the frontend are on different domains.
3316          *
3317          * @since 4.7.0
3318          * @access public
3319          *
3320          * @return bool Whether cross-domain.
3321          */
3322         public function is_cross_domain() {
3323                 $admin_origin = wp_parse_url( admin_url() );
3324                 $home_origin = wp_parse_url( home_url() );
3325                 $cross_domain = ( strtolower( $admin_origin['host'] ) !== strtolower( $home_origin['host'] ) );
3326                 return $cross_domain;
3327         }
3328
3329         /**
3330          * Get URLs allowed to be previewed.
3331          *
3332          * If the front end and the admin are served from the same domain, load the
3333          * preview over ssl if the Customizer is being loaded over ssl. This avoids
3334          * insecure content warnings. This is not attempted if the admin and front end
3335          * are on different domains to avoid the case where the front end doesn't have
3336          * ssl certs. Domain mapping plugins can allow other urls in these conditions
3337          * using the customize_allowed_urls filter.
3338          *
3339          * @since 4.7.0
3340          * @access public
3341          *
3342          * @returns array Allowed URLs.
3343          */
3344         public function get_allowed_urls() {
3345                 $allowed_urls = array( home_url( '/' ) );
3346
3347                 if ( is_ssl() && ! $this->is_cross_domain() ) {
3348                         $allowed_urls[] = home_url( '/', 'https' );
3349                 }
3350
3351                 /**
3352                  * Filters the list of URLs allowed to be clicked and followed in the Customizer preview.
3353                  *
3354                  * @since 3.4.0
3355                  *
3356                  * @param array $allowed_urls An array of allowed URLs.
3357                  */
3358                 $allowed_urls = array_unique( apply_filters( 'customize_allowed_urls', $allowed_urls ) );
3359
3360                 return $allowed_urls;
3361         }
3362
3363         /**
3364          * Get messenger channel.
3365          *
3366          * @since 4.7.0
3367          * @access public
3368          *
3369          * @return string Messenger channel.
3370          */
3371         public function get_messenger_channel() {
3372                 return $this->messenger_channel;
3373         }
3374
3375         /**
3376          * Set URL to link the user to when closing the Customizer.
3377          *
3378          * URL is validated.
3379          *
3380          * @since 4.4.0
3381          * @access public
3382          *
3383          * @param string $return_url URL for return link.
3384          */
3385         public function set_return_url( $return_url ) {
3386                 $return_url = esc_url_raw( $return_url );
3387                 $return_url = remove_query_arg( wp_removable_query_args(), $return_url );
3388                 $return_url = wp_validate_redirect( $return_url );
3389                 $this->return_url = $return_url;
3390         }
3391
3392         /**
3393          * Get URL to link the user to when closing the Customizer.
3394          *
3395          * @since 4.4.0
3396          * @access public
3397          *
3398          * @return string URL for link to close Customizer.
3399          */
3400         public function get_return_url() {
3401                 $referer = wp_get_referer();
3402                 $excluded_referer_basenames = array( 'customize.php', 'wp-login.php' );
3403
3404                 if ( $this->return_url ) {
3405                         $return_url = $this->return_url;
3406                 } else if ( $referer && ! in_array( basename( parse_url( $referer, PHP_URL_PATH ) ), $excluded_referer_basenames, true ) ) {
3407                         $return_url = $referer;
3408                 } else if ( $this->preview_url ) {
3409                         $return_url = $this->preview_url;
3410                 } else {
3411                         $return_url = home_url( '/' );
3412                 }
3413                 return $return_url;
3414         }
3415
3416         /**
3417          * Set the autofocused constructs.
3418          *
3419          * @since 4.4.0
3420          * @access public
3421          *
3422          * @param array $autofocus {
3423          *     Mapping of 'panel', 'section', 'control' to the ID which should be autofocused.
3424          *
3425          *     @type string [$control]  ID for control to be autofocused.
3426          *     @type string [$section]  ID for section to be autofocused.
3427          *     @type string [$panel]    ID for panel to be autofocused.
3428          * }
3429          */
3430         public function set_autofocus( $autofocus ) {
3431                 $this->autofocus = array_filter( wp_array_slice_assoc( $autofocus, array( 'panel', 'section', 'control' ) ), 'is_string' );
3432         }
3433
3434         /**
3435          * Get the autofocused constructs.
3436          *
3437          * @since 4.4.0
3438          * @access public
3439          *
3440          * @return array {
3441          *     Mapping of 'panel', 'section', 'control' to the ID which should be autofocused.
3442          *
3443          *     @type string [$control]  ID for control to be autofocused.
3444          *     @type string [$section]  ID for section to be autofocused.
3445          *     @type string [$panel]    ID for panel to be autofocused.
3446          * }
3447          */
3448         public function get_autofocus() {
3449                 return $this->autofocus;
3450         }
3451
3452         /**
3453          * Get nonces for the Customizer.
3454          *
3455          * @since 4.5.0
3456          * @return array Nonces.
3457          */
3458         public function get_nonces() {
3459                 $nonces = array(
3460                         'save' => wp_create_nonce( 'save-customize_' . $this->get_stylesheet() ),
3461                         'preview' => wp_create_nonce( 'preview-customize_' . $this->get_stylesheet() ),
3462                 );
3463
3464                 /**
3465                  * Filters nonces for Customizer.
3466                  *
3467                  * @since 4.2.0
3468                  *
3469                  * @param array                $nonces Array of refreshed nonces for save and
3470                  *                                     preview actions.
3471                  * @param WP_Customize_Manager $this   WP_Customize_Manager instance.
3472                  */
3473                 $nonces = apply_filters( 'customize_refresh_nonces', $nonces, $this );
3474
3475                 return $nonces;
3476         }
3477
3478         /**
3479          * Print JavaScript settings for parent window.
3480          *
3481          * @since 4.4.0
3482          */
3483         public function customize_pane_settings() {
3484
3485                 $login_url = add_query_arg( array(
3486                         'interim-login' => 1,
3487                         'customize-login' => 1,
3488                 ), wp_login_url() );
3489
3490                 // Ensure dirty flags are set for modified settings.
3491                 foreach ( array_keys( $this->unsanitized_post_values() ) as $setting_id ) {
3492                         $setting = $this->get_setting( $setting_id );
3493                         if ( $setting ) {
3494                                 $setting->dirty = true;
3495                         }
3496                 }
3497
3498                 // Prepare Customizer settings to pass to JavaScript.
3499                 $settings = array(
3500                         'changeset' => array(
3501                                 'uuid' => $this->changeset_uuid(),
3502                                 'status' => $this->changeset_post_id() ? get_post_status( $this->changeset_post_id() ) : '',
3503                         ),
3504                         'timeouts' => array(
3505                                 'windowRefresh' => 250,
3506                                 'changesetAutoSave' => AUTOSAVE_INTERVAL * 1000,
3507                                 'keepAliveCheck' => 2500,
3508                                 'reflowPaneContents' => 100,
3509                                 'previewFrameSensitivity' => 2000,
3510                         ),
3511                         'theme'    => array(
3512                                 'stylesheet' => $this->get_stylesheet(),
3513                                 'active'     => $this->is_theme_active(),
3514                         ),
3515                         'url'      => array(
3516                                 'preview'       => esc_url_raw( $this->get_preview_url() ),
3517                                 'parent'        => esc_url_raw( admin_url() ),
3518                                 'activated'     => esc_url_raw( home_url( '/' ) ),
3519                                 'ajax'          => esc_url_raw( admin_url( 'admin-ajax.php', 'relative' ) ),
3520                                 'allowed'       => array_map( 'esc_url_raw', $this->get_allowed_urls() ),
3521                                 'isCrossDomain' => $this->is_cross_domain(),
3522                                 'home'          => esc_url_raw( home_url( '/' ) ),
3523                                 'login'         => esc_url_raw( $login_url ),
3524                         ),
3525                         'browser'  => array(
3526                                 'mobile' => wp_is_mobile(),
3527                                 'ios'    => $this->is_ios(),
3528                         ),
3529                         'panels'   => array(),
3530                         'sections' => array(),
3531                         'nonce'    => $this->get_nonces(),
3532                         'autofocus' => $this->get_autofocus(),
3533                         'documentTitleTmpl' => $this->get_document_title_template(),
3534                         'previewableDevices' => $this->get_previewable_devices(),
3535                 );
3536
3537                 // Prepare Customize Section objects to pass to JavaScript.
3538                 foreach ( $this->sections() as $id => $section ) {
3539                         if ( $section->check_capabilities() ) {
3540                                 $settings['sections'][ $id ] = $section->json();
3541                         }
3542                 }
3543
3544                 // Prepare Customize Panel objects to pass to JavaScript.
3545                 foreach ( $this->panels() as $panel_id => $panel ) {
3546                         if ( $panel->check_capabilities() ) {
3547                                 $settings['panels'][ $panel_id ] = $panel->json();
3548                                 foreach ( $panel->sections as $section_id => $section ) {
3549                                         if ( $section->check_capabilities() ) {
3550                                                 $settings['sections'][ $section_id ] = $section->json();
3551                                         }
3552                                 }
3553                         }
3554                 }
3555
3556                 ?>
3557                 <script type="text/javascript">
3558                         var _wpCustomizeSettings = <?php echo wp_json_encode( $settings ); ?>;
3559                         _wpCustomizeSettings.controls = {};
3560                         _wpCustomizeSettings.settings = {};
3561                         <?php
3562
3563                         // Serialize settings one by one to improve memory usage.
3564                         echo "(function ( s ){\n";
3565                         foreach ( $this->settings() as $setting ) {
3566                                 if ( $setting->check_capabilities() ) {
3567                                         printf(
3568                                                 "s[%s] = %s;\n",
3569                                                 wp_json_encode( $setting->id ),
3570                                                 wp_json_encode( $setting->json() )
3571                                         );
3572                                 }
3573                         }
3574                         echo "})( _wpCustomizeSettings.settings );\n";
3575
3576                         // Serialize controls one by one to improve memory usage.
3577                         echo "(function ( c ){\n";
3578                         foreach ( $this->controls() as $control ) {
3579                                 if ( $control->check_capabilities() ) {
3580                                         printf(
3581                                                 "c[%s] = %s;\n",
3582                                                 wp_json_encode( $control->id ),
3583                                                 wp_json_encode( $control->json() )
3584                                         );
3585                                 }
3586                         }
3587                         echo "})( _wpCustomizeSettings.controls );\n";
3588                 ?>
3589                 </script>
3590                 <?php
3591         }
3592
3593         /**
3594          * Returns a list of devices to allow previewing.
3595          *
3596          * @access public
3597          * @since 4.5.0
3598          *
3599          * @return array List of devices with labels and default setting.
3600          */
3601         public function get_previewable_devices() {
3602                 $devices = array(
3603                         'desktop' => array(
3604                                 'label' => __( 'Enter desktop preview mode' ),
3605                                 'default' => true,
3606                         ),
3607                         'tablet' => array(
3608                                 'label' => __( 'Enter tablet preview mode' ),
3609                         ),
3610                         'mobile' => array(
3611                                 'label' => __( 'Enter mobile preview mode' ),
3612                         ),
3613                 );
3614
3615                 /**
3616                  * Filters the available devices to allow previewing in the Customizer.
3617                  *
3618                  * @since 4.5.0
3619                  *
3620                  * @see WP_Customize_Manager::get_previewable_devices()
3621                  *
3622                  * @param array $devices List of devices with labels and default setting.
3623                  */
3624                 $devices = apply_filters( 'customize_previewable_devices', $devices );
3625
3626                 return $devices;
3627         }
3628
3629         /**
3630          * Register some default controls.
3631          *
3632          * @since 3.4.0
3633          */
3634         public function register_controls() {
3635
3636                 /* Panel, Section, and Control Types */
3637                 $this->register_panel_type( 'WP_Customize_Panel' );
3638                 $this->register_section_type( 'WP_Customize_Section' );
3639                 $this->register_section_type( 'WP_Customize_Sidebar_Section' );
3640                 $this->register_control_type( 'WP_Customize_Color_Control' );
3641                 $this->register_control_type( 'WP_Customize_Media_Control' );
3642                 $this->register_control_type( 'WP_Customize_Upload_Control' );
3643                 $this->register_control_type( 'WP_Customize_Image_Control' );
3644                 $this->register_control_type( 'WP_Customize_Background_Image_Control' );
3645                 $this->register_control_type( 'WP_Customize_Background_Position_Control' );
3646                 $this->register_control_type( 'WP_Customize_Cropped_Image_Control' );
3647                 $this->register_control_type( 'WP_Customize_Site_Icon_Control' );
3648                 $this->register_control_type( 'WP_Customize_Theme_Control' );
3649
3650                 /* Themes */
3651
3652                 $this->add_section( new WP_Customize_Themes_Section( $this, 'themes', array(
3653                         'title'      => $this->theme()->display( 'Name' ),
3654                         'capability' => 'switch_themes',
3655                         'priority'   => 0,
3656                 ) ) );
3657
3658                 // Themes Setting (unused - the theme is considerably more fundamental to the Customizer experience).
3659                 $this->add_setting( new WP_Customize_Filter_Setting( $this, 'active_theme', array(
3660                         'capability' => 'switch_themes',
3661                 ) ) );
3662
3663                 require_once( ABSPATH . 'wp-admin/includes/theme.php' );
3664
3665                 // Theme Controls.
3666
3667                 // Add a control for the active/original theme.
3668                 if ( ! $this->is_theme_active() ) {
3669                         $themes = wp_prepare_themes_for_js( array( wp_get_theme( $this->original_stylesheet ) ) );
3670                         $active_theme = current( $themes );
3671                         $active_theme['isActiveTheme'] = true;
3672                         $this->add_control( new WP_Customize_Theme_Control( $this, $active_theme['id'], array(
3673                                 'theme'    => $active_theme,
3674                                 'section'  => 'themes',
3675                                 'settings' => 'active_theme',
3676                         ) ) );
3677                 }
3678
3679                 $themes = wp_prepare_themes_for_js();
3680                 foreach ( $themes as $theme ) {
3681                         if ( $theme['active'] || $theme['id'] === $this->original_stylesheet ) {
3682                                 continue;
3683                         }
3684
3685                         $theme_id = 'theme_' . $theme['id'];
3686                         $theme['isActiveTheme'] = false;
3687                         $this->add_control( new WP_Customize_Theme_Control( $this, $theme_id, array(
3688                                 'theme'    => $theme,
3689                                 'section'  => 'themes',
3690                                 'settings' => 'active_theme',
3691                         ) ) );
3692                 }
3693
3694                 /* Site Identity */
3695
3696                 $this->add_section( 'title_tagline', array(
3697                         'title'    => __( 'Site Identity' ),
3698                         'priority' => 20,
3699                 ) );
3700
3701                 $this->add_setting( 'blogname', array(
3702                         'default'    => get_option( 'blogname' ),
3703                         'type'       => 'option',
3704                         'capability' => 'manage_options',
3705                 ) );
3706
3707                 $this->add_control( 'blogname', array(
3708                         'label'      => __( 'Site Title' ),
3709                         'section'    => 'title_tagline',
3710                 ) );
3711
3712                 $this->add_setting( 'blogdescription', array(
3713                         'default'    => get_option( 'blogdescription' ),
3714                         'type'       => 'option',
3715                         'capability' => 'manage_options',
3716                 ) );
3717
3718                 $this->add_control( 'blogdescription', array(
3719                         'label'      => __( 'Tagline' ),
3720                         'section'    => 'title_tagline',
3721                 ) );
3722
3723                 // Add a setting to hide header text if the theme doesn't support custom headers.
3724                 if ( ! current_theme_supports( 'custom-header', 'header-text' ) ) {
3725                         $this->add_setting( 'header_text', array(
3726                                 'theme_supports'    => array( 'custom-logo', 'header-text' ),
3727                                 'default'           => 1,
3728                                 'sanitize_callback' => 'absint',
3729                         ) );
3730
3731                         $this->add_control( 'header_text', array(
3732                                 'label'    => __( 'Display Site Title and Tagline' ),
3733                                 'section'  => 'title_tagline',
3734                                 'settings' => 'header_text',
3735                                 'type'     => 'checkbox',
3736                         ) );
3737                 }
3738
3739                 $this->add_setting( 'site_icon', array(
3740                         'type'       => 'option',
3741                         'capability' => 'manage_options',
3742                         'transport'  => 'postMessage', // Previewed with JS in the Customizer controls window.
3743                 ) );
3744
3745                 $this->add_control( new WP_Customize_Site_Icon_Control( $this, 'site_icon', array(
3746                         'label'       => __( 'Site Icon' ),
3747                         'description' => sprintf(
3748                                 /* translators: %s: site icon size in pixels */
3749                                 __( '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.' ),
3750                                 '<strong>512</strong>'
3751                         ),
3752                         'section'     => 'title_tagline',
3753                         'priority'    => 60,
3754                         'height'      => 512,
3755                         'width'       => 512,
3756                 ) ) );
3757
3758                 $this->add_setting( 'custom_logo', array(
3759                         'theme_supports' => array( 'custom-logo' ),
3760                         'transport'      => 'postMessage',
3761                 ) );
3762
3763                 $custom_logo_args = get_theme_support( 'custom-logo' );
3764                 $this->add_control( new WP_Customize_Cropped_Image_Control( $this, 'custom_logo', array(
3765                         'label'         => __( 'Logo' ),
3766                         'section'       => 'title_tagline',
3767                         'priority'      => 8,
3768                         'height'        => $custom_logo_args[0]['height'],
3769                         'width'         => $custom_logo_args[0]['width'],
3770                         'flex_height'   => $custom_logo_args[0]['flex-height'],
3771                         'flex_width'    => $custom_logo_args[0]['flex-width'],
3772                         'button_labels' => array(
3773                                 'select'       => __( 'Select logo' ),
3774                                 'change'       => __( 'Change logo' ),
3775                                 'remove'       => __( 'Remove' ),
3776                                 'default'      => __( 'Default' ),
3777                                 'placeholder'  => __( 'No logo selected' ),
3778                                 'frame_title'  => __( 'Select logo' ),
3779                                 'frame_button' => __( 'Choose logo' ),
3780                         ),
3781                 ) ) );
3782
3783                 $this->selective_refresh->add_partial( 'custom_logo', array(
3784                         'settings'            => array( 'custom_logo' ),
3785                         'selector'            => '.custom-logo-link',
3786                         'render_callback'     => array( $this, '_render_custom_logo_partial' ),
3787                         'container_inclusive' => true,
3788                 ) );
3789
3790                 /* Colors */
3791
3792                 $this->add_section( 'colors', array(
3793                         'title'          => __( 'Colors' ),
3794                         'priority'       => 40,
3795                 ) );
3796
3797                 $this->add_setting( 'header_textcolor', array(
3798                         'theme_supports' => array( 'custom-header', 'header-text' ),
3799                         'default'        => get_theme_support( 'custom-header', 'default-text-color' ),
3800
3801                         'sanitize_callback'    => array( $this, '_sanitize_header_textcolor' ),
3802                         'sanitize_js_callback' => 'maybe_hash_hex_color',
3803                 ) );
3804
3805                 // Input type: checkbox
3806                 // With custom value
3807                 $this->add_control( 'display_header_text', array(
3808                         'settings' => 'header_textcolor',
3809                         'label'    => __( 'Display Site Title and Tagline' ),
3810                         'section'  => 'title_tagline',
3811                         'type'     => 'checkbox',
3812                         'priority' => 40,
3813                 ) );
3814
3815                 $this->add_control( new WP_Customize_Color_Control( $this, 'header_textcolor', array(
3816                         'label'   => __( 'Header Text Color' ),
3817                         'section' => 'colors',
3818                 ) ) );
3819
3820                 // Input type: Color
3821                 // With sanitize_callback
3822                 $this->add_setting( 'background_color', array(
3823                         'default'        => get_theme_support( 'custom-background', 'default-color' ),
3824                         'theme_supports' => 'custom-background',
3825
3826                         'sanitize_callback'    => 'sanitize_hex_color_no_hash',
3827                         'sanitize_js_callback' => 'maybe_hash_hex_color',
3828                 ) );
3829
3830                 $this->add_control( new WP_Customize_Color_Control( $this, 'background_color', array(
3831                         'label'   => __( 'Background Color' ),
3832                         'section' => 'colors',
3833                 ) ) );
3834
3835                 /* Custom Header */
3836
3837                 if ( current_theme_supports( 'custom-header', 'video' ) ) {
3838                         $title = __( 'Header Media' );
3839                         $description = '<p>' . __( 'If you add a video, the image will be used as a fallback while the video loads.' ) . '</p>';
3840
3841                         // @todo Customizer sections should support having notifications just like controls do. See <https://core.trac.wordpress.org/ticket/38794>.
3842                         $description .= '<div class="customize-control-notifications-container header-video-not-currently-previewable" style="display: none"><ul>';
3843                         $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>';
3844                         $description .= '</ul></div>';
3845                         $width = absint( get_theme_support( 'custom-header', 'width' ) );
3846                         $height = absint( get_theme_support( 'custom-header', 'height' ) );
3847                         if ( $width && $height ) {
3848                                 $control_description = sprintf(
3849                                         /* translators: 1: .mp4, 2: header size in pixels */
3850                                         __( 'Upload your video in %1$s format and minimize its file size for best results. Your theme recommends dimensions of %2$s pixels.' ),
3851                                         '<code>.mp4</code>',
3852                                         sprintf( '<strong>%s &times; %s</strong>', $width, $height )
3853                                 );
3854                         } elseif ( $width ) {
3855                                 $control_description = sprintf(
3856                                         /* translators: 1: .mp4, 2: header width in pixels */
3857                                         __( 'Upload your video in %1$s format and minimize its file size for best results. Your theme recommends a width of %2$s pixels.' ),
3858                                         '<code>.mp4</code>',
3859                                         sprintf( '<strong>%s</strong>', $width )
3860                                 );
3861                         } else {
3862                                 $control_description = sprintf(
3863                                         /* translators: 1: .mp4, 2: header height in pixels */
3864                                         __( 'Upload your video in %1$s format and minimize its file size for best results. Your theme recommends a height of %2$s pixels.' ),
3865                                         '<code>.mp4</code>',
3866                                         sprintf( '<strong>%s</strong>', $height )
3867                                 );
3868                         }
3869                 } else {
3870                         $title = __( 'Header Image' );
3871                         $description = '';
3872                         $control_description = '';
3873                 }
3874
3875                 $this->add_section( 'header_image', array(
3876                         'title'          => $title,
3877                         'description'    => $description,
3878                         'theme_supports' => 'custom-header',
3879                         'priority'       => 60,
3880                 ) );
3881
3882                 $this->add_setting( 'header_video', array(
3883                         'theme_supports'    => array( 'custom-header', 'video' ),
3884                         'transport'         => 'postMessage',
3885                         'sanitize_callback' => 'absint',
3886                         'validate_callback' => array( $this, '_validate_header_video' ),
3887                 ) );
3888
3889                 $this->add_setting( 'external_header_video', array(
3890                         'theme_supports'    => array( 'custom-header', 'video' ),
3891                         'transport'         => 'postMessage',
3892                         'sanitize_callback' => 'esc_url_raw',
3893                         'validate_callback' => array( $this, '_validate_external_header_video' ),
3894                 ) );
3895
3896                 $this->add_setting( new WP_Customize_Filter_Setting( $this, 'header_image', array(
3897                         'default'        => sprintf( get_theme_support( 'custom-header', 'default-image' ), get_template_directory_uri(), get_stylesheet_directory_uri() ),
3898                         'theme_supports' => 'custom-header',
3899                 ) ) );
3900
3901                 $this->add_setting( new WP_Customize_Header_Image_Setting( $this, 'header_image_data', array(
3902                         'theme_supports' => 'custom-header',
3903                 ) ) );
3904
3905                 /*
3906                  * Switch image settings to postMessage when video support is enabled since
3907                  * it entails that the_custom_header_markup() will be used, and thus selective
3908                  * refresh can be utilized.
3909                  */
3910                 if ( current_theme_supports( 'custom-header', 'video' ) ) {
3911                         $this->get_setting( 'header_image' )->transport = 'postMessage';
3912                         $this->get_setting( 'header_image_data' )->transport = 'postMessage';
3913                 }
3914
3915                 $this->add_control( new WP_Customize_Media_Control( $this, 'header_video', array(
3916                         'theme_supports' => array( 'custom-header', 'video' ),
3917                         'label'          => __( 'Header Video' ),
3918                         'description'    => $control_description,
3919                         'section'        => 'header_image',
3920                         'mime_type'      => 'video',
3921                         // @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>.
3922                         'button_labels'  => array(
3923                                 'select'       => __( 'Select Video' ),
3924                                 'change'       => __( 'Change Video' ),
3925                                 'placeholder'  => __( 'No video selected' ),
3926                                 'frame_title'  => __( 'Select Video' ),
3927                                 'frame_button' => __( 'Choose Video' ),
3928                         ),
3929                         'active_callback' => 'is_header_video_active',
3930                 ) ) );
3931
3932                 $this->add_control( 'external_header_video', array(
3933                         'theme_supports' => array( 'custom-header', 'video' ),
3934                         'type'           => 'url',
3935                         'description'    => __( 'Or, enter a YouTube URL:' ),
3936                         'section'        => 'header_image',
3937                         'active_callback'=> 'is_front_page',
3938                 ) );
3939
3940                 $this->add_control( new WP_Customize_Header_Image_Control( $this ) );
3941
3942                 $this->selective_refresh->add_partial( 'custom_header', array(
3943                         'selector'            => '#wp-custom-header',
3944                         'render_callback'     => 'the_custom_header_markup',
3945                         'settings'            => array( 'header_video', 'external_header_video', 'header_image' ), // The image is used as a video fallback here.
3946                         'container_inclusive' => true,
3947                 ) );
3948
3949                 /* Custom Background */
3950
3951                 $this->add_section( 'background_image', array(
3952                         'title'          => __( 'Background Image' ),
3953                         'theme_supports' => 'custom-background',
3954                         'priority'       => 80,
3955                 ) );
3956
3957                 $this->add_setting( 'background_image', array(
3958                         'default'        => get_theme_support( 'custom-background', 'default-image' ),
3959                         'theme_supports' => 'custom-background',
3960                         'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
3961                 ) );
3962
3963                 $this->add_setting( new WP_Customize_Background_Image_Setting( $this, 'background_image_thumb', array(
3964                         'theme_supports' => 'custom-background',
3965                         'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
3966                 ) ) );
3967
3968                 $this->add_control( new WP_Customize_Background_Image_Control( $this ) );
3969
3970                 $this->add_setting( 'background_preset', array(
3971                         'default'        => get_theme_support( 'custom-background', 'default-preset' ),
3972                         'theme_supports' => 'custom-background',
3973                         'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
3974                 ) );
3975
3976                 $this->add_control( 'background_preset', array(
3977                         'label'      => _x( 'Preset', 'Background Preset' ),
3978                         'section'    => 'background_image',
3979                         'type'       => 'select',
3980                         'choices'    => array(
3981                                 'default' => _x( 'Default', 'Default Preset' ),
3982                                 'fill'    => __( 'Fill Screen' ),
3983                                 'fit'     => __( 'Fit to Screen' ),
3984                                 'repeat'  => _x( 'Repeat', 'Repeat Image' ),
3985                                 'custom'  => _x( 'Custom', 'Custom Preset' ),
3986                         ),
3987                 ) );
3988
3989                 $this->add_setting( 'background_position_x', array(
3990                         'default'        => get_theme_support( 'custom-background', 'default-position-x' ),
3991                         'theme_supports' => 'custom-background',
3992                         'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
3993                 ) );
3994
3995                 $this->add_setting( 'background_position_y', array(
3996                         'default'        => get_theme_support( 'custom-background', 'default-position-y' ),
3997                         'theme_supports' => 'custom-background',
3998                         'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
3999                 ) );
4000
4001                 $this->add_control( new WP_Customize_Background_Position_Control( $this, 'background_position', array(
4002                         'label'    => __( 'Image Position' ),
4003                         'section'  => 'background_image',
4004                         'settings' => array(
4005                                 'x' => 'background_position_x',
4006                                 'y' => 'background_position_y',
4007                         ),
4008                 ) ) );
4009
4010                 $this->add_setting( 'background_size', array(
4011                         'default'        => get_theme_support( 'custom-background', 'default-size' ),
4012                         'theme_supports' => 'custom-background',
4013                         'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
4014                 ) );
4015
4016                 $this->add_control( 'background_size', array(
4017                         'label'      => __( 'Image Size' ),
4018                         'section'    => 'background_image',
4019                         'type'       => 'select',
4020                         'choices'    => array(
4021                                 'auto'    => __( 'Original' ),
4022                                 'contain' => __( 'Fit to Screen' ),
4023                                 'cover'   => __( 'Fill Screen' ),
4024                         ),
4025                 ) );
4026
4027                 $this->add_setting( 'background_repeat', array(
4028                         'default'           => get_theme_support( 'custom-background', 'default-repeat' ),
4029                         'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
4030                         'theme_supports'    => 'custom-background',
4031                 ) );
4032
4033                 $this->add_control( 'background_repeat', array(
4034                         'label'    => __( 'Repeat Background Image' ),
4035                         'section'  => 'background_image',
4036                         'type'     => 'checkbox',
4037                 ) );
4038
4039                 $this->add_setting( 'background_attachment', array(
4040                         'default'           => get_theme_support( 'custom-background', 'default-attachment' ),
4041                         'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
4042                         'theme_supports'    => 'custom-background',
4043                 ) );
4044
4045                 $this->add_control( 'background_attachment', array(
4046                         'label'    => __( 'Scroll with Page' ),
4047                         'section'  => 'background_image',
4048                         'type'     => 'checkbox',
4049                 ) );
4050
4051
4052                 // If the theme is using the default background callback, we can update
4053                 // the background CSS using postMessage.
4054                 if ( get_theme_support( 'custom-background', 'wp-head-callback' ) === '_custom_background_cb' ) {
4055                         foreach ( array( 'color', 'image', 'preset', 'position_x', 'position_y', 'size', 'repeat', 'attachment' ) as $prop ) {
4056                                 $this->get_setting( 'background_' . $prop )->transport = 'postMessage';
4057                         }
4058                 }
4059
4060                 /*
4061                  * Static Front Page
4062                  * See also https://core.trac.wordpress.org/ticket/19627 which introduces the the static-front-page theme_support.
4063                  * The following replicates behavior from options-reading.php.
4064                  */
4065
4066                 $this->add_section( 'static_front_page', array(
4067                         'title' => __( 'Static Front Page' ),
4068                         'priority' => 120,
4069                         'description' => __( 'Your theme supports a static front page.' ),
4070                         'active_callback' => array( $this, 'has_published_pages' ),
4071                 ) );
4072
4073                 $this->add_setting( 'show_on_front', array(
4074                         'default' => get_option( 'show_on_front' ),
4075                         'capability' => 'manage_options',
4076                         'type' => 'option',
4077                 ) );
4078
4079                 $this->add_control( 'show_on_front', array(
4080                         'label' => __( 'Front page displays' ),
4081                         'section' => 'static_front_page',
4082                         'type' => 'radio',
4083                         'choices' => array(
4084                                 'posts' => __( 'Your latest posts' ),
4085                                 'page'  => __( 'A static page' ),
4086                         ),
4087                 ) );
4088
4089                 $this->add_setting( 'page_on_front', array(
4090                         'type'       => 'option',
4091                         'capability' => 'manage_options',
4092                 ) );
4093
4094                 $this->add_control( 'page_on_front', array(
4095                         'label' => __( 'Front page' ),
4096                         'section' => 'static_front_page',
4097                         'type' => 'dropdown-pages',
4098                         'allow_addition' => true,
4099                 ) );
4100
4101                 $this->add_setting( 'page_for_posts', array(
4102                         'type' => 'option',
4103                         'capability' => 'manage_options',
4104                 ) );
4105
4106                 $this->add_control( 'page_for_posts', array(
4107                         'label' => __( 'Posts page' ),
4108                         'section' => 'static_front_page',
4109                         'type' => 'dropdown-pages',
4110                         'allow_addition' => true,
4111                 ) );
4112
4113                 /* Custom CSS */
4114                 $this->add_section( 'custom_css', array(
4115                         'title'              => __( 'Additional CSS' ),
4116                         'priority'           => 200,
4117                         'description_hidden' => true,
4118                         'description'        => sprintf( '%s<br /><a href="%s" class="external-link" target="_blank">%s<span class="screen-reader-text">%s</span></a>',
4119                                 __( '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.' ),
4120                                 esc_url( __( 'https://codex.wordpress.org/CSS' ) ),
4121                                 __( 'Learn more about CSS' ),
4122                                 __( '(link opens in a new window)' )
4123                         ),
4124                 ) );
4125
4126                 $custom_css_setting = new WP_Customize_Custom_CSS_Setting( $this, sprintf( 'custom_css[%s]', get_stylesheet() ), array(
4127                         'capability' => 'edit_css',
4128                         'default' => sprintf( "/*\n%s\n*/", __( "You can add your own CSS here.\n\nClick the help icon above to learn more." ) ),
4129                 ) );
4130                 $this->add_setting( $custom_css_setting );
4131
4132                 $this->add_control( 'custom_css', array(
4133                         'type'     => 'textarea',
4134                         'section'  => 'custom_css',
4135                         'settings' => array( 'default' => $custom_css_setting->id ),
4136                         'input_attrs' => array(
4137                                 'class' => 'code', // Ensures contents displayed as LTR instead of RTL.
4138                         ),
4139                 ) );
4140         }
4141
4142         /**
4143          * Return whether there are published pages.
4144          *
4145          * Used as active callback for static front page section and controls.
4146          *
4147          * @access private
4148          * @since 4.7.0
4149          *
4150          * @returns bool Whether there are published (or to be published) pages.
4151          */
4152         public function has_published_pages() {
4153
4154                 $setting = $this->get_setting( 'nav_menus_created_posts' );
4155                 if ( $setting ) {
4156                         foreach ( $setting->value() as $post_id ) {
4157                                 if ( 'page' === get_post_type( $post_id ) ) {
4158                                         return true;
4159                                 }
4160                         }
4161                 }
4162                 return 0 !== count( get_pages() );
4163         }
4164
4165         /**
4166          * Add settings from the POST data that were not added with code, e.g. dynamically-created settings for Widgets
4167          *
4168          * @since 4.2.0
4169          * @access public
4170          *
4171          * @see add_dynamic_settings()
4172          */
4173         public function register_dynamic_settings() {
4174                 $setting_ids = array_keys( $this->unsanitized_post_values() );
4175                 $this->add_dynamic_settings( $setting_ids );
4176         }
4177
4178         /**
4179          * Callback for validating the header_textcolor value.
4180          *
4181          * Accepts 'blank', and otherwise uses sanitize_hex_color_no_hash().
4182          * Returns default text color if hex color is empty.
4183          *
4184          * @since 3.4.0
4185          *
4186          * @param string $color
4187          * @return mixed
4188          */
4189         public function _sanitize_header_textcolor( $color ) {
4190                 if ( 'blank' === $color )
4191                         return 'blank';
4192
4193                 $color = sanitize_hex_color_no_hash( $color );
4194                 if ( empty( $color ) )
4195                         $color = get_theme_support( 'custom-header', 'default-text-color' );
4196
4197                 return $color;
4198         }
4199
4200         /**
4201          * Callback for validating a background setting value.
4202          *
4203          * @since 4.7.0
4204          * @access private
4205          *
4206          * @param string $value Repeat value.
4207          * @param WP_Customize_Setting $setting Setting.
4208          * @return string|WP_Error Background value or validation error.
4209          */
4210         public function _sanitize_background_setting( $value, $setting ) {
4211                 if ( 'background_repeat' === $setting->id ) {
4212                         if ( ! in_array( $value, array( 'repeat-x', 'repeat-y', 'repeat', 'no-repeat' ) ) ) {
4213                                 return new WP_Error( 'invalid_value', __( 'Invalid value for background repeat.' ) );
4214                         }
4215                 } elseif ( 'background_attachment' === $setting->id ) {
4216                         if ( ! in_array( $value, array( 'fixed', 'scroll' ) ) ) {
4217                                 return new WP_Error( 'invalid_value', __( 'Invalid value for background attachment.' ) );
4218                         }
4219                 } elseif ( 'background_position_x' === $setting->id ) {
4220                         if ( ! in_array( $value, array( 'left', 'center', 'right' ), true ) ) {
4221                                 return new WP_Error( 'invalid_value', __( 'Invalid value for background position X.' ) );
4222                         }
4223                 } elseif ( 'background_position_y' === $setting->id ) {
4224                         if ( ! in_array( $value, array( 'top', 'center', 'bottom' ), true ) ) {
4225                                 return new WP_Error( 'invalid_value', __( 'Invalid value for background position Y.' ) );
4226                         }
4227                 } elseif ( 'background_size' === $setting->id ) {
4228                         if ( ! in_array( $value, array( 'auto', 'contain', 'cover' ), true ) ) {
4229                                 return new WP_Error( 'invalid_value', __( 'Invalid value for background size.' ) );
4230                         }
4231                 } elseif ( 'background_preset' === $setting->id ) {
4232                         if ( ! in_array( $value, array( 'default', 'fill', 'fit', 'repeat', 'custom' ), true ) ) {
4233                                 return new WP_Error( 'invalid_value', __( 'Invalid value for background size.' ) );
4234                         }
4235                 } elseif ( 'background_image' === $setting->id || 'background_image_thumb' === $setting->id ) {
4236                         $value = empty( $value ) ? '' : esc_url_raw( $value );
4237                 } else {
4238                         return new WP_Error( 'unrecognized_setting', __( 'Unrecognized background setting.' ) );
4239                 }
4240                 return $value;
4241         }
4242
4243         /**
4244          * Export header video settings to facilitate selective refresh.
4245          *
4246          * @since 4.7.0
4247          *
4248          * @param array $response Response.
4249          * @param WP_Customize_Selective_Refresh $selective_refresh Selective refresh component.
4250          * @param array $partials Array of partials.
4251          * @return array
4252          */
4253         public function export_header_video_settings( $response, $selective_refresh, $partials ) {
4254                 if ( isset( $partials['custom_header'] ) ) {
4255                         $response['custom_header_settings'] = get_header_video_settings();
4256                 }
4257
4258                 return $response;
4259         }
4260
4261         /**
4262          * Callback for validating the header_video value.
4263          *
4264          * Ensures that the selected video is less than 8MB and provides an error message.
4265          *
4266          * @since 4.7.0
4267          *
4268          * @param WP_Error $validity
4269          * @param mixed $value
4270          * @return mixed
4271          */
4272         public function _validate_header_video( $validity, $value ) {
4273                 $video = get_attached_file( absint( $value ) );
4274                 if ( $video ) {
4275                         $size = filesize( $video );
4276                         if ( 8 < $size / pow( 1024, 2 ) ) { // Check whether the size is larger than 8MB.
4277                                 $validity->add( 'size_too_large',
4278                                         __( '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.' )
4279                                 );
4280                         }
4281                         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.
4282                                 $validity->add( 'invalid_file_type', sprintf(
4283                                         /* translators: 1: .mp4, 2: .mov */
4284                                         __( '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.' ),
4285                                         '<code>.mp4</code>',
4286                                         '<code>.mov</code>'
4287                                 ) );
4288                         }
4289                 }
4290                 return $validity;
4291         }
4292
4293         /**
4294          * Callback for validating the external_header_video value.
4295          *
4296          * Ensures that the provided URL is supported.
4297          *
4298          * @since 4.7.0
4299          *
4300          * @param WP_Error $validity
4301          * @param mixed $value
4302          * @return mixed
4303          */
4304         public function _validate_external_header_video( $validity, $value ) {
4305                 $video = esc_url_raw( $value );
4306                 if ( $video ) {
4307                         if ( ! preg_match( '#^https?://(?:www\.)?(?:youtube\.com/watch|youtu\.be/)#', $video ) ) {
4308                                 $validity->add( 'invalid_url', __( 'Please enter a valid YouTube URL.' ) );
4309                         }
4310                 }
4311                 return $validity;
4312         }
4313
4314         /**
4315          * Callback for rendering the custom logo, used in the custom_logo partial.
4316          *
4317          * This method exists because the partial object and context data are passed
4318          * into a partial's render_callback so we cannot use get_custom_logo() as
4319          * the render_callback directly since it expects a blog ID as the first
4320          * argument. When WP no longer supports PHP 5.3, this method can be removed
4321          * in favor of an anonymous function.
4322          *
4323          * @see WP_Customize_Manager::register_controls()
4324          *
4325          * @since 4.5.0
4326          * @access private
4327          *
4328          * @return string Custom logo.
4329          */
4330         public function _render_custom_logo_partial() {
4331                 return get_custom_logo();
4332         }
4333 }