3 * WordPress Customize Manager classes
6 * @subpackage Customize
11 * Customize Manager class.
13 * Bootstraps the Customize experience on the server-side.
15 * Sets up the theme-switching process if a theme other than the active one is
16 * being previewed and customized.
18 * Serves as a factory for Customize Controls and Settings, and
19 * instantiates default Customize Controls and Settings.
23 final class WP_Customize_Manager {
25 * An instance of the theme being previewed.
34 * The directory name of the previously active theme (within the theme_root).
40 protected $original_stylesheet;
43 * Whether this is a Customizer pageload.
49 protected $previewing = false;
52 * Methods and properties dealing with managing widgets in the Customizer.
56 * @var WP_Customize_Widgets
61 * Methods and properties dealing with managing nav menus in the Customizer.
65 * @var WP_Customize_Nav_Menus
70 * Methods and properties dealing with selective refresh in the Customizer preview.
74 * @var WP_Customize_Selective_Refresh
76 public $selective_refresh;
79 * Registered instances of WP_Customize_Setting.
85 protected $settings = array();
88 * Sorted top-level instances of WP_Customize_Panel and WP_Customize_Section.
94 protected $containers = array();
97 * Registered instances of WP_Customize_Panel.
103 protected $panels = array();
106 * List of core components.
112 protected $components = array( 'widgets', 'nav_menus' );
115 * Registered instances of WP_Customize_Section.
121 protected $sections = array();
124 * Registered instances of WP_Customize_Control.
130 protected $controls = array();
133 * Panel types that may be rendered from JS templates.
139 protected $registered_panel_types = array();
142 * Section types that may be rendered from JS templates.
148 protected $registered_section_types = array();
151 * Control types that may be rendered from JS templates.
157 protected $registered_control_types = array();
160 * Initial URL being previewed.
166 protected $preview_url;
169 * URL to link the user to when closing the Customizer.
175 protected $return_url;
178 * Mapping of 'panel', 'section', 'control' to the ID which should be autofocused.
184 protected $autofocus = array();
193 protected $messenger_channel;
196 * Unsanitized values for Customize Settings parsed from $_POST['customized'].
200 private $_post_values;
209 private $_changeset_uuid;
218 private $_changeset_post_id;
221 * Changeset data loaded from a customize_changeset post.
227 private $_changeset_data;
233 * @since 4.7.0 Added $args param.
235 * @param array $args {
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.
243 public function __construct( $args = array() ) {
246 array_fill_keys( array( 'changeset_uuid', 'theme', 'messenger_channel' ), null ),
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();
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'] );
263 if ( ! isset( $args['messenger_channel'] ) && isset( $_REQUEST['customize_messenger_channel'] ) ) {
264 $args['messenger_channel'] = sanitize_key( wp_unslash( $_REQUEST['customize_messenger_channel'] ) );
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'];
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' );
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' );
296 require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menus-panel.php' );
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' );
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' );
311 * Filters the core Customizer components to load.
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
320 * @see WP_Customize_Manager::__construct()
322 * @param array $components List of core components to load.
323 * @param WP_Customize_Manager $this WP_Customize_Manager instance.
325 $components = apply_filters( 'customize_loaded_components', $this->components, $this );
327 require_once( ABSPATH . WPINC . '/customize/class-wp-customize-selective-refresh.php' );
328 $this->selective_refresh = new WP_Customize_Selective_Refresh( $this );
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 );
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 );
340 add_action( 'setup_theme', array( $this, 'setup_theme' ) );
341 add_action( 'wp_loaded', array( $this, 'wp_loaded' ) );
343 // Do not spawn cron (especially the alternate cron) while running the Customizer.
344 remove_action( 'init', 'wp_cron' );
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' );
351 add_action( 'wp_ajax_customize_save', array( $this, 'save' ) );
352 add_action( 'wp_ajax_customize_refresh_nonces', array( $this, 'refresh_nonces' ) );
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' ) );
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 );
364 // Export header video settings with the partial response.
365 add_filter( 'customize_render_partials_response', array( $this, 'export_header_video_settings' ), 10, 3 );
367 // Export the settings to JS via the _wpCustomizeSettings variable.
368 add_action( 'customize_controls_print_footer_scripts', array( $this, 'customize_pane_settings' ), 1000 );
372 * Return true if it's an Ajax request.
375 * @since 4.2.0 Added `$action` param.
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.
381 public function doing_ajax( $action = null ) {
382 if ( ! wp_doing_ajax() ) {
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.
393 return isset( $_REQUEST['action'] ) && wp_unslash( $_REQUEST['action'] ) === $action;
398 * Custom wp_die wrapper. Returns either the standard message for UI
399 * or the Ajax message.
403 * @param mixed $ajax_message Ajax return
404 * @param mixed $message UI message
406 protected function wp_die( $ajax_message, $message = null ) {
407 if ( $this->doing_ajax() ) {
408 wp_die( $ajax_message );
412 $message = __( 'Cheatin’ uh?' );
415 if ( $this->messenger_channel ) {
417 wp_enqueue_scripts();
418 wp_print_scripts( array( 'customize-base' ) );
421 'messengerArgs' => array(
422 'channel' => $this->messenger_channel,
423 'url' => wp_customize_url(),
425 'error' => $ajax_message,
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 ) ?> );
435 $message .= ob_get_clean();
442 * Return the Ajax wp_die() handler if it's a customized request.
447 * @return callable Die handler.
449 public function wp_die_handler() {
450 _deprecated_function( __METHOD__, '4.7.0' );
452 if ( $this->doing_ajax() || isset( $_POST['customized'] ) ) {
453 return '_ajax_wp_die_handler';
456 return '_default_wp_die_handler';
460 * Start preview and customize theme.
462 * Check if customize query variable exist. Init filters to filter the current theme.
466 public function setup_theme() {
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() ) {
475 '<h1>' . __( 'Cheatin’ uh?' ) . '</h1>' .
476 '<p>' . __( 'Sorry, you are not allowed to customize this site.' ) . '</p>',
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' ) );
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.
492 if ( ! current_user_can( 'customize' ) && ! $this->changeset_post_id() ) {
493 $this->wp_die( $this->messenger_channel ? 0 : -1, __( 'Non-existent changeset UUID.' ) );
496 if ( ! headers_sent() ) {
497 send_origin_headers();
500 // Hide the admin bar if we're embedded in the customizer iframe.
501 if ( $this->messenger_channel ) {
502 show_admin_bar( false );
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' ) );
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.' ) );
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() );
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.' ) );
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.
531 if ( get_option( 'fresh_site' ) && 'customize.php' === $pagenow ) {
532 add_action( 'after_setup_theme', array( $this, 'import_theme_starter_content' ), 100 );
535 $this->start_previewing_theme();
539 * Callback to validate a theme once it is loaded
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' );
552 * If the theme to be previewed isn't the active theme, add filter callbacks
553 * to swap it out at runtime.
557 public function start_previewing_theme() {
558 // Bail if we're already previewing.
559 if ( $this->is_preview() ) {
563 $this->previewing = true;
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' ) );
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' ) );
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' ) );
580 * Fires once the Customizer theme preview has started.
584 * @param WP_Customize_Manager $this WP_Customize_Manager instance.
586 do_action( 'start_previewing_theme', $this );
590 * Stop previewing the selected theme.
592 * Removes filters to change the current theme.
596 public function stop_previewing_theme() {
597 if ( ! $this->is_preview() ) {
601 $this->previewing = false;
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' ) );
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' ) );
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' ) );
618 * Fires once the Customizer theme preview has stopped.
622 * @param WP_Customize_Manager $this WP_Customize_Manager instance.
624 do_action( 'stop_previewing_theme', $this );
628 * Get the changeset UUID.
633 * @return string UUID.
635 public function changeset_uuid() {
636 return $this->_changeset_uuid;
640 * Get the theme being customized.
646 public function theme() {
647 if ( ! $this->theme ) {
648 $this->theme = wp_get_theme();
654 * Get the registered settings.
660 public function settings() {
661 return $this->settings;
665 * Get the registered controls.
671 public function controls() {
672 return $this->controls;
676 * Get the registered containers.
682 public function containers() {
683 return $this->containers;
687 * Get the registered sections.
693 public function sections() {
694 return $this->sections;
698 * Get the registered panels.
703 * @return array Panels.
705 public function panels() {
706 return $this->panels;
710 * Checks if the current theme is active.
716 public function is_theme_active() {
717 return $this->get_stylesheet() == $this->original_stylesheet;
721 * Register styles/scripts and initialize the preview of each setting
725 public function wp_loaded() {
728 * Fires once WordPress has loaded, allowing scripts and styles to be initialized.
732 * @param WP_Customize_Manager $this WP_Customize_Manager instance.
734 do_action( 'customize_register', $this );
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
744 if ( ! $this->doing_ajax( 'customize_save' ) ) {
745 foreach ( $this->settings as $setting ) {
750 if ( $this->is_preview() && ! is_admin() ) {
751 $this->customize_preview_init();
756 * Prevents Ajax requests from following redirects when previewing a theme
757 * by issuing a 200 response instead of a 30x.
759 * Instead, the JS will sniff out the location header.
764 * @param int $status Status.
767 public function wp_redirect_status( $status ) {
768 _deprecated_function( __FUNCTION__, '4.7.0' );
770 if ( $this->is_preview() && ! is_admin() ) {
778 * Find the changeset post ID for a given changeset UUID.
783 * @param string $uuid Changeset UUID.
784 * @return int|null Returns post ID on success and null on failure.
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;
793 $changeset_post_query = new WP_Query( array(
794 'post_type' => 'customize_changeset',
795 'post_status' => get_post_stati(),
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,
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;
814 * Get the changeset post id for the loaded changeset.
819 * @return int|null Post ID on success or null if there is no post yet saved.
821 public function changeset_post_id() {
822 if ( ! isset( $this->_changeset_post_id ) ) {
823 $post_id = $this->find_changeset_post_id( $this->_changeset_uuid );
827 $this->_changeset_post_id = $post_id;
829 if ( false === $this->_changeset_post_id ) {
832 return $this->_changeset_post_id;
836 * Get the data stored in a changeset post.
841 * @param int $post_id Changeset post ID.
842 * @return array|WP_Error Changeset data or WP_Error on error.
844 protected function get_changeset_post_data( $post_id ) {
846 return new WP_Error( 'empty_post_id' );
848 $changeset_post = get_post( $post_id );
849 if ( ! $changeset_post ) {
850 return new WP_Error( 'missing_post' );
852 if ( 'customize_changeset' !== $changeset_post->post_type ) {
853 return new WP_Error( 'wrong_post_type' );
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() );
859 if ( ! is_array( $changeset_data ) ) {
860 return new WP_Error( 'expected_array' );
862 return $changeset_data;
866 * Get changeset data.
871 * @return array Changeset data.
873 public function changeset_data() {
874 if ( isset( $this->_changeset_data ) ) {
875 return $this->_changeset_data;
877 $changeset_post_id = $this->changeset_post_id();
878 if ( ! $changeset_post_id ) {
879 $this->_changeset_data = array();
881 $data = $this->get_changeset_post_data( $changeset_post_id );
882 if ( ! is_wp_error( $data ) ) {
883 $this->_changeset_data = $data;
885 $this->_changeset_data = array();
888 return $this->_changeset_data;
892 * Starter content setting IDs.
898 protected $pending_starter_content_settings_ids = array();
901 * Import theme starter content into the customized state.
906 * @param array $starter_content Starter content. Defaults to `get_theme_starter_content()`.
908 function import_theme_starter_content( $starter_content = array() ) {
909 if ( empty( $starter_content ) ) {
910 $starter_content = get_theme_starter_content();
913 $changeset_data = array();
914 if ( $this->changeset_post_id() ) {
915 $changeset_data = $this->get_changeset_post_data( $this->changeset_post_id() );
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();
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;
932 if ( ! isset( $max_widget_numbers[ $id_base ] ) ) {
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();
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 );
946 $max_widget_numbers[ $id_base ] = 1;
949 $max_widget_numbers[ $id_base ] += 1;
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 ] );
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;
959 $sidebar_widget_ids[] = $widget_id;
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;
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'] );
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;
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 ] );
986 if ( empty( $posts[ $post_symbol ]['post_name'] ) ) {
987 $posts[ $post_symbol ]['post_name'] = sanitize_title( $posts[ $post_symbol ]['post_title'] );
989 if ( empty( $posts[ $post_symbol ]['post_type'] ) ) {
990 $posts[ $post_symbol ]['post_type'] = 'post';
992 $needed_posts[ $posts[ $post_symbol ]['post_type'] . ':' . $posts[ $post_symbol ]['post_name'] ] = true;
994 $all_post_slugs = array_merge(
995 wp_list_pluck( $attachments, 'post_name' ),
996 wp_list_pluck( $posts, 'post_name' )
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,
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 );
1013 $existing_starter_content_posts[ $existing_post->post_type . ':' . $post_name ] = $existing_post;
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,
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;
1033 // Attachments are technically posts but handled differently.
1034 if ( ! empty( $attachments ) ) {
1036 $attachment_ids = array();
1038 foreach ( $attachments as $symbol => $attachment ) {
1039 $file_array = array(
1040 'name' => $attachment['file_name'],
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 ) ) {
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() );
1061 // Insert the attachment auto-draft because it doesn't yet exist or the attached file is gone.
1062 if ( ! $attachment_id ) {
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;
1069 if ( empty( $file_array['tmp_name'] ) ) {
1073 $attachment_post_data = array_merge(
1074 wp_array_slice_assoc( $attachment, array( 'post_title', 'post_content', 'post_excerpt' ) ),
1076 'post_status' => 'auto-draft', // So attachment will be garbage collected in a week if changeset is never published.
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', '<' ) ) {
1087 $attachment_id = media_handle_sideload( $file_array, 0, null, $attachment_post_data );
1088 if ( is_wp_error( $attachment_id ) ) {
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'] );
1095 $attachment_ids[ $symbol ] = $attachment_id;
1097 $starter_content_auto_draft_post_ids = array_merge( $starter_content_auto_draft_post_ids, array_values( $attachment_ids ) );
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'] ) ) {
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'] );
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;
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'] ];
1128 if ( ! empty( $posts[ $post_symbol ]['template'] ) ) {
1129 $posts[ $post_symbol ]['meta_input']['_wp_page_template'] = $posts[ $post_symbol ]['template'];
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;
1138 $starter_content_auto_draft_post_ids = array_merge( $starter_content_auto_draft_post_ids, wp_list_pluck( $posts, 'ID' ) );
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;
1149 $placeholder_id = -1;
1150 $reused_nav_menu_setting_ids = array();
1151 foreach ( $nav_menus as $nav_menu_location => $nav_menu ) {
1153 $nav_menu_term_id = null;
1154 $nav_menu_setting_id = null;
1157 // Look for an existing placeholder menu with starter content to re-use.
1158 foreach ( $changeset_data as $setting_id => $setting_params ) {
1160 ! empty( $setting_params['starter_content'] )
1162 ! in_array( $setting_id, $reused_nav_menu_setting_ids, true )
1164 preg_match( '#^nav_menu\[(?P<nav_menu_id>-?\d+)\]$#', $setting_id, $matches )
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;
1174 if ( ! $nav_menu_term_id ) {
1175 while ( isset( $changeset_data[ sprintf( 'nav_menu[%d]', $placeholder_id ) ] ) ) {
1178 $nav_menu_term_id = $placeholder_id;
1179 $nav_menu_setting_id = sprintf( 'nav_menu[%d]', $placeholder_id );
1182 $this->set_post_value( $nav_menu_setting_id, array(
1183 'name' => isset( $nav_menu['name'] ) ? $nav_menu['name'] : $nav_menu_location,
1185 $this->pending_starter_content_settings_ids[] = $nav_menu_setting_id;
1187 // @todo Add support for menu_item_parent.
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++;
1194 $nav_menu_item['nav_menu_term_id'] = $nav_menu_term_id;
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;
1207 $nav_menu_item['object_id'] = 0;
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;
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;
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'] ];
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;
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'] ];
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 ) ) {
1261 'attachment_id' => $value,
1262 'url' => wp_get_attachment_url( $value ),
1263 'height' => $metadata['height'],
1264 'width' => $metadata['width'],
1266 } elseif ( 'background_image' === $name ) {
1267 $value = wp_get_attachment_url( $value );
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;
1276 if ( ! empty( $this->pending_starter_content_settings_ids ) ) {
1277 if ( did_action( 'customize_register' ) ) {
1278 $this->_save_starter_content_changeset();
1280 add_action( 'customize_register', array( $this, '_save_starter_content_changeset' ), 1000 );
1286 * Prepare starter content attachments.
1288 * Ensure that the attachments are valid and that they have slugs and file name/path.
1293 * @param array $attachments Attachments.
1294 * @return array Prepared attachments.
1296 protected function prepare_starter_content_attachments( $attachments ) {
1297 $prepared_attachments = array();
1298 if ( empty( $attachments ) ) {
1299 return $prepared_attachments;
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' );
1307 foreach ( $attachments as $symbol => $attachment ) {
1309 // A file is required and URLs to files are not currently allowed.
1310 if ( empty( $attachment['file'] ) || preg_match( '#^https?://$#', $attachment['file'] ) ) {
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'];
1324 $file_name = basename( $attachment['file'] );
1326 // Skip file types that are not recognized.
1327 $checked_filetype = wp_check_filetype( $file_name );
1328 if ( empty( $checked_filetype['type'] ) ) {
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'] );
1337 $attachment['post_name'] = sanitize_title( preg_replace( '/\.\w+$/', '', $file_name ) );
1341 $attachment['file_name'] = $file_name;
1342 $attachment['file_path'] = $file_path;
1343 $prepared_attachments[ $symbol ] = $attachment;
1345 return $prepared_attachments;
1349 * Save starter content changeset.
1354 public function _save_starter_content_changeset() {
1356 if ( empty( $this->pending_starter_content_settings_ids ) ) {
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,
1365 $this->pending_starter_content_settings_ids = array();
1369 * Get dirty pre-sanitized setting values in the current customized state.
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()`.
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.
1385 * @since 4.7.0 Added $args param and merging with changeset values and stashed theme mods.
1387 * @param array $args {
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.
1395 public function unsanitized_post_values( $args = array() ) {
1396 $args = array_merge(
1398 'exclude_changeset' => false,
1399 'exclude_post_data' => ! current_user_can( 'customize' ),
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' ) );
1415 if ( ! $args['exclude_changeset'] ) {
1416 foreach ( $this->changeset_data() as $setting_id => $setting_params ) {
1417 if ( ! array_key_exists( 'value', $setting_params ) ) {
1420 if ( isset( $setting_params['type'] ) && 'theme_mod' === $setting_params['type'] ) {
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'];
1428 $values[ $setting_id ] = $setting_params['value'];
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 );
1438 $post_values = array();
1440 if ( is_array( $post_values ) ) {
1441 $this->_post_values = $post_values;
1443 $this->_post_values = array();
1446 $values = array_merge( $values, $this->_post_values );
1452 * Returns the sanitized value for a given setting from the current customized state.
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.
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.
1463 * @see WP_REST_Server::dispatch()
1464 * @see WP_Rest_Request::sanitize_params()
1465 * @see WP_Rest_Request::has_valid_params()
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.
1472 public function post_value( $setting, $default = null ) {
1473 $post_values = $this->unsanitized_post_values();
1474 if ( ! array_key_exists( $setting->id, $post_values ) ) {
1477 $value = $post_values[ $setting->id ];
1478 $valid = $setting->validate( $value );
1479 if ( is_wp_error( $valid ) ) {
1482 $value = $setting->sanitize( $value );
1483 if ( is_null( $value ) || is_wp_error( $value ) ) {
1490 * Override a setting's value in the current customized state.
1492 * The name "post_value" is a carry-over from when the customized state was
1493 * exclusively sourced from `$_POST['customized']`.
1498 * @param string $setting_id ID for the WP_Customize_Setting instance.
1499 * @param mixed $value Post value.
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;
1506 * Announce when a specific setting's unsanitized post value has been set.
1508 * Fires when the WP_Customize_Manager::set_post_value() method is called.
1510 * The dynamic portion of the hook name, `$setting_id`, refers to the setting ID.
1514 * @param mixed $value Unsanitized setting post value.
1515 * @param WP_Customize_Manager $this WP_Customize_Manager instance.
1517 do_action( "customize_post_value_set_{$setting_id}", $value, $this );
1520 * Announce when any setting's unsanitized post value has been set.
1522 * Fires when the WP_Customize_Manager::set_post_value() method is called.
1524 * This is useful for `WP_Customize_Setting` instances to watch
1525 * in order to update a cached previewed value.
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.
1533 do_action( 'customize_post_value_set', $setting_id, $value, $this );
1537 * Print JavaScript settings.
1541 public function customize_preview_init() {
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.
1551 if ( ! headers_sent() ) {
1553 header( 'X-Robots: noindex, nofollow, noarchive' );
1555 add_action( 'wp_head', 'wp_no_robots' );
1556 add_filter( 'wp_headers', array( $this, 'filter_iframe_security_headers' ) );
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.
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.' ) );
1568 $this->prepare_controls();
1570 add_filter( 'wp_redirect', array( $this, 'add_state_query_params' ) );
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' );
1579 * Fires once the Customizer preview has initialized and JavaScript
1580 * settings have been printed.
1584 * @param WP_Customize_Manager $this WP_Customize_Manager instance.
1586 do_action( 'customize_preview_init', $this );
1590 * Filter the X-Frame-Options and Content-Security-Policy headers to ensure frontend can load in customizer.
1595 * @param array $headers Headers.
1596 * @return array Headers.
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 );
1606 * Add customize state query params to a given URL if preview is allowed.
1610 * @see wp_redirect()
1611 * @see WP_Customize_Manager::get_allowed_url()
1613 * @param string $url URL.
1614 * @return string URL.
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 );
1622 $parsed_allowed_url['scheme'] === $parsed_original_url['scheme']
1624 $parsed_allowed_url['host'] === $parsed_original_url['host']
1626 0 === strpos( $parsed_original_url['path'], $parsed_allowed_url['path'] )
1628 if ( $is_allowed ) {
1633 if ( $is_allowed ) {
1634 $query_params = array(
1635 'customize_changeset_uuid' => $this->changeset_uuid(),
1637 if ( ! $this->is_theme_active() ) {
1638 $query_params['customize_theme'] = $this->get_stylesheet();
1640 if ( $this->messenger_channel ) {
1641 $query_params['customize_messenger_channel'] = $this->messenger_channel;
1643 $url = add_query_arg( $query_params, $url );
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.
1657 public function customize_preview_override_404_status() {
1658 _deprecated_function( __METHOD__, '4.7.0' );
1662 * Print base element for preview frame.
1667 public function customize_preview_base() {
1668 _deprecated_function( __METHOD__, '4.7.0' );
1672 * Print a workaround to handle HTML5 tags in IE < 9.
1675 * @deprecated 4.7.0 Customizer no longer supports IE8, so all supported browsers recognize HTML5.
1677 public function customize_preview_html5() {
1678 _deprecated_function( __FUNCTION__, '4.7.0' );
1682 * Print CSS for loading indicators for the Customizer preview.
1687 public function customize_preview_loading_style() {
1689 body.wp-customizer-unloading {
1691 cursor: progress !important;
1692 -webkit-transition: opacity 0.5s;
1693 transition: opacity 0.5s;
1695 body.wp-customizer-unloading * {
1696 pointer-events: none !important;
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;
1710 * Remove customize_messenger_channel query parameter from the preview window when it is not in an iframe.
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.
1718 public function remove_frameless_preview_messenger_channel() {
1719 if ( ! $this->messenger_channel ) {
1725 var urlParser, oldQueryParams, newQueryParams, i;
1726 if ( parent !== window ) {
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 ] );
1738 urlParser.search = newQueryParams.join( '&' );
1739 if ( urlParser.search !== location.search ) {
1740 location.replace( urlParser.href );
1748 * Print JavaScript settings for preview frame.
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 );
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(
1761 'customize_changeset_uuid',
1762 'customize_messenger_channel',
1764 $self_url = remove_query_arg( $state_query_params, $self_url );
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'] ) ) {
1773 $host = $parsed['host'];
1774 if ( ! empty( $parsed['port'] ) ) {
1775 $host .= ':' . $parsed['port'];
1777 $allowed_hosts[] = $host;
1780 'changeset' => array(
1781 'uuid' => $this->_changeset_uuid,
1783 'timeouts' => array(
1784 'selectiveRefresh' => 250,
1785 'keepAliveSend' => 1000,
1788 'stylesheet' => $this->get_stylesheet(),
1789 'active' => $this->is_theme_active(),
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(),
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(),
1804 'shiftClickToEdit' => __( 'Shift-click to edit this element.' ),
1805 'linkUnpreviewable' => __( 'This link is not live-previewable.' ),
1806 'formUnpreviewable' => __( 'This form is not live-previewable.' ),
1808 '_dirty' => array_keys( $post_values ),
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();
1821 foreach ( $this->sections as $id => $section ) {
1822 if ( $section->check_capabilities() ) {
1823 $settings['activeSections'][ $id ] = $section->active();
1826 foreach ( $this->controls as $id => $control ) {
1827 if ( $control->check_capabilities() ) {
1828 $settings['activeControls'][ $id ] = $control->active();
1833 <script type="text/javascript">
1834 var _wpCustomizeSettings = <?php echo wp_json_encode( $settings ); ?>;
1835 _wpCustomizeSettings.values = {};
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.
1843 foreach ( $this->settings as $id => $setting ) {
1844 if ( $setting->check_capabilities() ) {
1847 wp_json_encode( $id ),
1848 wp_json_encode( $setting->js_value() )
1853 })( _wpCustomizeSettings.values );
1859 * Prints a signature so we can ensure the Customizer was properly executed.
1864 public function customize_preview_signature() {
1865 _deprecated_function( __METHOD__, '4.7.0' );
1869 * Removes the signature in case we experience a case where the Customizer was not properly executed.
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.
1877 public function remove_preview_signature( $return = null ) {
1878 _deprecated_function( __METHOD__, '4.7.0' );
1884 * Is it a theme preview?
1888 * @return bool True if it's a preview, false if not.
1890 public function is_preview() {
1891 return (bool) $this->previewing;
1895 * Retrieve the template name of the previewed theme.
1899 * @return string Template name.
1901 public function get_template() {
1902 return $this->theme()->get_template();
1906 * Retrieve the stylesheet name of the previewed theme.
1910 * @return string Stylesheet name.
1912 public function get_stylesheet() {
1913 return $this->theme()->get_stylesheet();
1917 * Retrieve the template root of the previewed theme.
1921 * @return string Theme root.
1923 public function get_template_root() {
1924 return get_raw_theme_root( $this->get_template(), true );
1928 * Retrieve the stylesheet root of the previewed theme.
1932 * @return string Theme root.
1934 public function get_stylesheet_root() {
1935 return get_raw_theme_root( $this->get_stylesheet(), true );
1939 * Filters the current theme and return the name of the previewed theme.
1943 * @param $current_theme {@internal Parameter is not used}
1944 * @return string Theme name.
1946 public function current_theme( $current_theme ) {
1947 return $this->theme()->display('Name');
1951 * Validates setting values.
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.
1961 * @see WP_REST_Request::has_valid_params()
1962 * @see WP_Customize_Setting::validate()
1964 * @param array $setting_values Mapping of setting IDs to values to validate and sanitize.
1965 * @param array $options {
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.
1971 * @return array Mapping of setting IDs to return value of validate method calls, either `true` or `WP_Error`.
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,
1979 $validities = array();
1980 foreach ( $setting_values as $setting_id => $unsanitized_value ) {
1981 $setting = $this->get_setting( $setting_id );
1983 if ( $options['validate_existence'] ) {
1984 $validities[ $setting_id ] = new WP_Error( 'unrecognized', __( 'Setting does not exist or is unrecognized.' ) );
1988 if ( $options['validate_capability'] && ! current_user_can( $setting->capability ) ) {
1989 $validity = new WP_Error( 'unauthorized', __( 'Unauthorized to modify setting due to capability.' ) );
1991 if ( is_null( $unsanitized_value ) ) {
1994 $validity = $setting->validate( $unsanitized_value );
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;
2003 if ( ! is_wp_error( $validity ) ) {
2004 $value = $setting->sanitize( $unsanitized_value );
2005 if ( is_null( $value ) ) {
2007 } elseif ( is_wp_error( $value ) ) {
2011 if ( false === $validity ) {
2012 $validity = new WP_Error( 'invalid_value', __( 'Invalid value.' ) );
2014 $validities[ $setting_id ] = $validity;
2020 * Prepares setting validity for exporting to the client (JS).
2022 * Converts `WP_Error` instance into array suitable for passing into the
2023 * `wp.customize.Notification` JS model.
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.
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 ),
2042 return $notification;
2049 * Handle customize_save WP Ajax request to save/update a changeset.
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.
2054 public function save() {
2055 if ( ! is_user_logged_in() ) {
2056 wp_send_json_error( 'unauthenticated' );
2059 if ( ! $this->is_preview() ) {
2060 wp_send_json_error( 'not_preview' );
2063 $action = 'save-customize_' . $this->get_stylesheet();
2064 if ( ! check_ajax_referer( $action, 'nonce', false ) ) {
2065 wp_send_json_error( 'invalid_nonce' );
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' );
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' );
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' );
2085 $input_changeset_data = array();
2089 $changeset_title = null;
2090 if ( isset( $_POST['customize_changeset_title'] ) ) {
2091 $changeset_title = sanitize_text_field( wp_unslash( $_POST['customize_changeset_title'] ) );
2094 // Validate changeset status param.
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 );
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 );
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".
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 );
2125 $changeset_date_gmt = get_gmt_from_date( $changeset_date );
2127 $timestamp = strtotime( $changeset_date );
2128 if ( ! $timestamp ) {
2129 wp_send_json_error( 'bad_customize_changeset_date', 400 );
2131 $changeset_date_gmt = gmdate( 'Y-m-d H:i:s', $timestamp );
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,
2141 if ( is_wp_error( $r ) ) {
2143 'message' => $r->get_error_message(),
2144 'code' => $r->get_error_code(),
2146 if ( is_array( $r->get_error_data() ) ) {
2147 $response = array_merge( $response, $r->get_error_data() );
2149 $response['data'] = $r->get_error_data();
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';
2160 if ( 'publish' === $response['changeset_status'] ) {
2161 $response['next_changeset_uuid'] = wp_generate_uuid4();
2165 if ( isset( $response['setting_validities'] ) ) {
2166 $response['setting_validities'] = array_map( array( $this, 'prepare_setting_validity_for_js' ), $response['setting_validities'] );
2170 * Filters response data for a successful customize_save Ajax request.
2172 * This filter does not apply if there was a nonce or authentication failure.
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.
2180 $response = apply_filters( 'customize_save_response', $response, $this );
2182 if ( is_wp_error( $r ) ) {
2183 wp_send_json_error( $response );
2185 wp_send_json_success( $response );
2190 * Save the post for the loaded changeset.
2195 * @param array $args {
2196 * Args for changeset post.
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.
2206 * @return array|WP_Error Returns array on success and WP_Error with array data on error.
2208 function save_changeset_post( $args = array() ) {
2210 $args = array_merge(
2216 'user_id' => get_current_user_id(),
2217 'starter_content' => false,
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' );
2230 $existing_changeset_data = $this->get_changeset_post_data( $changeset_post_id );
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' );
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.
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.
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' );
2253 } elseif ( $changeset_post_id && 'future' === $args['status'] ) {
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' );
2262 // The request was made via wp.customize.previewer.save().
2263 $update_transactionally = (bool) $args['status'];
2264 $allow_revision = (bool) $args['status'];
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.
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,
2278 $this->add_dynamic_settings( array_keys( $post_values ) ); // Ensure settings get created even if they lack an input value.
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.
2288 $changed_setting_ids = array();
2289 foreach ( $post_values as $setting_id => $setting_value ) {
2290 $setting = $this->get_setting( $setting_id );
2292 if ( $setting && 'theme_mod' === $setting->type ) {
2293 $prefixed_setting_id = $this->get_stylesheet() . '::' . $setting->id;
2295 $prefixed_setting_id = $setting_id;
2298 $is_value_changed = (
2299 ! isset( $existing_changeset_data[ $prefixed_setting_id ] )
2301 ! array_key_exists( 'value', $existing_changeset_data[ $prefixed_setting_id ] )
2303 $existing_changeset_data[ $prefixed_setting_id ]['value'] !== $setting_value
2305 if ( $is_value_changed ) {
2306 $changed_setting_ids[] = $setting_id;
2311 * Fires before save validation happens.
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.
2319 * @param WP_Customize_Manager $this WP_Customize_Manager instance.
2321 do_action( 'customize_save_validation_before', $this );
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.
2328 $setting_validities = $this->validate_setting_values( $validated_values, array(
2329 'validate_capability' => true,
2330 'validate_existence' => true,
2332 $invalid_setting_count = count( array_filter( $setting_validities, 'is_wp_error' ) );
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.
2338 if ( $update_transactionally && $invalid_setting_count > 0 ) {
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 ) ),
2343 return new WP_Error( 'transaction_fail', '', $response );
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 ) ) {
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();
2358 if ( ! isset( $args['data'][ $setting_id ]['value'] ) ) {
2359 $args['data'][ $setting_id ]['value'] = $post_value;
2363 foreach ( $args['data'] as $setting_id => $setting_params ) {
2364 $setting = $this->get_setting( $setting_id );
2365 if ( ! $setting || ! $setting->check_capabilities() ) {
2369 // Skip updating changeset for invalid setting values.
2370 if ( isset( $setting_validities[ $setting_id ] ) && is_wp_error( $setting_validities[ $setting_id ] ) ) {
2374 $changeset_setting_id = $setting_id;
2375 if ( 'theme_mod' === $setting->type ) {
2376 $changeset_setting_id = sprintf( '%s::%s', $this->get_stylesheet(), $setting_id );
2379 if ( null === $setting_params ) {
2380 // Remove setting from changeset entirely.
2381 unset( $data[ $changeset_setting_id ] );
2384 if ( ! isset( $data[ $changeset_setting_id ] ) ) {
2385 $data[ $changeset_setting_id ] = array();
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 );
2391 // Skip updating setting params if unchanged (ensuring the user_id is not overwritten).
2392 if ( $data[ $changeset_setting_id ] === $merged_setting_params ) {
2396 $data[ $changeset_setting_id ] = array_merge(
2397 $merged_setting_params,
2399 'type' => $setting->type,
2400 'user_id' => $args['user_id'],
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'] );
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,
2422 * Filters the settings' data that will be persisted into the changeset.
2424 * Plugins may amend additional data (such as additional meta for settings) into the changeset with this filter.
2428 * @param array $data Updated changeset data, mapping setting IDs to arrays containing a $value item and optionally other metadata.
2429 * @param array $context {
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.
2441 $data = apply_filters( 'customize_changeset_save_data', $data, $filter_context );
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();
2452 // Gather the data for wp_insert_post()/wp_update_post().
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.
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 ),
2461 if ( $args['title'] ) {
2462 $post_array['post_title'] = $args['title'];
2464 if ( $changeset_post_id ) {
2465 $post_array['ID'] = $changeset_post_id;
2467 $post_array['post_type'] = 'customize_changeset';
2468 $post_array['post_name'] = $this->changeset_uuid();
2469 $post_array['post_status'] = 'auto-draft';
2471 if ( $args['status'] ) {
2472 $post_array['post_status'] = $args['status'];
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'] );
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 );
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' ) );
2490 kses_remove_filters(); // Prevent KSES from corrupting JSON in post_content.
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 );
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.
2504 kses_init_filters();
2506 $this->_changeset_data = null; // Reset so WP_Customize_Manager::changeset_data() will re-populate with updated contents.
2508 remove_filter( 'wp_save_post_revision_post_has_changed', array( $this, '_filter_revision_post_has_changed' ) );
2511 'setting_validities' => $setting_validities,
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 );
2523 * Whether a changeset revision should be made.
2529 protected $store_changeset_revision;
2532 * Filters whether a changeset has changed to create a new revision.
2534 * Note that this will not be called while a changeset post remains in auto-draft status.
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.
2543 * @return bool Whether a revision should be made.
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;
2550 return $post_has_changed;
2554 * Publish changeset values.
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.
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.
2568 * @see _wp_customize_publish_changeset()
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.
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;
2579 $changeset_post = get_post( $changeset_post_id );
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.
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;
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>.+)$/';
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'] )
2603 isset( $setting_params['type'] )
2605 'theme_mod' === $setting_params['type']
2607 preg_match( $namespace_pattern, $raw_setting_id, $matches )
2609 if ( $is_theme_mod_setting ) {
2610 if ( ! isset( $theme_mod_settings[ $matches['stylesheet'] ] ) ) {
2611 $theme_mod_settings[ $matches['stylesheet'] ] = array();
2613 $theme_mod_settings[ $matches['stylesheet'] ][ $matches['setting_id'] ] = $setting_params;
2615 if ( $this->get_stylesheet() === $matches['stylesheet'] ) {
2616 $actual_setting_id = $matches['setting_id'];
2619 $actual_setting_id = $raw_setting_id;
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'];
2628 $changeset_setting_values = $this->unsanitized_post_values( array(
2629 'exclude_post_data' => true,
2630 'exclude_changeset' => false,
2632 $changeset_setting_ids = array_keys( $changeset_setting_values );
2633 $this->add_dynamic_settings( $changeset_setting_ids );
2636 * Fires once the theme has switched in the Customizer, but before settings
2641 * @param WP_Customize_Manager $manager WP_Customize_Manager instance.
2643 do_action( 'customize_save', $this );
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.
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';
2660 $original_user_id = get_current_user_id();
2661 foreach ( $changeset_setting_ids as $setting_id ) {
2662 $setting = $this->get_setting( $setting_id );
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.
2671 if ( isset( $setting_user_ids[ $setting_id ] ) ) {
2672 wp_set_current_user( $setting_user_ids[ $setting_id ] );
2674 wp_set_current_user( $original_user_id );
2680 wp_set_current_user( $original_user_id );
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 );
2690 * Fires after Customize settings have been saved.
2694 * @param WP_Customize_Manager $manager WP_Customize_Manager instance.
2696 do_action( 'customize_save_after', $this );
2698 // Restore original capabilities.
2699 foreach ( $original_setting_capabilities as $setting_id => $capability ) {
2700 $setting = $this->get_setting( $setting_id );
2702 $setting->capability = $capability;
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;
2715 * Update stashed theme mod settings.
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.
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();
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() ] );
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();
2738 $stashed_theme_mod_settings[ $stylesheet ] = array_merge(
2739 $stashed_theme_mod_settings[ $stylesheet ],
2745 $result = update_option( 'customize_stashed_theme_mods', $stashed_theme_mod_settings, $autoload );
2749 return $stashed_theme_mod_settings;
2753 * Refresh nonces for the current preview.
2757 public function refresh_nonces() {
2758 if ( ! $this->is_preview() ) {
2759 wp_send_json_error( 'not_preview' );
2762 wp_send_json_success( $this->get_nonces() );
2766 * Add a customize setting.
2769 * @since 4.5.0 Return added WP_Customize_Setting instance.
2772 * @param WP_Customize_Setting|string $id Customize Setting object, or ID.
2773 * @param array $args Setting arguments; passed to WP_Customize_Setting
2775 * @return WP_Customize_Setting The instance of the setting that was added.
2777 public function add_setting( $id, $args = array() ) {
2778 if ( $id instanceof WP_Customize_Setting ) {
2781 $class = 'WP_Customize_Setting';
2783 /** This filter is documented in wp-includes/class-wp-customize-manager.php */
2784 $args = apply_filters( 'customize_dynamic_setting_args', $args, $id );
2786 /** This filter is documented in wp-includes/class-wp-customize-manager.php */
2787 $class = apply_filters( 'customize_dynamic_setting_class', $class, $id, $args );
2789 $setting = new $class( $this, $id, $args );
2792 $this->settings[ $setting->id ] = $setting;
2797 * Register any dynamically-created settings, such as those from $_POST['customized']
2798 * that have no corresponding setting created.
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.
2808 * @param array $setting_ids The setting IDs to add.
2809 * @return array The WP_Customize_Setting objects added.
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 ) ) {
2819 $setting_args = false;
2820 $setting_class = 'WP_Customize_Setting';
2823 * Filters a dynamic setting's constructor args.
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.
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']`.
2834 $setting_args = apply_filters( 'customize_dynamic_setting_args', $setting_args, $setting_id );
2835 if ( false === $setting_args ) {
2840 * Allow non-statically created settings to be constructed with custom WP_Customize_Setting subclass.
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.
2848 $setting_class = apply_filters( 'customize_dynamic_setting_class', $setting_class, $setting_id, $setting_args );
2850 $setting = new $setting_class( $this, $setting_id, $setting_args );
2852 $this->add_setting( $setting );
2853 $new_settings[] = $setting;
2855 return $new_settings;
2859 * Retrieve a customize setting.
2863 * @param string $id Customize Setting ID.
2864 * @return WP_Customize_Setting|void The setting, if set.
2866 public function get_setting( $id ) {
2867 if ( isset( $this->settings[ $id ] ) ) {
2868 return $this->settings[ $id ];
2873 * Remove a customize setting.
2877 * @param string $id Customize Setting ID.
2879 public function remove_setting( $id ) {
2880 unset( $this->settings[ $id ] );
2884 * Add a customize panel.
2887 * @since 4.5.0 Return added WP_Customize_Panel instance.
2890 * @param WP_Customize_Panel|string $id Customize Panel object, or Panel ID.
2891 * @param array $args Optional. Panel arguments. Default empty array.
2893 * @return WP_Customize_Panel The instance of the panel that was added.
2895 public function add_panel( $id, $args = array() ) {
2896 if ( $id instanceof WP_Customize_Panel ) {
2899 $panel = new WP_Customize_Panel( $this, $id, $args );
2902 $this->panels[ $panel->id ] = $panel;
2907 * Retrieve a customize panel.
2912 * @param string $id Panel ID to get.
2913 * @return WP_Customize_Panel|void Requested panel instance, if set.
2915 public function get_panel( $id ) {
2916 if ( isset( $this->panels[ $id ] ) ) {
2917 return $this->panels[ $id ];
2922 * Remove a customize panel.
2927 * @param string $id Panel ID to remove.
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.' ),
2935 '<a href="' . esc_url( 'https://developer.wordpress.org/reference/hooks/customize_loaded_components/' ) . '"><code>customize_loaded_components</code></a>'
2938 _doing_it_wrong( __METHOD__, $message, '4.5.0' );
2940 unset( $this->panels[ $id ] );
2944 * Register a customize panel type.
2946 * Registered types are eligible to be rendered via JS and created dynamically.
2951 * @see WP_Customize_Panel
2953 * @param string $panel Name of a custom panel which is a subclass of WP_Customize_Panel.
2955 public function register_panel_type( $panel ) {
2956 $this->registered_panel_types[] = $panel;
2960 * Render JS templates for all registered panel types.
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();
2973 * Add a customize section.
2976 * @since 4.5.0 Return added WP_Customize_Section instance.
2979 * @param WP_Customize_Section|string $id Customize Section object, or Section ID.
2980 * @param array $args Section arguments.
2982 * @return WP_Customize_Section The instance of the section that was added.
2984 public function add_section( $id, $args = array() ) {
2985 if ( $id instanceof WP_Customize_Section ) {
2988 $section = new WP_Customize_Section( $this, $id, $args );
2991 $this->sections[ $section->id ] = $section;
2996 * Retrieve a customize section.
3000 * @param string $id Section ID.
3001 * @return WP_Customize_Section|void The section, if set.
3003 public function get_section( $id ) {
3004 if ( isset( $this->sections[ $id ] ) )
3005 return $this->sections[ $id ];
3009 * Remove a customize section.
3013 * @param string $id Section ID.
3015 public function remove_section( $id ) {
3016 unset( $this->sections[ $id ] );
3020 * Register a customize section type.
3022 * Registered types are eligible to be rendered via JS and created dynamically.
3027 * @see WP_Customize_Section
3029 * @param string $section Name of a custom section which is a subclass of WP_Customize_Section.
3031 public function register_section_type( $section ) {
3032 $this->registered_section_types[] = $section;
3036 * Render JS templates for all registered section types.
3041 public function render_section_templates() {
3042 foreach ( $this->registered_section_types as $section_type ) {
3043 $section = new $section_type( $this, 'temp', array() );
3044 $section->print_template();
3049 * Add a customize control.
3052 * @since 4.5.0 Return added WP_Customize_Control instance.
3055 * @param WP_Customize_Control|string $id Customize Control object, or ID.
3056 * @param array $args Control arguments; passed to WP_Customize_Control
3058 * @return WP_Customize_Control The instance of the control that was added.
3060 public function add_control( $id, $args = array() ) {
3061 if ( $id instanceof WP_Customize_Control ) {
3064 $control = new WP_Customize_Control( $this, $id, $args );
3067 $this->controls[ $control->id ] = $control;
3072 * Retrieve a customize control.
3076 * @param string $id ID of the control.
3077 * @return WP_Customize_Control|void The control object, if set.
3079 public function get_control( $id ) {
3080 if ( isset( $this->controls[ $id ] ) )
3081 return $this->controls[ $id ];
3085 * Remove a customize control.
3089 * @param string $id ID of the control.
3091 public function remove_control( $id ) {
3092 unset( $this->controls[ $id ] );
3096 * Register a customize control type.
3098 * Registered types are eligible to be rendered via JS and created dynamically.
3103 * @param string $control Name of a custom control which is a subclass of
3104 * WP_Customize_Control.
3106 public function register_control_type( $control ) {
3107 $this->registered_control_types[] = $control;
3111 * Render JS templates for all registered control types.
3116 public function render_control_templates() {
3117 foreach ( $this->registered_control_types as $control_type ) {
3118 $control = new $control_type( $this, 'temp', array(
3119 'settings' => array(),
3121 $control->print_template();
3124 <script type="text/html" id="tmpl-customize-control-notifications">
3126 <# _.each( data.notifications, function( notification ) { #>
3127 <li class="notice notice-{{ notification.type || 'info' }} {{ data.altNotice ? 'notice-alt' : '' }}" data-code="{{ notification.code }}" data-type="{{ notification.type }}">{{{ notification.message || notification.code }}}</li>
3135 * Helper function to compare two objects by priority, ensuring sort stability via instance_number.
3138 * @deprecated 4.7.0 Use wp_list_sort()
3140 * @param WP_Customize_Panel|WP_Customize_Section|WP_Customize_Control $a Object A.
3141 * @param WP_Customize_Panel|WP_Customize_Section|WP_Customize_Control $b Object B.
3144 protected function _cmp_priority( $a, $b ) {
3145 _deprecated_function( __METHOD__, '4.7.0', 'wp_list_sort' );
3147 if ( $a->priority === $b->priority ) {
3148 return $a->instance_number - $b->instance_number;
3150 return $a->priority - $b->priority;
3155 * Prepare panels, sections, and controls.
3157 * For each, check if required related components exist,
3158 * whether the user has the necessary capabilities,
3159 * and sort by priority.
3163 public function prepare_controls() {
3165 $controls = array();
3166 $this->controls = wp_list_sort( $this->controls, array(
3167 'priority' => 'ASC',
3168 'instance_number' => 'ASC',
3171 foreach ( $this->controls as $id => $control ) {
3172 if ( ! isset( $this->sections[ $control->section ] ) || ! $control->check_capabilities() ) {
3176 $this->sections[ $control->section ]->controls[] = $control;
3177 $controls[ $id ] = $control;
3179 $this->controls = $controls;
3181 // Prepare sections.
3182 $this->sections = wp_list_sort( $this->sections, array(
3183 'priority' => 'ASC',
3184 'instance_number' => 'ASC',
3186 $sections = array();
3188 foreach ( $this->sections as $section ) {
3189 if ( ! $section->check_capabilities() ) {
3194 $section->controls = wp_list_sort( $section->controls, array(
3195 'priority' => 'ASC',
3196 'instance_number' => 'ASC',
3199 if ( ! $section->panel ) {
3200 // Top-level section.
3201 $sections[ $section->id ] = $section;
3203 // This section belongs to a panel.
3204 if ( isset( $this->panels [ $section->panel ] ) ) {
3205 $this->panels[ $section->panel ]->sections[ $section->id ] = $section;
3209 $this->sections = $sections;
3212 $this->panels = wp_list_sort( $this->panels, array(
3213 'priority' => 'ASC',
3214 'instance_number' => 'ASC',
3218 foreach ( $this->panels as $panel ) {
3219 if ( ! $panel->check_capabilities() ) {
3223 $panel->sections = wp_list_sort( $panel->sections, array(
3224 'priority' => 'ASC',
3225 'instance_number' => 'ASC',
3227 $panels[ $panel->id ] = $panel;
3229 $this->panels = $panels;
3231 // Sort panels and top-level sections together.
3232 $this->containers = array_merge( $this->panels, $this->sections );
3233 $this->containers = wp_list_sort( $this->containers, array(
3234 'priority' => 'ASC',
3235 'instance_number' => 'ASC',
3240 * Enqueue scripts for customize controls.
3244 public function enqueue_control_scripts() {
3245 foreach ( $this->controls as $control ) {
3246 $control->enqueue();
3251 * Determine whether the user agent is iOS.
3256 * @return bool Whether the user agent is iOS.
3258 public function is_ios() {
3259 return wp_is_mobile() && preg_match( '/iPad|iPod|iPhone/', $_SERVER['HTTP_USER_AGENT'] );
3263 * Get the template string for the Customizer pane document title.
3268 * @return string The template string for the document title.
3270 public function get_document_title_template() {
3271 if ( $this->is_theme_active() ) {
3272 /* translators: %s: document title from the preview */
3273 $document_title_tmpl = __( 'Customize: %s' );
3275 /* translators: %s: document title from the preview */
3276 $document_title_tmpl = __( 'Live Preview: %s' );
3278 $document_title_tmpl = html_entity_decode( $document_title_tmpl, ENT_QUOTES, 'UTF-8' ); // Because exported to JS and assigned to document.title.
3279 return $document_title_tmpl;
3283 * Set the initial URL to be previewed.
3290 * @param string $preview_url URL to be previewed.
3292 public function set_preview_url( $preview_url ) {
3293 $preview_url = esc_url_raw( $preview_url );
3294 $this->preview_url = wp_validate_redirect( $preview_url, home_url( '/' ) );
3298 * Get the initial URL to be previewed.
3303 * @return string URL being previewed.
3305 public function get_preview_url() {
3306 if ( empty( $this->preview_url ) ) {
3307 $preview_url = home_url( '/' );
3309 $preview_url = $this->preview_url;
3311 return $preview_url;
3315 * Determines whether the admin and the frontend are on different domains.
3320 * @return bool Whether cross-domain.
3322 public function is_cross_domain() {
3323 $admin_origin = wp_parse_url( admin_url() );
3324 $home_origin = wp_parse_url( home_url() );
3325 $cross_domain = ( strtolower( $admin_origin['host'] ) !== strtolower( $home_origin['host'] ) );
3326 return $cross_domain;
3330 * Get URLs allowed to be previewed.
3332 * If the front end and the admin are served from the same domain, load the
3333 * preview over ssl if the Customizer is being loaded over ssl. This avoids
3334 * insecure content warnings. This is not attempted if the admin and front end
3335 * are on different domains to avoid the case where the front end doesn't have
3336 * ssl certs. Domain mapping plugins can allow other urls in these conditions
3337 * using the customize_allowed_urls filter.
3342 * @returns array Allowed URLs.
3344 public function get_allowed_urls() {
3345 $allowed_urls = array( home_url( '/' ) );
3347 if ( is_ssl() && ! $this->is_cross_domain() ) {
3348 $allowed_urls[] = home_url( '/', 'https' );
3352 * Filters the list of URLs allowed to be clicked and followed in the Customizer preview.
3356 * @param array $allowed_urls An array of allowed URLs.
3358 $allowed_urls = array_unique( apply_filters( 'customize_allowed_urls', $allowed_urls ) );
3360 return $allowed_urls;
3364 * Get messenger channel.
3369 * @return string Messenger channel.
3371 public function get_messenger_channel() {
3372 return $this->messenger_channel;
3376 * Set URL to link the user to when closing the Customizer.
3383 * @param string $return_url URL for return link.
3385 public function set_return_url( $return_url ) {
3386 $return_url = esc_url_raw( $return_url );
3387 $return_url = remove_query_arg( wp_removable_query_args(), $return_url );
3388 $return_url = wp_validate_redirect( $return_url );
3389 $this->return_url = $return_url;
3393 * Get URL to link the user to when closing the Customizer.
3398 * @return string URL for link to close Customizer.
3400 public function get_return_url() {
3401 $referer = wp_get_referer();
3402 $excluded_referer_basenames = array( 'customize.php', 'wp-login.php' );
3404 if ( $this->return_url ) {
3405 $return_url = $this->return_url;
3406 } else if ( $referer && ! in_array( basename( parse_url( $referer, PHP_URL_PATH ) ), $excluded_referer_basenames, true ) ) {
3407 $return_url = $referer;
3408 } else if ( $this->preview_url ) {
3409 $return_url = $this->preview_url;
3411 $return_url = home_url( '/' );
3417 * Set the autofocused constructs.
3422 * @param array $autofocus {
3423 * Mapping of 'panel', 'section', 'control' to the ID which should be autofocused.
3425 * @type string [$control] ID for control to be autofocused.
3426 * @type string [$section] ID for section to be autofocused.
3427 * @type string [$panel] ID for panel to be autofocused.
3430 public function set_autofocus( $autofocus ) {
3431 $this->autofocus = array_filter( wp_array_slice_assoc( $autofocus, array( 'panel', 'section', 'control' ) ), 'is_string' );
3435 * Get the autofocused constructs.
3441 * Mapping of 'panel', 'section', 'control' to the ID which should be autofocused.
3443 * @type string [$control] ID for control to be autofocused.
3444 * @type string [$section] ID for section to be autofocused.
3445 * @type string [$panel] ID for panel to be autofocused.
3448 public function get_autofocus() {
3449 return $this->autofocus;
3453 * Get nonces for the Customizer.
3456 * @return array Nonces.
3458 public function get_nonces() {
3460 'save' => wp_create_nonce( 'save-customize_' . $this->get_stylesheet() ),
3461 'preview' => wp_create_nonce( 'preview-customize_' . $this->get_stylesheet() ),
3465 * Filters nonces for Customizer.
3469 * @param array $nonces Array of refreshed nonces for save and
3471 * @param WP_Customize_Manager $this WP_Customize_Manager instance.
3473 $nonces = apply_filters( 'customize_refresh_nonces', $nonces, $this );
3479 * Print JavaScript settings for parent window.
3483 public function customize_pane_settings() {
3485 $login_url = add_query_arg( array(
3486 'interim-login' => 1,
3487 'customize-login' => 1,
3488 ), wp_login_url() );
3490 // Ensure dirty flags are set for modified settings.
3491 foreach ( array_keys( $this->unsanitized_post_values() ) as $setting_id ) {
3492 $setting = $this->get_setting( $setting_id );
3494 $setting->dirty = true;
3498 // Prepare Customizer settings to pass to JavaScript.
3500 'changeset' => array(
3501 'uuid' => $this->changeset_uuid(),
3502 'status' => $this->changeset_post_id() ? get_post_status( $this->changeset_post_id() ) : '',
3504 'timeouts' => array(
3505 'windowRefresh' => 250,
3506 'changesetAutoSave' => AUTOSAVE_INTERVAL * 1000,
3507 'keepAliveCheck' => 2500,
3508 'reflowPaneContents' => 100,
3509 'previewFrameSensitivity' => 2000,
3512 'stylesheet' => $this->get_stylesheet(),
3513 'active' => $this->is_theme_active(),
3516 'preview' => esc_url_raw( $this->get_preview_url() ),
3517 'parent' => esc_url_raw( admin_url() ),
3518 'activated' => esc_url_raw( home_url( '/' ) ),
3519 'ajax' => esc_url_raw( admin_url( 'admin-ajax.php', 'relative' ) ),
3520 'allowed' => array_map( 'esc_url_raw', $this->get_allowed_urls() ),
3521 'isCrossDomain' => $this->is_cross_domain(),
3522 'home' => esc_url_raw( home_url( '/' ) ),
3523 'login' => esc_url_raw( $login_url ),
3526 'mobile' => wp_is_mobile(),
3527 'ios' => $this->is_ios(),
3529 'panels' => array(),
3530 'sections' => array(),
3531 'nonce' => $this->get_nonces(),
3532 'autofocus' => $this->get_autofocus(),
3533 'documentTitleTmpl' => $this->get_document_title_template(),
3534 'previewableDevices' => $this->get_previewable_devices(),
3537 // Prepare Customize Section objects to pass to JavaScript.
3538 foreach ( $this->sections() as $id => $section ) {
3539 if ( $section->check_capabilities() ) {
3540 $settings['sections'][ $id ] = $section->json();
3544 // Prepare Customize Panel objects to pass to JavaScript.
3545 foreach ( $this->panels() as $panel_id => $panel ) {
3546 if ( $panel->check_capabilities() ) {
3547 $settings['panels'][ $panel_id ] = $panel->json();
3548 foreach ( $panel->sections as $section_id => $section ) {
3549 if ( $section->check_capabilities() ) {
3550 $settings['sections'][ $section_id ] = $section->json();
3557 <script type="text/javascript">
3558 var _wpCustomizeSettings = <?php echo wp_json_encode( $settings ); ?>;
3559 _wpCustomizeSettings.controls = {};
3560 _wpCustomizeSettings.settings = {};
3563 // Serialize settings one by one to improve memory usage.
3564 echo "(function ( s ){\n";
3565 foreach ( $this->settings() as $setting ) {
3566 if ( $setting->check_capabilities() ) {
3569 wp_json_encode( $setting->id ),
3570 wp_json_encode( $setting->json() )
3574 echo "})( _wpCustomizeSettings.settings );\n";
3576 // Serialize controls one by one to improve memory usage.
3577 echo "(function ( c ){\n";
3578 foreach ( $this->controls() as $control ) {
3579 if ( $control->check_capabilities() ) {
3582 wp_json_encode( $control->id ),
3583 wp_json_encode( $control->json() )
3587 echo "})( _wpCustomizeSettings.controls );\n";
3594 * Returns a list of devices to allow previewing.
3599 * @return array List of devices with labels and default setting.
3601 public function get_previewable_devices() {
3604 'label' => __( 'Enter desktop preview mode' ),
3608 'label' => __( 'Enter tablet preview mode' ),
3611 'label' => __( 'Enter mobile preview mode' ),
3616 * Filters the available devices to allow previewing in the Customizer.
3620 * @see WP_Customize_Manager::get_previewable_devices()
3622 * @param array $devices List of devices with labels and default setting.
3624 $devices = apply_filters( 'customize_previewable_devices', $devices );
3630 * Register some default controls.
3634 public function register_controls() {
3636 /* Panel, Section, and Control Types */
3637 $this->register_panel_type( 'WP_Customize_Panel' );
3638 $this->register_section_type( 'WP_Customize_Section' );
3639 $this->register_section_type( 'WP_Customize_Sidebar_Section' );
3640 $this->register_control_type( 'WP_Customize_Color_Control' );
3641 $this->register_control_type( 'WP_Customize_Media_Control' );
3642 $this->register_control_type( 'WP_Customize_Upload_Control' );
3643 $this->register_control_type( 'WP_Customize_Image_Control' );
3644 $this->register_control_type( 'WP_Customize_Background_Image_Control' );
3645 $this->register_control_type( 'WP_Customize_Background_Position_Control' );
3646 $this->register_control_type( 'WP_Customize_Cropped_Image_Control' );
3647 $this->register_control_type( 'WP_Customize_Site_Icon_Control' );
3648 $this->register_control_type( 'WP_Customize_Theme_Control' );
3652 $this->add_section( new WP_Customize_Themes_Section( $this, 'themes', array(
3653 'title' => $this->theme()->display( 'Name' ),
3654 'capability' => 'switch_themes',
3658 // Themes Setting (unused - the theme is considerably more fundamental to the Customizer experience).
3659 $this->add_setting( new WP_Customize_Filter_Setting( $this, 'active_theme', array(
3660 'capability' => 'switch_themes',
3663 require_once( ABSPATH . 'wp-admin/includes/theme.php' );
3667 // Add a control for the active/original theme.
3668 if ( ! $this->is_theme_active() ) {
3669 $themes = wp_prepare_themes_for_js( array( wp_get_theme( $this->original_stylesheet ) ) );
3670 $active_theme = current( $themes );
3671 $active_theme['isActiveTheme'] = true;
3672 $this->add_control( new WP_Customize_Theme_Control( $this, $active_theme['id'], array(
3673 'theme' => $active_theme,
3674 'section' => 'themes',
3675 'settings' => 'active_theme',
3679 $themes = wp_prepare_themes_for_js();
3680 foreach ( $themes as $theme ) {
3681 if ( $theme['active'] || $theme['id'] === $this->original_stylesheet ) {
3685 $theme_id = 'theme_' . $theme['id'];
3686 $theme['isActiveTheme'] = false;
3687 $this->add_control( new WP_Customize_Theme_Control( $this, $theme_id, array(
3689 'section' => 'themes',
3690 'settings' => 'active_theme',
3696 $this->add_section( 'title_tagline', array(
3697 'title' => __( 'Site Identity' ),
3701 $this->add_setting( 'blogname', array(
3702 'default' => get_option( 'blogname' ),
3704 'capability' => 'manage_options',
3707 $this->add_control( 'blogname', array(
3708 'label' => __( 'Site Title' ),
3709 'section' => 'title_tagline',
3712 $this->add_setting( 'blogdescription', array(
3713 'default' => get_option( 'blogdescription' ),
3715 'capability' => 'manage_options',
3718 $this->add_control( 'blogdescription', array(
3719 'label' => __( 'Tagline' ),
3720 'section' => 'title_tagline',
3723 // Add a setting to hide header text if the theme doesn't support custom headers.
3724 if ( ! current_theme_supports( 'custom-header', 'header-text' ) ) {
3725 $this->add_setting( 'header_text', array(
3726 'theme_supports' => array( 'custom-logo', 'header-text' ),
3728 'sanitize_callback' => 'absint',
3731 $this->add_control( 'header_text', array(
3732 'label' => __( 'Display Site Title and Tagline' ),
3733 'section' => 'title_tagline',
3734 'settings' => 'header_text',
3735 'type' => 'checkbox',
3739 $this->add_setting( 'site_icon', array(
3741 'capability' => 'manage_options',
3742 'transport' => 'postMessage', // Previewed with JS in the Customizer controls window.
3745 $this->add_control( new WP_Customize_Site_Icon_Control( $this, 'site_icon', array(
3746 'label' => __( 'Site Icon' ),
3747 'description' => sprintf(
3748 /* translators: %s: site icon size in pixels */
3749 __( 'The Site Icon is used as a browser and app icon for your site. Icons must be square, and at least %s pixels wide and tall.' ),
3750 '<strong>512</strong>'
3752 'section' => 'title_tagline',
3758 $this->add_setting( 'custom_logo', array(
3759 'theme_supports' => array( 'custom-logo' ),
3760 'transport' => 'postMessage',
3763 $custom_logo_args = get_theme_support( 'custom-logo' );
3764 $this->add_control( new WP_Customize_Cropped_Image_Control( $this, 'custom_logo', array(
3765 'label' => __( 'Logo' ),
3766 'section' => 'title_tagline',
3768 'height' => $custom_logo_args[0]['height'],
3769 'width' => $custom_logo_args[0]['width'],
3770 'flex_height' => $custom_logo_args[0]['flex-height'],
3771 'flex_width' => $custom_logo_args[0]['flex-width'],
3772 'button_labels' => array(
3773 'select' => __( 'Select logo' ),
3774 'change' => __( 'Change logo' ),
3775 'remove' => __( 'Remove' ),
3776 'default' => __( 'Default' ),
3777 'placeholder' => __( 'No logo selected' ),
3778 'frame_title' => __( 'Select logo' ),
3779 'frame_button' => __( 'Choose logo' ),
3783 $this->selective_refresh->add_partial( 'custom_logo', array(
3784 'settings' => array( 'custom_logo' ),
3785 'selector' => '.custom-logo-link',
3786 'render_callback' => array( $this, '_render_custom_logo_partial' ),
3787 'container_inclusive' => true,
3792 $this->add_section( 'colors', array(
3793 'title' => __( 'Colors' ),
3797 $this->add_setting( 'header_textcolor', array(
3798 'theme_supports' => array( 'custom-header', 'header-text' ),
3799 'default' => get_theme_support( 'custom-header', 'default-text-color' ),
3801 'sanitize_callback' => array( $this, '_sanitize_header_textcolor' ),
3802 'sanitize_js_callback' => 'maybe_hash_hex_color',
3805 // Input type: checkbox
3806 // With custom value
3807 $this->add_control( 'display_header_text', array(
3808 'settings' => 'header_textcolor',
3809 'label' => __( 'Display Site Title and Tagline' ),
3810 'section' => 'title_tagline',
3811 'type' => 'checkbox',
3815 $this->add_control( new WP_Customize_Color_Control( $this, 'header_textcolor', array(
3816 'label' => __( 'Header Text Color' ),
3817 'section' => 'colors',
3820 // Input type: Color
3821 // With sanitize_callback
3822 $this->add_setting( 'background_color', array(
3823 'default' => get_theme_support( 'custom-background', 'default-color' ),
3824 'theme_supports' => 'custom-background',
3826 'sanitize_callback' => 'sanitize_hex_color_no_hash',
3827 'sanitize_js_callback' => 'maybe_hash_hex_color',
3830 $this->add_control( new WP_Customize_Color_Control( $this, 'background_color', array(
3831 'label' => __( 'Background Color' ),
3832 'section' => 'colors',
3837 if ( current_theme_supports( 'custom-header', 'video' ) ) {
3838 $title = __( 'Header Media' );
3839 $description = '<p>' . __( 'If you add a video, the image will be used as a fallback while the video loads.' ) . '</p>';
3841 // @todo Customizer sections should support having notifications just like controls do. See <https://core.trac.wordpress.org/ticket/38794>.
3842 $description .= '<div class="customize-control-notifications-container header-video-not-currently-previewable" style="display: none"><ul>';
3843 $description .= '<li class="notice notice-info">' . __( 'This theme doesn\'t support video headers on this page. Navigate to the front page or another page that supports video headers.' ) . '</li>';
3844 $description .= '</ul></div>';
3845 $width = absint( get_theme_support( 'custom-header', 'width' ) );
3846 $height = absint( get_theme_support( 'custom-header', 'height' ) );
3847 if ( $width && $height ) {
3848 $control_description = sprintf(
3849 /* translators: 1: .mp4, 2: header size in pixels */
3850 __( 'Upload your video in %1$s format and minimize its file size for best results. Your theme recommends dimensions of %2$s pixels.' ),
3851 '<code>.mp4</code>',
3852 sprintf( '<strong>%s × %s</strong>', $width, $height )
3854 } elseif ( $width ) {
3855 $control_description = sprintf(
3856 /* translators: 1: .mp4, 2: header width in pixels */
3857 __( 'Upload your video in %1$s format and minimize its file size for best results. Your theme recommends a width of %2$s pixels.' ),
3858 '<code>.mp4</code>',
3859 sprintf( '<strong>%s</strong>', $width )
3862 $control_description = sprintf(
3863 /* translators: 1: .mp4, 2: header height in pixels */
3864 __( 'Upload your video in %1$s format and minimize its file size for best results. Your theme recommends a height of %2$s pixels.' ),
3865 '<code>.mp4</code>',
3866 sprintf( '<strong>%s</strong>', $height )
3870 $title = __( 'Header Image' );
3872 $control_description = '';
3875 $this->add_section( 'header_image', array(
3877 'description' => $description,
3878 'theme_supports' => 'custom-header',
3882 $this->add_setting( 'header_video', array(
3883 'theme_supports' => array( 'custom-header', 'video' ),
3884 'transport' => 'postMessage',
3885 'sanitize_callback' => 'absint',
3886 'validate_callback' => array( $this, '_validate_header_video' ),
3889 $this->add_setting( 'external_header_video', array(
3890 'theme_supports' => array( 'custom-header', 'video' ),
3891 'transport' => 'postMessage',
3892 'sanitize_callback' => 'esc_url_raw',
3893 'validate_callback' => array( $this, '_validate_external_header_video' ),
3896 $this->add_setting( new WP_Customize_Filter_Setting( $this, 'header_image', array(
3897 'default' => sprintf( get_theme_support( 'custom-header', 'default-image' ), get_template_directory_uri(), get_stylesheet_directory_uri() ),
3898 'theme_supports' => 'custom-header',
3901 $this->add_setting( new WP_Customize_Header_Image_Setting( $this, 'header_image_data', array(
3902 'theme_supports' => 'custom-header',
3906 * Switch image settings to postMessage when video support is enabled since
3907 * it entails that the_custom_header_markup() will be used, and thus selective
3908 * refresh can be utilized.
3910 if ( current_theme_supports( 'custom-header', 'video' ) ) {
3911 $this->get_setting( 'header_image' )->transport = 'postMessage';
3912 $this->get_setting( 'header_image_data' )->transport = 'postMessage';
3915 $this->add_control( new WP_Customize_Media_Control( $this, 'header_video', array(
3916 'theme_supports' => array( 'custom-header', 'video' ),
3917 'label' => __( 'Header Video' ),
3918 'description' => $control_description,
3919 'section' => 'header_image',
3920 'mime_type' => 'video',
3921 // @todo These button_labels can be removed once WP_Customize_Media_Control provides mime_type-specific labels automatically. See <https://core.trac.wordpress.org/ticket/38796>.
3922 'button_labels' => array(
3923 'select' => __( 'Select Video' ),
3924 'change' => __( 'Change Video' ),
3925 'placeholder' => __( 'No video selected' ),
3926 'frame_title' => __( 'Select Video' ),
3927 'frame_button' => __( 'Choose Video' ),
3929 'active_callback' => 'is_header_video_active',
3932 $this->add_control( 'external_header_video', array(
3933 'theme_supports' => array( 'custom-header', 'video' ),
3935 'description' => __( 'Or, enter a YouTube URL:' ),
3936 'section' => 'header_image',
3937 'active_callback'=> 'is_front_page',
3940 $this->add_control( new WP_Customize_Header_Image_Control( $this ) );
3942 $this->selective_refresh->add_partial( 'custom_header', array(
3943 'selector' => '#wp-custom-header',
3944 'render_callback' => 'the_custom_header_markup',
3945 'settings' => array( 'header_video', 'external_header_video', 'header_image' ), // The image is used as a video fallback here.
3946 'container_inclusive' => true,
3949 /* Custom Background */
3951 $this->add_section( 'background_image', array(
3952 'title' => __( 'Background Image' ),
3953 'theme_supports' => 'custom-background',
3957 $this->add_setting( 'background_image', array(
3958 'default' => get_theme_support( 'custom-background', 'default-image' ),
3959 'theme_supports' => 'custom-background',
3960 'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
3963 $this->add_setting( new WP_Customize_Background_Image_Setting( $this, 'background_image_thumb', array(
3964 'theme_supports' => 'custom-background',
3965 'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
3968 $this->add_control( new WP_Customize_Background_Image_Control( $this ) );
3970 $this->add_setting( 'background_preset', array(
3971 'default' => get_theme_support( 'custom-background', 'default-preset' ),
3972 'theme_supports' => 'custom-background',
3973 'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
3976 $this->add_control( 'background_preset', array(
3977 'label' => _x( 'Preset', 'Background Preset' ),
3978 'section' => 'background_image',
3981 'default' => _x( 'Default', 'Default Preset' ),
3982 'fill' => __( 'Fill Screen' ),
3983 'fit' => __( 'Fit to Screen' ),
3984 'repeat' => _x( 'Repeat', 'Repeat Image' ),
3985 'custom' => _x( 'Custom', 'Custom Preset' ),
3989 $this->add_setting( 'background_position_x', array(
3990 'default' => get_theme_support( 'custom-background', 'default-position-x' ),
3991 'theme_supports' => 'custom-background',
3992 'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
3995 $this->add_setting( 'background_position_y', array(
3996 'default' => get_theme_support( 'custom-background', 'default-position-y' ),
3997 'theme_supports' => 'custom-background',
3998 'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
4001 $this->add_control( new WP_Customize_Background_Position_Control( $this, 'background_position', array(
4002 'label' => __( 'Image Position' ),
4003 'section' => 'background_image',
4004 'settings' => array(
4005 'x' => 'background_position_x',
4006 'y' => 'background_position_y',
4010 $this->add_setting( 'background_size', array(
4011 'default' => get_theme_support( 'custom-background', 'default-size' ),
4012 'theme_supports' => 'custom-background',
4013 'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
4016 $this->add_control( 'background_size', array(
4017 'label' => __( 'Image Size' ),
4018 'section' => 'background_image',
4021 'auto' => __( 'Original' ),
4022 'contain' => __( 'Fit to Screen' ),
4023 'cover' => __( 'Fill Screen' ),
4027 $this->add_setting( 'background_repeat', array(
4028 'default' => get_theme_support( 'custom-background', 'default-repeat' ),
4029 'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
4030 'theme_supports' => 'custom-background',
4033 $this->add_control( 'background_repeat', array(
4034 'label' => __( 'Repeat Background Image' ),
4035 'section' => 'background_image',
4036 'type' => 'checkbox',
4039 $this->add_setting( 'background_attachment', array(
4040 'default' => get_theme_support( 'custom-background', 'default-attachment' ),
4041 'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
4042 'theme_supports' => 'custom-background',
4045 $this->add_control( 'background_attachment', array(
4046 'label' => __( 'Scroll with Page' ),
4047 'section' => 'background_image',
4048 'type' => 'checkbox',
4052 // If the theme is using the default background callback, we can update
4053 // the background CSS using postMessage.
4054 if ( get_theme_support( 'custom-background', 'wp-head-callback' ) === '_custom_background_cb' ) {
4055 foreach ( array( 'color', 'image', 'preset', 'position_x', 'position_y', 'size', 'repeat', 'attachment' ) as $prop ) {
4056 $this->get_setting( 'background_' . $prop )->transport = 'postMessage';
4062 * See also https://core.trac.wordpress.org/ticket/19627 which introduces the the static-front-page theme_support.
4063 * The following replicates behavior from options-reading.php.
4066 $this->add_section( 'static_front_page', array(
4067 'title' => __( 'Static Front Page' ),
4069 'description' => __( 'Your theme supports a static front page.' ),
4070 'active_callback' => array( $this, 'has_published_pages' ),
4073 $this->add_setting( 'show_on_front', array(
4074 'default' => get_option( 'show_on_front' ),
4075 'capability' => 'manage_options',
4079 $this->add_control( 'show_on_front', array(
4080 'label' => __( 'Front page displays' ),
4081 'section' => 'static_front_page',
4084 'posts' => __( 'Your latest posts' ),
4085 'page' => __( 'A static page' ),
4089 $this->add_setting( 'page_on_front', array(
4091 'capability' => 'manage_options',
4094 $this->add_control( 'page_on_front', array(
4095 'label' => __( 'Front page' ),
4096 'section' => 'static_front_page',
4097 'type' => 'dropdown-pages',
4098 'allow_addition' => true,
4101 $this->add_setting( 'page_for_posts', array(
4103 'capability' => 'manage_options',
4106 $this->add_control( 'page_for_posts', array(
4107 'label' => __( 'Posts page' ),
4108 'section' => 'static_front_page',
4109 'type' => 'dropdown-pages',
4110 'allow_addition' => true,
4114 $this->add_section( 'custom_css', array(
4115 'title' => __( 'Additional CSS' ),
4117 'description_hidden' => true,
4118 'description' => sprintf( '%s<br /><a href="%s" class="external-link" target="_blank">%s<span class="screen-reader-text">%s</span></a>',
4119 __( 'CSS allows you to customize the appearance and layout of your site with code. Separate CSS is saved for each of your themes. In the editing area the Tab key enters a tab character. To move below this area by pressing Tab, press the Esc key followed by the Tab key.' ),
4120 esc_url( __( 'https://codex.wordpress.org/CSS' ) ),
4121 __( 'Learn more about CSS' ),
4122 __( '(link opens in a new window)' )
4126 $custom_css_setting = new WP_Customize_Custom_CSS_Setting( $this, sprintf( 'custom_css[%s]', get_stylesheet() ), array(
4127 'capability' => 'edit_css',
4128 'default' => sprintf( "/*\n%s\n*/", __( "You can add your own CSS here.\n\nClick the help icon above to learn more." ) ),
4130 $this->add_setting( $custom_css_setting );
4132 $this->add_control( 'custom_css', array(
4133 'type' => 'textarea',
4134 'section' => 'custom_css',
4135 'settings' => array( 'default' => $custom_css_setting->id ),
4136 'input_attrs' => array(
4137 'class' => 'code', // Ensures contents displayed as LTR instead of RTL.
4143 * Return whether there are published pages.
4145 * Used as active callback for static front page section and controls.
4150 * @returns bool Whether there are published (or to be published) pages.
4152 public function has_published_pages() {
4154 $setting = $this->get_setting( 'nav_menus_created_posts' );
4156 foreach ( $setting->value() as $post_id ) {
4157 if ( 'page' === get_post_type( $post_id ) ) {
4162 return 0 !== count( get_pages() );
4166 * Add settings from the POST data that were not added with code, e.g. dynamically-created settings for Widgets
4171 * @see add_dynamic_settings()
4173 public function register_dynamic_settings() {
4174 $setting_ids = array_keys( $this->unsanitized_post_values() );
4175 $this->add_dynamic_settings( $setting_ids );
4179 * Callback for validating the header_textcolor value.
4181 * Accepts 'blank', and otherwise uses sanitize_hex_color_no_hash().
4182 * Returns default text color if hex color is empty.
4186 * @param string $color
4189 public function _sanitize_header_textcolor( $color ) {
4190 if ( 'blank' === $color )
4193 $color = sanitize_hex_color_no_hash( $color );
4194 if ( empty( $color ) )
4195 $color = get_theme_support( 'custom-header', 'default-text-color' );
4201 * Callback for validating a background setting value.
4206 * @param string $value Repeat value.
4207 * @param WP_Customize_Setting $setting Setting.
4208 * @return string|WP_Error Background value or validation error.
4210 public function _sanitize_background_setting( $value, $setting ) {
4211 if ( 'background_repeat' === $setting->id ) {
4212 if ( ! in_array( $value, array( 'repeat-x', 'repeat-y', 'repeat', 'no-repeat' ) ) ) {
4213 return new WP_Error( 'invalid_value', __( 'Invalid value for background repeat.' ) );
4215 } elseif ( 'background_attachment' === $setting->id ) {
4216 if ( ! in_array( $value, array( 'fixed', 'scroll' ) ) ) {
4217 return new WP_Error( 'invalid_value', __( 'Invalid value for background attachment.' ) );
4219 } elseif ( 'background_position_x' === $setting->id ) {
4220 if ( ! in_array( $value, array( 'left', 'center', 'right' ), true ) ) {
4221 return new WP_Error( 'invalid_value', __( 'Invalid value for background position X.' ) );
4223 } elseif ( 'background_position_y' === $setting->id ) {
4224 if ( ! in_array( $value, array( 'top', 'center', 'bottom' ), true ) ) {
4225 return new WP_Error( 'invalid_value', __( 'Invalid value for background position Y.' ) );
4227 } elseif ( 'background_size' === $setting->id ) {
4228 if ( ! in_array( $value, array( 'auto', 'contain', 'cover' ), true ) ) {
4229 return new WP_Error( 'invalid_value', __( 'Invalid value for background size.' ) );
4231 } elseif ( 'background_preset' === $setting->id ) {
4232 if ( ! in_array( $value, array( 'default', 'fill', 'fit', 'repeat', 'custom' ), true ) ) {
4233 return new WP_Error( 'invalid_value', __( 'Invalid value for background size.' ) );
4235 } elseif ( 'background_image' === $setting->id || 'background_image_thumb' === $setting->id ) {
4236 $value = empty( $value ) ? '' : esc_url_raw( $value );
4238 return new WP_Error( 'unrecognized_setting', __( 'Unrecognized background setting.' ) );
4244 * Export header video settings to facilitate selective refresh.
4248 * @param array $response Response.
4249 * @param WP_Customize_Selective_Refresh $selective_refresh Selective refresh component.
4250 * @param array $partials Array of partials.
4253 public function export_header_video_settings( $response, $selective_refresh, $partials ) {
4254 if ( isset( $partials['custom_header'] ) ) {
4255 $response['custom_header_settings'] = get_header_video_settings();
4262 * Callback for validating the header_video value.
4264 * Ensures that the selected video is less than 8MB and provides an error message.
4268 * @param WP_Error $validity
4269 * @param mixed $value
4272 public function _validate_header_video( $validity, $value ) {
4273 $video = get_attached_file( absint( $value ) );
4275 $size = filesize( $video );
4276 if ( 8 < $size / pow( 1024, 2 ) ) { // Check whether the size is larger than 8MB.
4277 $validity->add( 'size_too_large',
4278 __( 'This video file is too large to use as a header video. Try a shorter video or optimize the compression settings and re-upload a file that is less than 8MB. Or, upload your video to YouTube and link it with the option below.' )
4281 if ( '.mp4' !== substr( $video, -4 ) && '.mov' !== substr( $video, -4 ) ) { // Check for .mp4 or .mov format, which (assuming h.264 encoding) are the only cross-browser-supported formats.
4282 $validity->add( 'invalid_file_type', sprintf(
4283 /* translators: 1: .mp4, 2: .mov */
4284 __( 'Only %1$s or %2$s files may be used for header video. Please convert your video file and try again, or, upload your video to YouTube and link it with the option below.' ),
4285 '<code>.mp4</code>',
4294 * Callback for validating the external_header_video value.
4296 * Ensures that the provided URL is supported.
4300 * @param WP_Error $validity
4301 * @param mixed $value
4304 public function _validate_external_header_video( $validity, $value ) {
4305 $video = esc_url_raw( $value );
4307 if ( ! preg_match( '#^https?://(?:www\.)?(?:youtube\.com/watch|youtu\.be/)#', $video ) ) {
4308 $validity->add( 'invalid_url', __( 'Please enter a valid YouTube URL.' ) );
4315 * Callback for rendering the custom logo, used in the custom_logo partial.
4317 * This method exists because the partial object and context data are passed
4318 * into a partial's render_callback so we cannot use get_custom_logo() as
4319 * the render_callback directly since it expects a blog ID as the first
4320 * argument. When WP no longer supports PHP 5.3, this method can be removed
4321 * in favor of an anonymous function.
4323 * @see WP_Customize_Manager::register_controls()
4328 * @return string Custom logo.
4330 public function _render_custom_logo_partial() {
4331 return get_custom_logo();