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