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_post_term_cache' => false,
802 'lazy_load_term_meta' => false,
804 if ( ! empty( $changeset_post_query->posts ) ) {
805 // Note: 'fields'=>'ids' is not being used in order to cache the post object as it will be needed.
806 $changeset_post_id = $changeset_post_query->posts[0]->ID;
807 wp_cache_set( $this->_changeset_uuid, $changeset_post_id, $cache_group );
808 return $changeset_post_id;
815 * Get the changeset post id for the loaded changeset.
820 * @return int|null Post ID on success or null if there is no post yet saved.
822 public function changeset_post_id() {
823 if ( ! isset( $this->_changeset_post_id ) ) {
824 $post_id = $this->find_changeset_post_id( $this->_changeset_uuid );
828 $this->_changeset_post_id = $post_id;
830 if ( false === $this->_changeset_post_id ) {
833 return $this->_changeset_post_id;
837 * Get the data stored in a changeset post.
842 * @param int $post_id Changeset post ID.
843 * @return array|WP_Error Changeset data or WP_Error on error.
845 protected function get_changeset_post_data( $post_id ) {
847 return new WP_Error( 'empty_post_id' );
849 $changeset_post = get_post( $post_id );
850 if ( ! $changeset_post ) {
851 return new WP_Error( 'missing_post' );
853 if ( 'customize_changeset' !== $changeset_post->post_type ) {
854 return new WP_Error( 'wrong_post_type' );
856 $changeset_data = json_decode( $changeset_post->post_content, true );
857 if ( function_exists( 'json_last_error' ) && json_last_error() ) {
858 return new WP_Error( 'json_parse_error', '', json_last_error() );
860 if ( ! is_array( $changeset_data ) ) {
861 return new WP_Error( 'expected_array' );
863 return $changeset_data;
867 * Get changeset data.
872 * @return array Changeset data.
874 public function changeset_data() {
875 if ( isset( $this->_changeset_data ) ) {
876 return $this->_changeset_data;
878 $changeset_post_id = $this->changeset_post_id();
879 if ( ! $changeset_post_id ) {
880 $this->_changeset_data = array();
882 $data = $this->get_changeset_post_data( $changeset_post_id );
883 if ( ! is_wp_error( $data ) ) {
884 $this->_changeset_data = $data;
886 $this->_changeset_data = array();
889 return $this->_changeset_data;
893 * Starter content setting IDs.
899 protected $pending_starter_content_settings_ids = array();
902 * Import theme starter content into the customized state.
907 * @param array $starter_content Starter content. Defaults to `get_theme_starter_content()`.
909 function import_theme_starter_content( $starter_content = array() ) {
910 if ( empty( $starter_content ) ) {
911 $starter_content = get_theme_starter_content();
914 $changeset_data = array();
915 if ( $this->changeset_post_id() ) {
916 $changeset_data = $this->get_changeset_post_data( $this->changeset_post_id() );
919 $sidebars_widgets = isset( $starter_content['widgets'] ) && ! empty( $this->widgets ) ? $starter_content['widgets'] : array();
920 $attachments = isset( $starter_content['attachments'] ) && ! empty( $this->nav_menus ) ? $starter_content['attachments'] : array();
921 $posts = isset( $starter_content['posts'] ) && ! empty( $this->nav_menus ) ? $starter_content['posts'] : array();
922 $options = isset( $starter_content['options'] ) ? $starter_content['options'] : array();
923 $nav_menus = isset( $starter_content['nav_menus'] ) && ! empty( $this->nav_menus ) ? $starter_content['nav_menus'] : array();
924 $theme_mods = isset( $starter_content['theme_mods'] ) ? $starter_content['theme_mods'] : array();
927 $max_widget_numbers = array();
928 foreach ( $sidebars_widgets as $sidebar_id => $widgets ) {
929 $sidebar_widget_ids = array();
930 foreach ( $widgets as $widget ) {
931 list( $id_base, $instance ) = $widget;
933 if ( ! isset( $max_widget_numbers[ $id_base ] ) ) {
935 // When $settings is an array-like object, get an intrinsic array for use with array_keys().
936 $settings = get_option( "widget_{$id_base}", array() );
937 if ( $settings instanceof ArrayObject || $settings instanceof ArrayIterator ) {
938 $settings = $settings->getArrayCopy();
941 // Find the max widget number for this type.
942 $widget_numbers = array_keys( $settings );
943 if ( count( $widget_numbers ) > 0 ) {
944 $widget_numbers[] = 1;
945 $max_widget_numbers[ $id_base ] = call_user_func_array( 'max', $widget_numbers );
947 $max_widget_numbers[ $id_base ] = 1;
950 $max_widget_numbers[ $id_base ] += 1;
952 $widget_id = sprintf( '%s-%d', $id_base, $max_widget_numbers[ $id_base ] );
953 $setting_id = sprintf( 'widget_%s[%d]', $id_base, $max_widget_numbers[ $id_base ] );
955 $setting_value = $this->widgets->sanitize_widget_js_instance( $instance );
956 if ( empty( $changeset_data[ $setting_id ] ) || ! empty( $changeset_data[ $setting_id ]['starter_content'] ) ) {
957 $this->set_post_value( $setting_id, $setting_value );
958 $this->pending_starter_content_settings_ids[] = $setting_id;
960 $sidebar_widget_ids[] = $widget_id;
963 $setting_id = sprintf( 'sidebars_widgets[%s]', $sidebar_id );
964 if ( empty( $changeset_data[ $setting_id ] ) || ! empty( $changeset_data[ $setting_id ]['starter_content'] ) ) {
965 $this->set_post_value( $setting_id, $sidebar_widget_ids );
966 $this->pending_starter_content_settings_ids[] = $setting_id;
970 $starter_content_auto_draft_post_ids = array();
971 if ( ! empty( $changeset_data['nav_menus_created_posts']['value'] ) ) {
972 $starter_content_auto_draft_post_ids = array_merge( $starter_content_auto_draft_post_ids, $changeset_data['nav_menus_created_posts']['value'] );
975 // Make an index of all the posts needed and what their slugs are.
976 $needed_posts = array();
977 $attachments = $this->prepare_starter_content_attachments( $attachments );
978 foreach ( $attachments as $attachment ) {
979 $key = 'attachment:' . $attachment['post_name'];
980 $needed_posts[ $key ] = true;
982 foreach ( array_keys( $posts ) as $post_symbol ) {
983 if ( empty( $posts[ $post_symbol ]['post_name'] ) && empty( $posts[ $post_symbol ]['post_title'] ) ) {
984 unset( $posts[ $post_symbol ] );
987 if ( empty( $posts[ $post_symbol ]['post_name'] ) ) {
988 $posts[ $post_symbol ]['post_name'] = sanitize_title( $posts[ $post_symbol ]['post_title'] );
990 if ( empty( $posts[ $post_symbol ]['post_type'] ) ) {
991 $posts[ $post_symbol ]['post_type'] = 'post';
993 $needed_posts[ $posts[ $post_symbol ]['post_type'] . ':' . $posts[ $post_symbol ]['post_name'] ] = true;
995 $all_post_slugs = array_merge(
996 wp_list_pluck( $attachments, 'post_name' ),
997 wp_list_pluck( $posts, 'post_name' )
1000 // Re-use auto-draft starter content posts referenced in the current customized state.
1001 $existing_starter_content_posts = array();
1002 if ( ! empty( $starter_content_auto_draft_post_ids ) ) {
1003 $existing_posts_query = new WP_Query( array(
1004 'post__in' => $starter_content_auto_draft_post_ids,
1005 'post_status' => 'auto-draft',
1006 'post_type' => 'any',
1007 'posts_per_page' => -1,
1009 foreach ( $existing_posts_query->posts as $existing_post ) {
1010 $post_name = $existing_post->post_name;
1011 if ( empty( $post_name ) ) {
1012 $post_name = get_post_meta( $existing_post->ID, '_customize_draft_post_name', true );
1014 $existing_starter_content_posts[ $existing_post->post_type . ':' . $post_name ] = $existing_post;
1018 // Re-use non-auto-draft posts.
1019 if ( ! empty( $all_post_slugs ) ) {
1020 $existing_posts_query = new WP_Query( array(
1021 'post_name__in' => $all_post_slugs,
1022 'post_status' => array_diff( get_post_stati(), array( 'auto-draft' ) ),
1023 'post_type' => 'any',
1024 'posts_per_page' => -1,
1026 foreach ( $existing_posts_query->posts as $existing_post ) {
1027 $key = $existing_post->post_type . ':' . $existing_post->post_name;
1028 if ( isset( $needed_posts[ $key ] ) && ! isset( $existing_starter_content_posts[ $key ] ) ) {
1029 $existing_starter_content_posts[ $key ] = $existing_post;
1034 // Attachments are technically posts but handled differently.
1035 if ( ! empty( $attachments ) ) {
1037 $attachment_ids = array();
1039 foreach ( $attachments as $symbol => $attachment ) {
1040 $file_array = array(
1041 'name' => $attachment['file_name'],
1043 $file_path = $attachment['file_path'];
1044 $attachment_id = null;
1045 $attached_file = null;
1046 if ( isset( $existing_starter_content_posts[ 'attachment:' . $attachment['post_name'] ] ) ) {
1047 $attachment_post = $existing_starter_content_posts[ 'attachment:' . $attachment['post_name'] ];
1048 $attachment_id = $attachment_post->ID;
1049 $attached_file = get_attached_file( $attachment_id );
1050 if ( empty( $attached_file ) || ! file_exists( $attached_file ) ) {
1051 $attachment_id = null;
1052 $attached_file = null;
1053 } elseif ( $this->get_stylesheet() !== get_post_meta( $attachment_post->ID, '_starter_content_theme', true ) ) {
1055 // Re-generate attachment metadata since it was previously generated for a different theme.
1056 $metadata = wp_generate_attachment_metadata( $attachment_post->ID, $attached_file );
1057 wp_update_attachment_metadata( $attachment_id, $metadata );
1058 update_post_meta( $attachment_id, '_starter_content_theme', $this->get_stylesheet() );
1062 // Insert the attachment auto-draft because it doesn't yet exist or the attached file is gone.
1063 if ( ! $attachment_id ) {
1065 // Copy file to temp location so that original file won't get deleted from theme after sideloading.
1066 $temp_file_name = wp_tempnam( basename( $file_path ) );
1067 if ( $temp_file_name && copy( $file_path, $temp_file_name ) ) {
1068 $file_array['tmp_name'] = $temp_file_name;
1070 if ( empty( $file_array['tmp_name'] ) ) {
1074 $attachment_post_data = array_merge(
1075 wp_array_slice_assoc( $attachment, array( 'post_title', 'post_content', 'post_excerpt' ) ),
1077 'post_status' => 'auto-draft', // So attachment will be garbage collected in a week if changeset is never published.
1081 // In PHP < 5.6 filesize() returns 0 for the temp files unless we clear the file status cache.
1082 // Technically, PHP < 5.6.0 || < 5.5.13 || < 5.4.29 but no need to be so targeted.
1083 // See https://bugs.php.net/bug.php?id=65701
1084 if ( version_compare( PHP_VERSION, '5.6', '<' ) ) {
1088 $attachment_id = media_handle_sideload( $file_array, 0, null, $attachment_post_data );
1089 if ( is_wp_error( $attachment_id ) ) {
1092 update_post_meta( $attachment_id, '_starter_content_theme', $this->get_stylesheet() );
1093 update_post_meta( $attachment_id, '_customize_draft_post_name', $attachment['post_name'] );
1096 $attachment_ids[ $symbol ] = $attachment_id;
1098 $starter_content_auto_draft_post_ids = array_merge( $starter_content_auto_draft_post_ids, array_values( $attachment_ids ) );
1102 if ( ! empty( $posts ) ) {
1103 foreach ( array_keys( $posts ) as $post_symbol ) {
1104 if ( empty( $posts[ $post_symbol ]['post_type'] ) || empty( $posts[ $post_symbol ]['post_name'] ) ) {
1107 $post_type = $posts[ $post_symbol ]['post_type'];
1108 if ( ! empty( $posts[ $post_symbol ]['post_name'] ) ) {
1109 $post_name = $posts[ $post_symbol ]['post_name'];
1110 } elseif ( ! empty( $posts[ $post_symbol ]['post_title'] ) ) {
1111 $post_name = sanitize_title( $posts[ $post_symbol ]['post_title'] );
1116 // Use existing auto-draft post if one already exists with the same type and name.
1117 if ( isset( $existing_starter_content_posts[ $post_type . ':' . $post_name ] ) ) {
1118 $posts[ $post_symbol ]['ID'] = $existing_starter_content_posts[ $post_type . ':' . $post_name ]->ID;
1122 // Translate the featured image symbol.
1123 if ( ! empty( $posts[ $post_symbol ]['thumbnail'] )
1124 && preg_match( '/^{{(?P<symbol>.+)}}$/', $posts[ $post_symbol ]['thumbnail'], $matches )
1125 && isset( $attachment_ids[ $matches['symbol'] ] ) ) {
1126 $posts[ $post_symbol ]['meta_input']['_thumbnail_id'] = $attachment_ids[ $matches['symbol'] ];
1129 if ( ! empty( $posts[ $post_symbol ]['template'] ) ) {
1130 $posts[ $post_symbol ]['meta_input']['_wp_page_template'] = $posts[ $post_symbol ]['template'];
1133 $r = $this->nav_menus->insert_auto_draft_post( $posts[ $post_symbol ] );
1134 if ( $r instanceof WP_Post ) {
1135 $posts[ $post_symbol ]['ID'] = $r->ID;
1139 $starter_content_auto_draft_post_ids = array_merge( $starter_content_auto_draft_post_ids, wp_list_pluck( $posts, 'ID' ) );
1142 // The nav_menus_created_posts setting is why nav_menus component is dependency for adding posts.
1143 if ( ! empty( $this->nav_menus ) && ! empty( $starter_content_auto_draft_post_ids ) ) {
1144 $setting_id = 'nav_menus_created_posts';
1145 $this->set_post_value( $setting_id, array_unique( array_values( $starter_content_auto_draft_post_ids ) ) );
1146 $this->pending_starter_content_settings_ids[] = $setting_id;
1150 $placeholder_id = -1;
1151 $reused_nav_menu_setting_ids = array();
1152 foreach ( $nav_menus as $nav_menu_location => $nav_menu ) {
1154 $nav_menu_term_id = null;
1155 $nav_menu_setting_id = null;
1158 // Look for an existing placeholder menu with starter content to re-use.
1159 foreach ( $changeset_data as $setting_id => $setting_params ) {
1161 ! empty( $setting_params['starter_content'] )
1163 ! in_array( $setting_id, $reused_nav_menu_setting_ids, true )
1165 preg_match( '#^nav_menu\[(?P<nav_menu_id>-?\d+)\]$#', $setting_id, $matches )
1168 $nav_menu_term_id = intval( $matches['nav_menu_id'] );
1169 $nav_menu_setting_id = $setting_id;
1170 $reused_nav_menu_setting_ids[] = $setting_id;
1175 if ( ! $nav_menu_term_id ) {
1176 while ( isset( $changeset_data[ sprintf( 'nav_menu[%d]', $placeholder_id ) ] ) ) {
1179 $nav_menu_term_id = $placeholder_id;
1180 $nav_menu_setting_id = sprintf( 'nav_menu[%d]', $placeholder_id );
1183 $this->set_post_value( $nav_menu_setting_id, array(
1184 'name' => isset( $nav_menu['name'] ) ? $nav_menu['name'] : $nav_menu_location,
1186 $this->pending_starter_content_settings_ids[] = $nav_menu_setting_id;
1188 // @todo Add support for menu_item_parent.
1190 foreach ( $nav_menu['items'] as $nav_menu_item ) {
1191 $nav_menu_item_setting_id = sprintf( 'nav_menu_item[%d]', $placeholder_id-- );
1192 if ( ! isset( $nav_menu_item['position'] ) ) {
1193 $nav_menu_item['position'] = $position++;
1195 $nav_menu_item['nav_menu_term_id'] = $nav_menu_term_id;
1197 if ( isset( $nav_menu_item['object_id'] ) ) {
1198 if ( 'post_type' === $nav_menu_item['type'] && preg_match( '/^{{(?P<symbol>.+)}}$/', $nav_menu_item['object_id'], $matches ) && isset( $posts[ $matches['symbol'] ] ) ) {
1199 $nav_menu_item['object_id'] = $posts[ $matches['symbol'] ]['ID'];
1200 if ( empty( $nav_menu_item['title'] ) ) {
1201 $original_object = get_post( $nav_menu_item['object_id'] );
1202 $nav_menu_item['title'] = $original_object->post_title;
1208 $nav_menu_item['object_id'] = 0;
1211 if ( empty( $changeset_data[ $nav_menu_item_setting_id ] ) || ! empty( $changeset_data[ $nav_menu_item_setting_id ]['starter_content'] ) ) {
1212 $this->set_post_value( $nav_menu_item_setting_id, $nav_menu_item );
1213 $this->pending_starter_content_settings_ids[] = $nav_menu_item_setting_id;
1217 $setting_id = sprintf( 'nav_menu_locations[%s]', $nav_menu_location );
1218 if ( empty( $changeset_data[ $setting_id ] ) || ! empty( $changeset_data[ $setting_id ]['starter_content'] ) ) {
1219 $this->set_post_value( $setting_id, $nav_menu_term_id );
1220 $this->pending_starter_content_settings_ids[] = $setting_id;
1225 foreach ( $options as $name => $value ) {
1226 if ( preg_match( '/^{{(?P<symbol>.+)}}$/', $value, $matches ) ) {
1227 if ( isset( $posts[ $matches['symbol'] ] ) ) {
1228 $value = $posts[ $matches['symbol'] ]['ID'];
1229 } elseif ( isset( $attachment_ids[ $matches['symbol'] ] ) ) {
1230 $value = $attachment_ids[ $matches['symbol'] ];
1236 if ( empty( $changeset_data[ $name ] ) || ! empty( $changeset_data[ $name ]['starter_content'] ) ) {
1237 $this->set_post_value( $name, $value );
1238 $this->pending_starter_content_settings_ids[] = $name;
1243 foreach ( $theme_mods as $name => $value ) {
1244 if ( preg_match( '/^{{(?P<symbol>.+)}}$/', $value, $matches ) ) {
1245 if ( isset( $posts[ $matches['symbol'] ] ) ) {
1246 $value = $posts[ $matches['symbol'] ]['ID'];
1247 } elseif ( isset( $attachment_ids[ $matches['symbol'] ] ) ) {
1248 $value = $attachment_ids[ $matches['symbol'] ];
1254 // Handle header image as special case since setting has a legacy format.
1255 if ( 'header_image' === $name ) {
1256 $name = 'header_image_data';
1257 $metadata = wp_get_attachment_metadata( $value );
1258 if ( empty( $metadata ) ) {
1262 'attachment_id' => $value,
1263 'url' => wp_get_attachment_url( $value ),
1264 'height' => $metadata['height'],
1265 'width' => $metadata['width'],
1267 } elseif ( 'background_image' === $name ) {
1268 $value = wp_get_attachment_url( $value );
1271 if ( empty( $changeset_data[ $name ] ) || ! empty( $changeset_data[ $name ]['starter_content'] ) ) {
1272 $this->set_post_value( $name, $value );
1273 $this->pending_starter_content_settings_ids[] = $name;
1277 if ( ! empty( $this->pending_starter_content_settings_ids ) ) {
1278 if ( did_action( 'customize_register' ) ) {
1279 $this->_save_starter_content_changeset();
1281 add_action( 'customize_register', array( $this, '_save_starter_content_changeset' ), 1000 );
1287 * Prepare starter content attachments.
1289 * Ensure that the attachments are valid and that they have slugs and file name/path.
1294 * @param array $attachments Attachments.
1295 * @return array Prepared attachments.
1297 protected function prepare_starter_content_attachments( $attachments ) {
1298 $prepared_attachments = array();
1299 if ( empty( $attachments ) ) {
1300 return $prepared_attachments;
1303 // Such is The WordPress Way.
1304 require_once( ABSPATH . 'wp-admin/includes/file.php' );
1305 require_once( ABSPATH . 'wp-admin/includes/media.php' );
1306 require_once( ABSPATH . 'wp-admin/includes/image.php' );
1308 foreach ( $attachments as $symbol => $attachment ) {
1310 // A file is required and URLs to files are not currently allowed.
1311 if ( empty( $attachment['file'] ) || preg_match( '#^https?://$#', $attachment['file'] ) ) {
1316 if ( file_exists( $attachment['file'] ) ) {
1317 $file_path = $attachment['file']; // Could be absolute path to file in plugin.
1318 } elseif ( is_child_theme() && file_exists( get_stylesheet_directory() . '/' . $attachment['file'] ) ) {
1319 $file_path = get_stylesheet_directory() . '/' . $attachment['file'];
1320 } elseif ( file_exists( get_template_directory() . '/' . $attachment['file'] ) ) {
1321 $file_path = get_template_directory() . '/' . $attachment['file'];
1325 $file_name = basename( $attachment['file'] );
1327 // Skip file types that are not recognized.
1328 $checked_filetype = wp_check_filetype( $file_name );
1329 if ( empty( $checked_filetype['type'] ) ) {
1333 // Ensure post_name is set since not automatically derived from post_title for new auto-draft posts.
1334 if ( empty( $attachment['post_name'] ) ) {
1335 if ( ! empty( $attachment['post_title'] ) ) {
1336 $attachment['post_name'] = sanitize_title( $attachment['post_title'] );
1338 $attachment['post_name'] = sanitize_title( preg_replace( '/\.\w+$/', '', $file_name ) );
1342 $attachment['file_name'] = $file_name;
1343 $attachment['file_path'] = $file_path;
1344 $prepared_attachments[ $symbol ] = $attachment;
1346 return $prepared_attachments;
1350 * Save starter content changeset.
1355 public function _save_starter_content_changeset() {
1357 if ( empty( $this->pending_starter_content_settings_ids ) ) {
1361 $this->save_changeset_post( array(
1362 'data' => array_fill_keys( $this->pending_starter_content_settings_ids, array( 'starter_content' => true ) ),
1363 'starter_content' => true,
1366 $this->pending_starter_content_settings_ids = array();
1370 * Get dirty pre-sanitized setting values in the current customized state.
1372 * The returned array consists of a merge of three sources:
1373 * 1. If the theme is not currently active, then the base array is any stashed
1374 * theme mods that were modified previously but never published.
1375 * 2. The values from the current changeset, if it exists.
1376 * 3. If the user can customize, the values parsed from the incoming
1377 * `$_POST['customized']` JSON data.
1378 * 4. Any programmatically-set post values via `WP_Customize_Manager::set_post_value()`.
1380 * The name "unsanitized_post_values" is a carry-over from when the customized
1381 * state was exclusively sourced from `$_POST['customized']`. Nevertheless,
1382 * the value returned will come from the current changeset post and from the
1383 * incoming post data.
1386 * @since 4.7.0 Added $args param and merging with changeset values and stashed theme mods.
1388 * @param array $args {
1391 * @type bool $exclude_changeset Whether the changeset values should also be excluded. Defaults to false.
1392 * @type bool $exclude_post_data Whether the post input values should also be excluded. Defaults to false when lacking the customize capability.
1396 public function unsanitized_post_values( $args = array() ) {
1397 $args = array_merge(
1399 'exclude_changeset' => false,
1400 'exclude_post_data' => ! current_user_can( 'customize' ),
1407 // Let default values be from the stashed theme mods if doing a theme switch and if no changeset is present.
1408 if ( ! $this->is_theme_active() ) {
1409 $stashed_theme_mods = get_option( 'customize_stashed_theme_mods' );
1410 $stylesheet = $this->get_stylesheet();
1411 if ( isset( $stashed_theme_mods[ $stylesheet ] ) ) {
1412 $values = array_merge( $values, wp_list_pluck( $stashed_theme_mods[ $stylesheet ], 'value' ) );
1416 if ( ! $args['exclude_changeset'] ) {
1417 foreach ( $this->changeset_data() as $setting_id => $setting_params ) {
1418 if ( ! array_key_exists( 'value', $setting_params ) ) {
1421 if ( isset( $setting_params['type'] ) && 'theme_mod' === $setting_params['type'] ) {
1423 // Ensure that theme mods values are only used if they were saved under the current theme.
1424 $namespace_pattern = '/^(?P<stylesheet>.+?)::(?P<setting_id>.+)$/';
1425 if ( preg_match( $namespace_pattern, $setting_id, $matches ) && $this->get_stylesheet() === $matches['stylesheet'] ) {
1426 $values[ $matches['setting_id'] ] = $setting_params['value'];
1429 $values[ $setting_id ] = $setting_params['value'];
1434 if ( ! $args['exclude_post_data'] ) {
1435 if ( ! isset( $this->_post_values ) ) {
1436 if ( isset( $_POST['customized'] ) ) {
1437 $post_values = json_decode( wp_unslash( $_POST['customized'] ), true );
1439 $post_values = array();
1441 if ( is_array( $post_values ) ) {
1442 $this->_post_values = $post_values;
1444 $this->_post_values = array();
1447 $values = array_merge( $values, $this->_post_values );
1453 * Returns the sanitized value for a given setting from the current customized state.
1455 * The name "post_value" is a carry-over from when the customized state was exclusively
1456 * sourced from `$_POST['customized']`. Nevertheless, the value returned will come
1457 * from the current changeset post and from the incoming post data.
1460 * @since 4.1.1 Introduced the `$default` parameter.
1461 * @since 4.6.0 `$default` is now returned early when the setting post value is invalid.
1464 * @see WP_REST_Server::dispatch()
1465 * @see WP_Rest_Request::sanitize_params()
1466 * @see WP_Rest_Request::has_valid_params()
1468 * @param WP_Customize_Setting $setting A WP_Customize_Setting derived object.
1469 * @param mixed $default Value returned $setting has no post value (added in 4.2.0)
1470 * or the post value is invalid (added in 4.6.0).
1471 * @return string|mixed $post_value Sanitized value or the $default provided.
1473 public function post_value( $setting, $default = null ) {
1474 $post_values = $this->unsanitized_post_values();
1475 if ( ! array_key_exists( $setting->id, $post_values ) ) {
1478 $value = $post_values[ $setting->id ];
1479 $valid = $setting->validate( $value );
1480 if ( is_wp_error( $valid ) ) {
1483 $value = $setting->sanitize( $value );
1484 if ( is_null( $value ) || is_wp_error( $value ) ) {
1491 * Override a setting's value in the current customized state.
1493 * The name "post_value" is a carry-over from when the customized state was
1494 * exclusively sourced from `$_POST['customized']`.
1499 * @param string $setting_id ID for the WP_Customize_Setting instance.
1500 * @param mixed $value Post value.
1502 public function set_post_value( $setting_id, $value ) {
1503 $this->unsanitized_post_values(); // Populate _post_values from $_POST['customized'].
1504 $this->_post_values[ $setting_id ] = $value;
1507 * Announce when a specific setting's unsanitized post value has been set.
1509 * Fires when the WP_Customize_Manager::set_post_value() method is called.
1511 * The dynamic portion of the hook name, `$setting_id`, refers to the setting ID.
1515 * @param mixed $value Unsanitized setting post value.
1516 * @param WP_Customize_Manager $this WP_Customize_Manager instance.
1518 do_action( "customize_post_value_set_{$setting_id}", $value, $this );
1521 * Announce when any setting's unsanitized post value has been set.
1523 * Fires when the WP_Customize_Manager::set_post_value() method is called.
1525 * This is useful for `WP_Customize_Setting` instances to watch
1526 * in order to update a cached previewed value.
1530 * @param string $setting_id Setting ID.
1531 * @param mixed $value Unsanitized setting post value.
1532 * @param WP_Customize_Manager $this WP_Customize_Manager instance.
1534 do_action( 'customize_post_value_set', $setting_id, $value, $this );
1538 * Print JavaScript settings.
1542 public function customize_preview_init() {
1545 * Now that Customizer previews are loaded into iframes via GET requests
1546 * and natural URLs with transaction UUIDs added, we need to ensure that
1547 * the responses are never cached by proxies. In practice, this will not
1548 * be needed if the user is logged-in anyway. But if anonymous access is
1549 * allowed then the auth cookies would not be sent and WordPress would
1550 * not send no-cache headers by default.
1552 if ( ! headers_sent() ) {
1554 header( 'X-Robots: noindex, nofollow, noarchive' );
1556 add_action( 'wp_head', 'wp_no_robots' );
1557 add_filter( 'wp_headers', array( $this, 'filter_iframe_security_headers' ) );
1560 * If preview is being served inside the customizer preview iframe, and
1561 * if the user doesn't have customize capability, then it is assumed
1562 * that the user's session has expired and they need to re-authenticate.
1564 if ( $this->messenger_channel && ! current_user_can( 'customize' ) ) {
1565 $this->wp_die( -1, __( 'Unauthorized. You may remove the customize_messenger_channel param to preview as frontend.' ) );
1569 $this->prepare_controls();
1571 add_filter( 'wp_redirect', array( $this, 'add_state_query_params' ) );
1573 wp_enqueue_script( 'customize-preview' );
1574 add_action( 'wp_head', array( $this, 'customize_preview_loading_style' ) );
1575 add_action( 'wp_head', array( $this, 'remove_frameless_preview_messenger_channel' ) );
1576 add_action( 'wp_footer', array( $this, 'customize_preview_settings' ), 20 );
1577 add_filter( 'get_edit_post_link', '__return_empty_string' );
1580 * Fires once the Customizer preview has initialized and JavaScript
1581 * settings have been printed.
1585 * @param WP_Customize_Manager $this WP_Customize_Manager instance.
1587 do_action( 'customize_preview_init', $this );
1591 * Filter the X-Frame-Options and Content-Security-Policy headers to ensure frontend can load in customizer.
1596 * @param array $headers Headers.
1597 * @return array Headers.
1599 public function filter_iframe_security_headers( $headers ) {
1600 $customize_url = admin_url( 'customize.php' );
1601 $headers['X-Frame-Options'] = 'ALLOW-FROM ' . $customize_url;
1602 $headers['Content-Security-Policy'] = 'frame-ancestors ' . preg_replace( '#^(\w+://[^/]+).+?$#', '$1', $customize_url );
1607 * Add customize state query params to a given URL if preview is allowed.
1611 * @see wp_redirect()
1612 * @see WP_Customize_Manager::get_allowed_url()
1614 * @param string $url URL.
1615 * @return string URL.
1617 public function add_state_query_params( $url ) {
1618 $parsed_original_url = wp_parse_url( $url );
1619 $is_allowed = false;
1620 foreach ( $this->get_allowed_urls() as $allowed_url ) {
1621 $parsed_allowed_url = wp_parse_url( $allowed_url );
1623 $parsed_allowed_url['scheme'] === $parsed_original_url['scheme']
1625 $parsed_allowed_url['host'] === $parsed_original_url['host']
1627 0 === strpos( $parsed_original_url['path'], $parsed_allowed_url['path'] )
1629 if ( $is_allowed ) {
1634 if ( $is_allowed ) {
1635 $query_params = array(
1636 'customize_changeset_uuid' => $this->changeset_uuid(),
1638 if ( ! $this->is_theme_active() ) {
1639 $query_params['customize_theme'] = $this->get_stylesheet();
1641 if ( $this->messenger_channel ) {
1642 $query_params['customize_messenger_channel'] = $this->messenger_channel;
1644 $url = add_query_arg( $query_params, $url );
1651 * Prevent sending a 404 status when returning the response for the customize
1652 * preview, since it causes the jQuery Ajax to fail. Send 200 instead.
1658 public function customize_preview_override_404_status() {
1659 _deprecated_function( __METHOD__, '4.7.0' );
1663 * Print base element for preview frame.
1668 public function customize_preview_base() {
1669 _deprecated_function( __METHOD__, '4.7.0' );
1673 * Print a workaround to handle HTML5 tags in IE < 9.
1676 * @deprecated 4.7.0 Customizer no longer supports IE8, so all supported browsers recognize HTML5.
1678 public function customize_preview_html5() {
1679 _deprecated_function( __FUNCTION__, '4.7.0' );
1683 * Print CSS for loading indicators for the Customizer preview.
1688 public function customize_preview_loading_style() {
1690 body.wp-customizer-unloading {
1692 cursor: progress !important;
1693 -webkit-transition: opacity 0.5s;
1694 transition: opacity 0.5s;
1696 body.wp-customizer-unloading * {
1697 pointer-events: none !important;
1699 form.customize-unpreviewable,
1700 form.customize-unpreviewable input,
1701 form.customize-unpreviewable select,
1702 form.customize-unpreviewable button,
1703 a.customize-unpreviewable,
1704 area.customize-unpreviewable {
1705 cursor: not-allowed !important;
1711 * Remove customize_messenger_channel query parameter from the preview window when it is not in an iframe.
1713 * This ensures that the admin bar will be shown. It also ensures that link navigation will
1714 * work as expected since the parent frame is not being sent the URL to navigate to.
1719 public function remove_frameless_preview_messenger_channel() {
1720 if ( ! $this->messenger_channel ) {
1726 var urlParser, oldQueryParams, newQueryParams, i;
1727 if ( parent !== window ) {
1730 urlParser = document.createElement( 'a' );
1731 urlParser.href = location.href;
1732 oldQueryParams = urlParser.search.substr( 1 ).split( /&/ );
1733 newQueryParams = [];
1734 for ( i = 0; i < oldQueryParams.length; i += 1 ) {
1735 if ( ! /^customize_messenger_channel=/.test( oldQueryParams[ i ] ) ) {
1736 newQueryParams.push( oldQueryParams[ i ] );
1739 urlParser.search = newQueryParams.join( '&' );
1740 if ( urlParser.search !== location.search ) {
1741 location.replace( urlParser.href );
1749 * Print JavaScript settings for preview frame.
1753 public function customize_preview_settings() {
1754 $post_values = $this->unsanitized_post_values( array( 'exclude_changeset' => true ) );
1755 $setting_validities = $this->validate_setting_values( $post_values );
1756 $exported_setting_validities = array_map( array( $this, 'prepare_setting_validity_for_js' ), $setting_validities );
1758 // Note that the REQUEST_URI is not passed into home_url() since this breaks subdirectory installs.
1759 $self_url = empty( $_SERVER['REQUEST_URI'] ) ? home_url( '/' ) : esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) );
1760 $state_query_params = array(
1762 'customize_changeset_uuid',
1763 'customize_messenger_channel',
1765 $self_url = remove_query_arg( $state_query_params, $self_url );
1767 $allowed_urls = $this->get_allowed_urls();
1768 $allowed_hosts = array();
1769 foreach ( $allowed_urls as $allowed_url ) {
1770 $parsed = wp_parse_url( $allowed_url );
1771 if ( empty( $parsed['host'] ) ) {
1774 $host = $parsed['host'];
1775 if ( ! empty( $parsed['port'] ) ) {
1776 $host .= ':' . $parsed['port'];
1778 $allowed_hosts[] = $host;
1781 $switched_locale = switch_to_locale( get_user_locale() );
1783 'shiftClickToEdit' => __( 'Shift-click to edit this element.' ),
1784 'linkUnpreviewable' => __( 'This link is not live-previewable.' ),
1785 'formUnpreviewable' => __( 'This form is not live-previewable.' ),
1787 if ( $switched_locale ) {
1788 restore_previous_locale();
1792 'changeset' => array(
1793 'uuid' => $this->_changeset_uuid,
1795 'timeouts' => array(
1796 'selectiveRefresh' => 250,
1797 'keepAliveSend' => 1000,
1800 'stylesheet' => $this->get_stylesheet(),
1801 'active' => $this->is_theme_active(),
1804 'self' => $self_url,
1805 'allowed' => array_map( 'esc_url_raw', $this->get_allowed_urls() ),
1806 'allowedHosts' => array_unique( $allowed_hosts ),
1807 'isCrossDomain' => $this->is_cross_domain(),
1809 'channel' => $this->messenger_channel,
1810 'activePanels' => array(),
1811 'activeSections' => array(),
1812 'activeControls' => array(),
1813 'settingValidities' => $exported_setting_validities,
1814 'nonce' => current_user_can( 'customize' ) ? $this->get_nonces() : array(),
1816 '_dirty' => array_keys( $post_values ),
1819 foreach ( $this->panels as $panel_id => $panel ) {
1820 if ( $panel->check_capabilities() ) {
1821 $settings['activePanels'][ $panel_id ] = $panel->active();
1822 foreach ( $panel->sections as $section_id => $section ) {
1823 if ( $section->check_capabilities() ) {
1824 $settings['activeSections'][ $section_id ] = $section->active();
1829 foreach ( $this->sections as $id => $section ) {
1830 if ( $section->check_capabilities() ) {
1831 $settings['activeSections'][ $id ] = $section->active();
1834 foreach ( $this->controls as $id => $control ) {
1835 if ( $control->check_capabilities() ) {
1836 $settings['activeControls'][ $id ] = $control->active();
1841 <script type="text/javascript">
1842 var _wpCustomizeSettings = <?php echo wp_json_encode( $settings ); ?>;
1843 _wpCustomizeSettings.values = {};
1847 * Serialize settings separately from the initial _wpCustomizeSettings
1848 * serialization in order to avoid a peak memory usage spike.
1849 * @todo We may not even need to export the values at all since the pane syncs them anyway.
1851 foreach ( $this->settings as $id => $setting ) {
1852 if ( $setting->check_capabilities() ) {
1855 wp_json_encode( $id ),
1856 wp_json_encode( $setting->js_value() )
1861 })( _wpCustomizeSettings.values );
1867 * Prints a signature so we can ensure the Customizer was properly executed.
1872 public function customize_preview_signature() {
1873 _deprecated_function( __METHOD__, '4.7.0' );
1877 * Removes the signature in case we experience a case where the Customizer was not properly executed.
1882 * @param mixed $return Value passed through for {@see 'wp_die_handler'} filter.
1883 * @return mixed Value passed through for {@see 'wp_die_handler'} filter.
1885 public function remove_preview_signature( $return = null ) {
1886 _deprecated_function( __METHOD__, '4.7.0' );
1892 * Is it a theme preview?
1896 * @return bool True if it's a preview, false if not.
1898 public function is_preview() {
1899 return (bool) $this->previewing;
1903 * Retrieve the template name of the previewed theme.
1907 * @return string Template name.
1909 public function get_template() {
1910 return $this->theme()->get_template();
1914 * Retrieve the stylesheet name of the previewed theme.
1918 * @return string Stylesheet name.
1920 public function get_stylesheet() {
1921 return $this->theme()->get_stylesheet();
1925 * Retrieve the template root of the previewed theme.
1929 * @return string Theme root.
1931 public function get_template_root() {
1932 return get_raw_theme_root( $this->get_template(), true );
1936 * Retrieve the stylesheet root of the previewed theme.
1940 * @return string Theme root.
1942 public function get_stylesheet_root() {
1943 return get_raw_theme_root( $this->get_stylesheet(), true );
1947 * Filters the current theme and return the name of the previewed theme.
1951 * @param $current_theme {@internal Parameter is not used}
1952 * @return string Theme name.
1954 public function current_theme( $current_theme ) {
1955 return $this->theme()->display('Name');
1959 * Validates setting values.
1961 * Validation is skipped for unregistered settings or for values that are
1962 * already null since they will be skipped anyway. Sanitization is applied
1963 * to values that pass validation, and values that become null or `WP_Error`
1964 * after sanitizing are marked invalid.
1969 * @see WP_REST_Request::has_valid_params()
1970 * @see WP_Customize_Setting::validate()
1972 * @param array $setting_values Mapping of setting IDs to values to validate and sanitize.
1973 * @param array $options {
1976 * @type bool $validate_existence Whether a setting's existence will be checked.
1977 * @type bool $validate_capability Whether the setting capability will be checked.
1979 * @return array Mapping of setting IDs to return value of validate method calls, either `true` or `WP_Error`.
1981 public function validate_setting_values( $setting_values, $options = array() ) {
1982 $options = wp_parse_args( $options, array(
1983 'validate_capability' => false,
1984 'validate_existence' => false,
1987 $validities = array();
1988 foreach ( $setting_values as $setting_id => $unsanitized_value ) {
1989 $setting = $this->get_setting( $setting_id );
1991 if ( $options['validate_existence'] ) {
1992 $validities[ $setting_id ] = new WP_Error( 'unrecognized', __( 'Setting does not exist or is unrecognized.' ) );
1996 if ( $options['validate_capability'] && ! current_user_can( $setting->capability ) ) {
1997 $validity = new WP_Error( 'unauthorized', __( 'Unauthorized to modify setting due to capability.' ) );
1999 if ( is_null( $unsanitized_value ) ) {
2002 $validity = $setting->validate( $unsanitized_value );
2004 if ( ! is_wp_error( $validity ) ) {
2005 /** This filter is documented in wp-includes/class-wp-customize-setting.php */
2006 $late_validity = apply_filters( "customize_validate_{$setting->id}", new WP_Error(), $unsanitized_value, $setting );
2007 if ( ! empty( $late_validity->errors ) ) {
2008 $validity = $late_validity;
2011 if ( ! is_wp_error( $validity ) ) {
2012 $value = $setting->sanitize( $unsanitized_value );
2013 if ( is_null( $value ) ) {
2015 } elseif ( is_wp_error( $value ) ) {
2019 if ( false === $validity ) {
2020 $validity = new WP_Error( 'invalid_value', __( 'Invalid value.' ) );
2022 $validities[ $setting_id ] = $validity;
2028 * Prepares setting validity for exporting to the client (JS).
2030 * Converts `WP_Error` instance into array suitable for passing into the
2031 * `wp.customize.Notification` JS model.
2036 * @param true|WP_Error $validity Setting validity.
2037 * @return true|array If `$validity` was a WP_Error, the error codes will be array-mapped
2038 * to their respective `message` and `data` to pass into the
2039 * `wp.customize.Notification` JS model.
2041 public function prepare_setting_validity_for_js( $validity ) {
2042 if ( is_wp_error( $validity ) ) {
2043 $notification = array();
2044 foreach ( $validity->errors as $error_code => $error_messages ) {
2045 $notification[ $error_code ] = array(
2046 'message' => join( ' ', $error_messages ),
2047 'data' => $validity->get_error_data( $error_code ),
2050 return $notification;
2057 * Handle customize_save WP Ajax request to save/update a changeset.
2060 * @since 4.7.0 The semantics of this method have changed to update a changeset, optionally to also change the status and other attributes.
2062 public function save() {
2063 if ( ! is_user_logged_in() ) {
2064 wp_send_json_error( 'unauthenticated' );
2067 if ( ! $this->is_preview() ) {
2068 wp_send_json_error( 'not_preview' );
2071 $action = 'save-customize_' . $this->get_stylesheet();
2072 if ( ! check_ajax_referer( $action, 'nonce', false ) ) {
2073 wp_send_json_error( 'invalid_nonce' );
2076 $changeset_post_id = $this->changeset_post_id();
2077 if ( empty( $changeset_post_id ) ) {
2078 if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->create_posts ) ) {
2079 wp_send_json_error( 'cannot_create_changeset_post' );
2082 if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->edit_post, $changeset_post_id ) ) {
2083 wp_send_json_error( 'cannot_edit_changeset_post' );
2087 if ( ! empty( $_POST['customize_changeset_data'] ) ) {
2088 $input_changeset_data = json_decode( wp_unslash( $_POST['customize_changeset_data'] ), true );
2089 if ( ! is_array( $input_changeset_data ) ) {
2090 wp_send_json_error( 'invalid_customize_changeset_data' );
2093 $input_changeset_data = array();
2097 $changeset_title = null;
2098 if ( isset( $_POST['customize_changeset_title'] ) ) {
2099 $changeset_title = sanitize_text_field( wp_unslash( $_POST['customize_changeset_title'] ) );
2102 // Validate changeset status param.
2104 $changeset_status = null;
2105 if ( isset( $_POST['customize_changeset_status'] ) ) {
2106 $changeset_status = wp_unslash( $_POST['customize_changeset_status'] );
2107 if ( ! get_post_status_object( $changeset_status ) || ! in_array( $changeset_status, array( 'draft', 'pending', 'publish', 'future' ), true ) ) {
2108 wp_send_json_error( 'bad_customize_changeset_status', 400 );
2110 $is_publish = ( 'publish' === $changeset_status || 'future' === $changeset_status );
2111 if ( $is_publish && ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->publish_posts ) ) {
2112 wp_send_json_error( 'changeset_publish_unauthorized', 403 );
2117 * Validate changeset date param. Date is assumed to be in local time for
2118 * the WP if in MySQL format (YYYY-MM-DD HH:MM:SS). Otherwise, the date
2119 * is parsed with strtotime() so that ISO date format may be supplied
2120 * or a string like "+10 minutes".
2122 $changeset_date_gmt = null;
2123 if ( isset( $_POST['customize_changeset_date'] ) ) {
2124 $changeset_date = wp_unslash( $_POST['customize_changeset_date'] );
2125 if ( preg_match( '/^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d$/', $changeset_date ) ) {
2126 $mm = substr( $changeset_date, 5, 2 );
2127 $jj = substr( $changeset_date, 8, 2 );
2128 $aa = substr( $changeset_date, 0, 4 );
2129 $valid_date = wp_checkdate( $mm, $jj, $aa, $changeset_date );
2130 if ( ! $valid_date ) {
2131 wp_send_json_error( 'bad_customize_changeset_date', 400 );
2133 $changeset_date_gmt = get_gmt_from_date( $changeset_date );
2135 $timestamp = strtotime( $changeset_date );
2136 if ( ! $timestamp ) {
2137 wp_send_json_error( 'bad_customize_changeset_date', 400 );
2139 $changeset_date_gmt = gmdate( 'Y-m-d H:i:s', $timestamp );
2143 $r = $this->save_changeset_post( array(
2144 'status' => $changeset_status,
2145 'title' => $changeset_title,
2146 'date_gmt' => $changeset_date_gmt,
2147 'data' => $input_changeset_data,
2149 if ( is_wp_error( $r ) ) {
2151 'message' => $r->get_error_message(),
2152 'code' => $r->get_error_code(),
2154 if ( is_array( $r->get_error_data() ) ) {
2155 $response = array_merge( $response, $r->get_error_data() );
2157 $response['data'] = $r->get_error_data();
2162 // Note that if the changeset status was publish, then it will get set to trash if revisions are not supported.
2163 $response['changeset_status'] = get_post_status( $this->changeset_post_id() );
2164 if ( $is_publish && 'trash' === $response['changeset_status'] ) {
2165 $response['changeset_status'] = 'publish';
2168 if ( 'publish' === $response['changeset_status'] ) {
2169 $response['next_changeset_uuid'] = wp_generate_uuid4();
2173 if ( isset( $response['setting_validities'] ) ) {
2174 $response['setting_validities'] = array_map( array( $this, 'prepare_setting_validity_for_js' ), $response['setting_validities'] );
2178 * Filters response data for a successful customize_save Ajax request.
2180 * This filter does not apply if there was a nonce or authentication failure.
2184 * @param array $response Additional information passed back to the 'saved'
2185 * event on `wp.customize`.
2186 * @param WP_Customize_Manager $this WP_Customize_Manager instance.
2188 $response = apply_filters( 'customize_save_response', $response, $this );
2190 if ( is_wp_error( $r ) ) {
2191 wp_send_json_error( $response );
2193 wp_send_json_success( $response );
2198 * Save the post for the loaded changeset.
2203 * @param array $args {
2204 * Args for changeset post.
2206 * @type array $data Optional additional changeset data. Values will be merged on top of any existing post values.
2207 * @type string $status Post status. Optional. If supplied, the save will be transactional and a post revision will be allowed.
2208 * @type string $title Post title. Optional.
2209 * @type string $date_gmt Date in GMT. Optional.
2210 * @type int $user_id ID for user who is saving the changeset. Optional, defaults to the current user ID.
2211 * @type bool $starter_content Whether the data is starter content. If false (default), then $starter_content will be cleared for any $data being saved.
2214 * @return array|WP_Error Returns array on success and WP_Error with array data on error.
2216 function save_changeset_post( $args = array() ) {
2218 $args = array_merge(
2224 'user_id' => get_current_user_id(),
2225 'starter_content' => false,
2230 $changeset_post_id = $this->changeset_post_id();
2231 $existing_changeset_data = array();
2232 if ( $changeset_post_id ) {
2233 $existing_status = get_post_status( $changeset_post_id );
2234 if ( 'publish' === $existing_status || 'trash' === $existing_status ) {
2235 return new WP_Error( 'changeset_already_published' );
2238 $existing_changeset_data = $this->get_changeset_post_data( $changeset_post_id );
2241 // Fail if attempting to publish but publish hook is missing.
2242 if ( 'publish' === $args['status'] && false === has_action( 'transition_post_status', '_wp_customize_publish_changeset' ) ) {
2243 return new WP_Error( 'missing_publish_callback' );
2247 $now = gmdate( 'Y-m-d H:i:59' );
2248 if ( $args['date_gmt'] ) {
2249 $is_future_dated = ( mysql2date( 'U', $args['date_gmt'], false ) > mysql2date( 'U', $now, false ) );
2250 if ( ! $is_future_dated ) {
2251 return new WP_Error( 'not_future_date' ); // Only future dates are allowed.
2254 if ( ! $this->is_theme_active() && ( 'future' === $args['status'] || $is_future_dated ) ) {
2255 return new WP_Error( 'cannot_schedule_theme_switches' ); // This should be allowed in the future, when theme is a regular setting.
2257 $will_remain_auto_draft = ( ! $args['status'] && ( ! $changeset_post_id || 'auto-draft' === get_post_status( $changeset_post_id ) ) );
2258 if ( $will_remain_auto_draft ) {
2259 return new WP_Error( 'cannot_supply_date_for_auto_draft_changeset' );
2261 } elseif ( $changeset_post_id && 'future' === $args['status'] ) {
2263 // Fail if the new status is future but the existing post's date is not in the future.
2264 $changeset_post = get_post( $changeset_post_id );
2265 if ( mysql2date( 'U', $changeset_post->post_date_gmt, false ) <= mysql2date( 'U', $now, false ) ) {
2266 return new WP_Error( 'not_future_date' );
2270 // The request was made via wp.customize.previewer.save().
2271 $update_transactionally = (bool) $args['status'];
2272 $allow_revision = (bool) $args['status'];
2274 // Amend post values with any supplied data.
2275 foreach ( $args['data'] as $setting_id => $setting_params ) {
2276 if ( array_key_exists( 'value', $setting_params ) ) {
2277 $this->set_post_value( $setting_id, $setting_params['value'] ); // Add to post values so that they can be validated and sanitized.
2281 // Note that in addition to post data, this will include any stashed theme mods.
2282 $post_values = $this->unsanitized_post_values( array(
2283 'exclude_changeset' => true,
2284 'exclude_post_data' => false,
2286 $this->add_dynamic_settings( array_keys( $post_values ) ); // Ensure settings get created even if they lack an input value.
2289 * Get list of IDs for settings that have values different from what is currently
2290 * saved in the changeset. By skipping any values that are already the same, the
2291 * subset of changed settings can be passed into validate_setting_values to prevent
2292 * an underprivileged modifying a single setting for which they have the capability
2293 * from being blocked from saving. This also prevents a user from touching of the
2294 * previous saved settings and overriding the associated user_id if they made no change.
2296 $changed_setting_ids = array();
2297 foreach ( $post_values as $setting_id => $setting_value ) {
2298 $setting = $this->get_setting( $setting_id );
2300 if ( $setting && 'theme_mod' === $setting->type ) {
2301 $prefixed_setting_id = $this->get_stylesheet() . '::' . $setting->id;
2303 $prefixed_setting_id = $setting_id;
2306 $is_value_changed = (
2307 ! isset( $existing_changeset_data[ $prefixed_setting_id ] )
2309 ! array_key_exists( 'value', $existing_changeset_data[ $prefixed_setting_id ] )
2311 $existing_changeset_data[ $prefixed_setting_id ]['value'] !== $setting_value
2313 if ( $is_value_changed ) {
2314 $changed_setting_ids[] = $setting_id;
2319 * Fires before save validation happens.
2321 * Plugins can add just-in-time {@see 'customize_validate_{$this->ID}'} filters
2322 * at this point to catch any settings registered after `customize_register`.
2323 * The dynamic portion of the hook name, `$this->ID` refers to the setting ID.
2327 * @param WP_Customize_Manager $this WP_Customize_Manager instance.
2329 do_action( 'customize_save_validation_before', $this );
2331 // Validate settings.
2332 $validated_values = array_merge(
2333 array_fill_keys( array_keys( $args['data'] ), null ), // Make sure existence/capability checks are done on value-less setting updates.
2336 $setting_validities = $this->validate_setting_values( $validated_values, array(
2337 'validate_capability' => true,
2338 'validate_existence' => true,
2340 $invalid_setting_count = count( array_filter( $setting_validities, 'is_wp_error' ) );
2343 * Short-circuit if there are invalid settings the update is transactional.
2344 * A changeset update is transactional when a status is supplied in the request.
2346 if ( $update_transactionally && $invalid_setting_count > 0 ) {
2348 'setting_validities' => $setting_validities,
2349 'message' => sprintf( _n( 'There is %s invalid setting.', 'There are %s invalid settings.', $invalid_setting_count ), number_format_i18n( $invalid_setting_count ) ),
2351 return new WP_Error( 'transaction_fail', '', $response );
2354 // Obtain/merge data for changeset.
2355 $original_changeset_data = $this->get_changeset_post_data( $changeset_post_id );
2356 $data = $original_changeset_data;
2357 if ( is_wp_error( $data ) ) {
2361 // Ensure that all post values are included in the changeset data.
2362 foreach ( $post_values as $setting_id => $post_value ) {
2363 if ( ! isset( $args['data'][ $setting_id ] ) ) {
2364 $args['data'][ $setting_id ] = array();
2366 if ( ! isset( $args['data'][ $setting_id ]['value'] ) ) {
2367 $args['data'][ $setting_id ]['value'] = $post_value;
2371 foreach ( $args['data'] as $setting_id => $setting_params ) {
2372 $setting = $this->get_setting( $setting_id );
2373 if ( ! $setting || ! $setting->check_capabilities() ) {
2377 // Skip updating changeset for invalid setting values.
2378 if ( isset( $setting_validities[ $setting_id ] ) && is_wp_error( $setting_validities[ $setting_id ] ) ) {
2382 $changeset_setting_id = $setting_id;
2383 if ( 'theme_mod' === $setting->type ) {
2384 $changeset_setting_id = sprintf( '%s::%s', $this->get_stylesheet(), $setting_id );
2387 if ( null === $setting_params ) {
2388 // Remove setting from changeset entirely.
2389 unset( $data[ $changeset_setting_id ] );
2392 if ( ! isset( $data[ $changeset_setting_id ] ) ) {
2393 $data[ $changeset_setting_id ] = array();
2396 // Merge any additional setting params that have been supplied with the existing params.
2397 $merged_setting_params = array_merge( $data[ $changeset_setting_id ], $setting_params );
2399 // Skip updating setting params if unchanged (ensuring the user_id is not overwritten).
2400 if ( $data[ $changeset_setting_id ] === $merged_setting_params ) {
2404 $data[ $changeset_setting_id ] = array_merge(
2405 $merged_setting_params,
2407 'type' => $setting->type,
2408 'user_id' => $args['user_id'],
2412 // Clear starter_content flag in data if changeset is not explicitly being updated for starter content.
2413 if ( empty( $args['starter_content'] ) ) {
2414 unset( $data[ $changeset_setting_id ]['starter_content'] );
2419 $filter_context = array(
2420 'uuid' => $this->changeset_uuid(),
2421 'title' => $args['title'],
2422 'status' => $args['status'],
2423 'date_gmt' => $args['date_gmt'],
2424 'post_id' => $changeset_post_id,
2425 'previous_data' => is_wp_error( $original_changeset_data ) ? array() : $original_changeset_data,
2430 * Filters the settings' data that will be persisted into the changeset.
2432 * Plugins may amend additional data (such as additional meta for settings) into the changeset with this filter.
2436 * @param array $data Updated changeset data, mapping setting IDs to arrays containing a $value item and optionally other metadata.
2437 * @param array $context {
2440 * @type string $uuid Changeset UUID.
2441 * @type string $title Requested title for the changeset post.
2442 * @type string $status Requested status for the changeset post.
2443 * @type string $date_gmt Requested date for the changeset post in MySQL format and GMT timezone.
2444 * @type int|false $post_id Post ID for the changeset, or false if it doesn't exist yet.
2445 * @type array $previous_data Previous data contained in the changeset.
2446 * @type WP_Customize_Manager $manager Manager instance.
2449 $data = apply_filters( 'customize_changeset_save_data', $data, $filter_context );
2451 // Switch theme if publishing changes now.
2452 if ( 'publish' === $args['status'] && ! $this->is_theme_active() ) {
2453 // Temporarily stop previewing the theme to allow switch_themes() to operate properly.
2454 $this->stop_previewing_theme();
2455 switch_theme( $this->get_stylesheet() );
2456 update_option( 'theme_switched_via_customizer', true );
2457 $this->start_previewing_theme();
2460 // Gather the data for wp_insert_post()/wp_update_post().
2462 if ( defined( 'JSON_UNESCAPED_SLASHES' ) ) {
2463 $json_options |= JSON_UNESCAPED_SLASHES; // Introduced in PHP 5.4. This is only to improve readability as slashes needn't be escaped in storage.
2465 $json_options |= JSON_PRETTY_PRINT; // Also introduced in PHP 5.4, but WP defines constant for back compat. See WP Trac #30139.
2466 $post_array = array(
2467 'post_content' => wp_json_encode( $data, $json_options ),
2469 if ( $args['title'] ) {
2470 $post_array['post_title'] = $args['title'];
2472 if ( $changeset_post_id ) {
2473 $post_array['ID'] = $changeset_post_id;
2475 $post_array['post_type'] = 'customize_changeset';
2476 $post_array['post_name'] = $this->changeset_uuid();
2477 $post_array['post_status'] = 'auto-draft';
2479 if ( $args['status'] ) {
2480 $post_array['post_status'] = $args['status'];
2483 // Reset post date to now if we are publishing, otherwise pass post_date_gmt and translate for post_date.
2484 if ( 'publish' === $args['status'] ) {
2485 $post_array['post_date_gmt'] = '0000-00-00 00:00:00';
2486 $post_array['post_date'] = '0000-00-00 00:00:00';
2487 } elseif ( $args['date_gmt'] ) {
2488 $post_array['post_date_gmt'] = $args['date_gmt'];
2489 $post_array['post_date'] = get_date_from_gmt( $args['date_gmt'] );
2492 $this->store_changeset_revision = $allow_revision;
2493 add_filter( 'wp_save_post_revision_post_has_changed', array( $this, '_filter_revision_post_has_changed' ), 5, 3 );
2495 // Update the changeset post. The publish_customize_changeset action will cause the settings in the changeset to be saved via WP_Customize_Setting::save().
2496 $has_kses = ( false !== has_filter( 'content_save_pre', 'wp_filter_post_kses' ) );
2498 kses_remove_filters(); // Prevent KSES from corrupting JSON in post_content.
2501 // Note that updating a post with publish status will trigger WP_Customize_Manager::publish_changeset_values().
2502 if ( $changeset_post_id ) {
2503 $post_array['edit_date'] = true; // Prevent date clearing.
2504 $r = wp_update_post( wp_slash( $post_array ), true );
2506 $r = wp_insert_post( wp_slash( $post_array ), true );
2507 if ( ! is_wp_error( $r ) ) {
2508 $this->_changeset_post_id = $r; // Update cached post ID for the loaded changeset.
2512 kses_init_filters();
2514 $this->_changeset_data = null; // Reset so WP_Customize_Manager::changeset_data() will re-populate with updated contents.
2516 remove_filter( 'wp_save_post_revision_post_has_changed', array( $this, '_filter_revision_post_has_changed' ) );
2519 'setting_validities' => $setting_validities,
2522 if ( is_wp_error( $r ) ) {
2523 $response['changeset_post_save_failure'] = $r->get_error_code();
2524 return new WP_Error( 'changeset_post_save_failure', '', $response );
2531 * Whether a changeset revision should be made.
2537 protected $store_changeset_revision;
2540 * Filters whether a changeset has changed to create a new revision.
2542 * Note that this will not be called while a changeset post remains in auto-draft status.
2547 * @param bool $post_has_changed Whether the post has changed.
2548 * @param WP_Post $last_revision The last revision post object.
2549 * @param WP_Post $post The post object.
2551 * @return bool Whether a revision should be made.
2553 public function _filter_revision_post_has_changed( $post_has_changed, $last_revision, $post ) {
2554 unset( $last_revision );
2555 if ( 'customize_changeset' === $post->post_type ) {
2556 $post_has_changed = $this->store_changeset_revision;
2558 return $post_has_changed;
2562 * Publish changeset values.
2564 * This will the values contained in a changeset, even changesets that do not
2565 * correspond to current manager instance. This is called by
2566 * `_wp_customize_publish_changeset()` when a customize_changeset post is
2567 * transitioned to the `publish` status. As such, this method should not be
2568 * called directly and instead `wp_publish_post()` should be used.
2570 * Please note that if the settings in the changeset are for a non-activated
2571 * theme, the theme must first be switched to (via `switch_theme()`) before
2572 * invoking this method.
2576 * @see _wp_customize_publish_changeset()
2578 * @param int $changeset_post_id ID for customize_changeset post. Defaults to the changeset for the current manager instance.
2579 * @return true|WP_Error True or error info.
2581 public function _publish_changeset_values( $changeset_post_id ) {
2582 $publishing_changeset_data = $this->get_changeset_post_data( $changeset_post_id );
2583 if ( is_wp_error( $publishing_changeset_data ) ) {
2584 return $publishing_changeset_data;
2587 $changeset_post = get_post( $changeset_post_id );
2590 * Temporarily override the changeset context so that it will be read
2591 * in calls to unsanitized_post_values() and so that it will be available
2592 * on the $wp_customize object passed to hooks during the save logic.
2594 $previous_changeset_post_id = $this->_changeset_post_id;
2595 $this->_changeset_post_id = $changeset_post_id;
2596 $previous_changeset_uuid = $this->_changeset_uuid;
2597 $this->_changeset_uuid = $changeset_post->post_name;
2598 $previous_changeset_data = $this->_changeset_data;
2599 $this->_changeset_data = $publishing_changeset_data;
2601 // Parse changeset data to identify theme mod settings and user IDs associated with settings to be saved.
2602 $setting_user_ids = array();
2603 $theme_mod_settings = array();
2604 $namespace_pattern = '/^(?P<stylesheet>.+?)::(?P<setting_id>.+)$/';
2606 foreach ( $this->_changeset_data as $raw_setting_id => $setting_params ) {
2607 $actual_setting_id = null;
2608 $is_theme_mod_setting = (
2609 isset( $setting_params['value'] )
2611 isset( $setting_params['type'] )
2613 'theme_mod' === $setting_params['type']
2615 preg_match( $namespace_pattern, $raw_setting_id, $matches )
2617 if ( $is_theme_mod_setting ) {
2618 if ( ! isset( $theme_mod_settings[ $matches['stylesheet'] ] ) ) {
2619 $theme_mod_settings[ $matches['stylesheet'] ] = array();
2621 $theme_mod_settings[ $matches['stylesheet'] ][ $matches['setting_id'] ] = $setting_params;
2623 if ( $this->get_stylesheet() === $matches['stylesheet'] ) {
2624 $actual_setting_id = $matches['setting_id'];
2627 $actual_setting_id = $raw_setting_id;
2630 // Keep track of the user IDs for settings actually for this theme.
2631 if ( $actual_setting_id && isset( $setting_params['user_id'] ) ) {
2632 $setting_user_ids[ $actual_setting_id ] = $setting_params['user_id'];
2636 $changeset_setting_values = $this->unsanitized_post_values( array(
2637 'exclude_post_data' => true,
2638 'exclude_changeset' => false,
2640 $changeset_setting_ids = array_keys( $changeset_setting_values );
2641 $this->add_dynamic_settings( $changeset_setting_ids );
2644 * Fires once the theme has switched in the Customizer, but before settings
2649 * @param WP_Customize_Manager $manager WP_Customize_Manager instance.
2651 do_action( 'customize_save', $this );
2654 * Ensure that all settings will allow themselves to be saved. Note that
2655 * this is safe because the setting would have checked the capability
2656 * when the setting value was written into the changeset. So this is why
2657 * an additional capability check is not required here.
2659 $original_setting_capabilities = array();
2660 foreach ( $changeset_setting_ids as $setting_id ) {
2661 $setting = $this->get_setting( $setting_id );
2662 if ( $setting && ! isset( $setting_user_ids[ $setting_id ] ) ) {
2663 $original_setting_capabilities[ $setting->id ] = $setting->capability;
2664 $setting->capability = 'exist';
2668 $original_user_id = get_current_user_id();
2669 foreach ( $changeset_setting_ids as $setting_id ) {
2670 $setting = $this->get_setting( $setting_id );
2673 * Set the current user to match the user who saved the value into
2674 * the changeset so that any filters that apply during the save
2675 * process will respect the original user's capabilities. This
2676 * will ensure, for example, that KSES won't strip unsafe HTML
2677 * when a scheduled changeset publishes via WP Cron.
2679 if ( isset( $setting_user_ids[ $setting_id ] ) ) {
2680 wp_set_current_user( $setting_user_ids[ $setting_id ] );
2682 wp_set_current_user( $original_user_id );
2688 wp_set_current_user( $original_user_id );
2690 // Update the stashed theme mod settings, removing the active theme's stashed settings, if activated.
2691 if ( did_action( 'switch_theme' ) ) {
2692 $other_theme_mod_settings = $theme_mod_settings;
2693 unset( $other_theme_mod_settings[ $this->get_stylesheet() ] );
2694 $this->update_stashed_theme_mod_settings( $other_theme_mod_settings );
2698 * Fires after Customize settings have been saved.
2702 * @param WP_Customize_Manager $manager WP_Customize_Manager instance.
2704 do_action( 'customize_save_after', $this );
2706 // Restore original capabilities.
2707 foreach ( $original_setting_capabilities as $setting_id => $capability ) {
2708 $setting = $this->get_setting( $setting_id );
2710 $setting->capability = $capability;
2714 // Restore original changeset data.
2715 $this->_changeset_data = $previous_changeset_data;
2716 $this->_changeset_post_id = $previous_changeset_post_id;
2717 $this->_changeset_uuid = $previous_changeset_uuid;
2723 * Update stashed theme mod settings.
2728 * @param array $inactive_theme_mod_settings Mapping of stylesheet to arrays of theme mod settings.
2729 * @return array|false Returns array of updated stashed theme mods or false if the update failed or there were no changes.
2731 protected function update_stashed_theme_mod_settings( $inactive_theme_mod_settings ) {
2732 $stashed_theme_mod_settings = get_option( 'customize_stashed_theme_mods' );
2733 if ( empty( $stashed_theme_mod_settings ) ) {
2734 $stashed_theme_mod_settings = array();
2737 // Delete any stashed theme mods for the active theme since since they would have been loaded and saved upon activation.
2738 unset( $stashed_theme_mod_settings[ $this->get_stylesheet() ] );
2740 // Merge inactive theme mods with the stashed theme mod settings.
2741 foreach ( $inactive_theme_mod_settings as $stylesheet => $theme_mod_settings ) {
2742 if ( ! isset( $stashed_theme_mod_settings[ $stylesheet ] ) ) {
2743 $stashed_theme_mod_settings[ $stylesheet ] = array();
2746 $stashed_theme_mod_settings[ $stylesheet ] = array_merge(
2747 $stashed_theme_mod_settings[ $stylesheet ],
2753 $result = update_option( 'customize_stashed_theme_mods', $stashed_theme_mod_settings, $autoload );
2757 return $stashed_theme_mod_settings;
2761 * Refresh nonces for the current preview.
2765 public function refresh_nonces() {
2766 if ( ! $this->is_preview() ) {
2767 wp_send_json_error( 'not_preview' );
2770 wp_send_json_success( $this->get_nonces() );
2774 * Add a customize setting.
2777 * @since 4.5.0 Return added WP_Customize_Setting instance.
2780 * @param WP_Customize_Setting|string $id Customize Setting object, or ID.
2781 * @param array $args Setting arguments; passed to WP_Customize_Setting
2783 * @return WP_Customize_Setting The instance of the setting that was added.
2785 public function add_setting( $id, $args = array() ) {
2786 if ( $id instanceof WP_Customize_Setting ) {
2789 $class = 'WP_Customize_Setting';
2791 /** This filter is documented in wp-includes/class-wp-customize-manager.php */
2792 $args = apply_filters( 'customize_dynamic_setting_args', $args, $id );
2794 /** This filter is documented in wp-includes/class-wp-customize-manager.php */
2795 $class = apply_filters( 'customize_dynamic_setting_class', $class, $id, $args );
2797 $setting = new $class( $this, $id, $args );
2800 $this->settings[ $setting->id ] = $setting;
2805 * Register any dynamically-created settings, such as those from $_POST['customized']
2806 * that have no corresponding setting created.
2808 * This is a mechanism to "wake up" settings that have been dynamically created
2809 * on the front end and have been sent to WordPress in `$_POST['customized']`. When WP
2810 * loads, the dynamically-created settings then will get created and previewed
2811 * even though they are not directly created statically with code.
2816 * @param array $setting_ids The setting IDs to add.
2817 * @return array The WP_Customize_Setting objects added.
2819 public function add_dynamic_settings( $setting_ids ) {
2820 $new_settings = array();
2821 foreach ( $setting_ids as $setting_id ) {
2822 // Skip settings already created
2823 if ( $this->get_setting( $setting_id ) ) {
2827 $setting_args = false;
2828 $setting_class = 'WP_Customize_Setting';
2831 * Filters a dynamic setting's constructor args.
2833 * For a dynamic setting to be registered, this filter must be employed
2834 * to override the default false value with an array of args to pass to
2835 * the WP_Customize_Setting constructor.
2839 * @param false|array $setting_args The arguments to the WP_Customize_Setting constructor.
2840 * @param string $setting_id ID for dynamic setting, usually coming from `$_POST['customized']`.
2842 $setting_args = apply_filters( 'customize_dynamic_setting_args', $setting_args, $setting_id );
2843 if ( false === $setting_args ) {
2848 * Allow non-statically created settings to be constructed with custom WP_Customize_Setting subclass.
2852 * @param string $setting_class WP_Customize_Setting or a subclass.
2853 * @param string $setting_id ID for dynamic setting, usually coming from `$_POST['customized']`.
2854 * @param array $setting_args WP_Customize_Setting or a subclass.
2856 $setting_class = apply_filters( 'customize_dynamic_setting_class', $setting_class, $setting_id, $setting_args );
2858 $setting = new $setting_class( $this, $setting_id, $setting_args );
2860 $this->add_setting( $setting );
2861 $new_settings[] = $setting;
2863 return $new_settings;
2867 * Retrieve a customize setting.
2871 * @param string $id Customize Setting ID.
2872 * @return WP_Customize_Setting|void The setting, if set.
2874 public function get_setting( $id ) {
2875 if ( isset( $this->settings[ $id ] ) ) {
2876 return $this->settings[ $id ];
2881 * Remove a customize setting.
2885 * @param string $id Customize Setting ID.
2887 public function remove_setting( $id ) {
2888 unset( $this->settings[ $id ] );
2892 * Add a customize panel.
2895 * @since 4.5.0 Return added WP_Customize_Panel instance.
2898 * @param WP_Customize_Panel|string $id Customize Panel object, or Panel ID.
2899 * @param array $args Optional. Panel arguments. Default empty array.
2901 * @return WP_Customize_Panel The instance of the panel that was added.
2903 public function add_panel( $id, $args = array() ) {
2904 if ( $id instanceof WP_Customize_Panel ) {
2907 $panel = new WP_Customize_Panel( $this, $id, $args );
2910 $this->panels[ $panel->id ] = $panel;
2915 * Retrieve a customize panel.
2920 * @param string $id Panel ID to get.
2921 * @return WP_Customize_Panel|void Requested panel instance, if set.
2923 public function get_panel( $id ) {
2924 if ( isset( $this->panels[ $id ] ) ) {
2925 return $this->panels[ $id ];
2930 * Remove a customize panel.
2935 * @param string $id Panel ID to remove.
2937 public function remove_panel( $id ) {
2938 // Removing core components this way is _doing_it_wrong().
2939 if ( in_array( $id, $this->components, true ) ) {
2940 /* translators: 1: panel id, 2: link to 'customize_loaded_components' filter reference */
2941 $message = sprintf( __( 'Removing %1$s manually will cause PHP warnings. Use the %2$s filter instead.' ),
2943 '<a href="' . esc_url( 'https://developer.wordpress.org/reference/hooks/customize_loaded_components/' ) . '"><code>customize_loaded_components</code></a>'
2946 _doing_it_wrong( __METHOD__, $message, '4.5.0' );
2948 unset( $this->panels[ $id ] );
2952 * Register a customize panel type.
2954 * Registered types are eligible to be rendered via JS and created dynamically.
2959 * @see WP_Customize_Panel
2961 * @param string $panel Name of a custom panel which is a subclass of WP_Customize_Panel.
2963 public function register_panel_type( $panel ) {
2964 $this->registered_panel_types[] = $panel;
2968 * Render JS templates for all registered panel types.
2973 public function render_panel_templates() {
2974 foreach ( $this->registered_panel_types as $panel_type ) {
2975 $panel = new $panel_type( $this, 'temp', array() );
2976 $panel->print_template();
2981 * Add a customize section.
2984 * @since 4.5.0 Return added WP_Customize_Section instance.
2987 * @param WP_Customize_Section|string $id Customize Section object, or Section ID.
2988 * @param array $args Section arguments.
2990 * @return WP_Customize_Section The instance of the section that was added.
2992 public function add_section( $id, $args = array() ) {
2993 if ( $id instanceof WP_Customize_Section ) {
2996 $section = new WP_Customize_Section( $this, $id, $args );
2999 $this->sections[ $section->id ] = $section;
3004 * Retrieve a customize section.
3008 * @param string $id Section ID.
3009 * @return WP_Customize_Section|void The section, if set.
3011 public function get_section( $id ) {
3012 if ( isset( $this->sections[ $id ] ) )
3013 return $this->sections[ $id ];
3017 * Remove a customize section.
3021 * @param string $id Section ID.
3023 public function remove_section( $id ) {
3024 unset( $this->sections[ $id ] );
3028 * Register a customize section type.
3030 * Registered types are eligible to be rendered via JS and created dynamically.
3035 * @see WP_Customize_Section
3037 * @param string $section Name of a custom section which is a subclass of WP_Customize_Section.
3039 public function register_section_type( $section ) {
3040 $this->registered_section_types[] = $section;
3044 * Render JS templates for all registered section types.
3049 public function render_section_templates() {
3050 foreach ( $this->registered_section_types as $section_type ) {
3051 $section = new $section_type( $this, 'temp', array() );
3052 $section->print_template();
3057 * Add a customize control.
3060 * @since 4.5.0 Return added WP_Customize_Control instance.
3063 * @param WP_Customize_Control|string $id Customize Control object, or ID.
3064 * @param array $args Control arguments; passed to WP_Customize_Control
3066 * @return WP_Customize_Control The instance of the control that was added.
3068 public function add_control( $id, $args = array() ) {
3069 if ( $id instanceof WP_Customize_Control ) {
3072 $control = new WP_Customize_Control( $this, $id, $args );
3075 $this->controls[ $control->id ] = $control;
3080 * Retrieve a customize control.
3084 * @param string $id ID of the control.
3085 * @return WP_Customize_Control|void The control object, if set.
3087 public function get_control( $id ) {
3088 if ( isset( $this->controls[ $id ] ) )
3089 return $this->controls[ $id ];
3093 * Remove a customize control.
3097 * @param string $id ID of the control.
3099 public function remove_control( $id ) {
3100 unset( $this->controls[ $id ] );
3104 * Register a customize control type.
3106 * Registered types are eligible to be rendered via JS and created dynamically.
3111 * @param string $control Name of a custom control which is a subclass of
3112 * WP_Customize_Control.
3114 public function register_control_type( $control ) {
3115 $this->registered_control_types[] = $control;
3119 * Render JS templates for all registered control types.
3124 public function render_control_templates() {
3125 foreach ( $this->registered_control_types as $control_type ) {
3126 $control = new $control_type( $this, 'temp', array(
3127 'settings' => array(),
3129 $control->print_template();
3132 <script type="text/html" id="tmpl-customize-control-notifications">
3134 <# _.each( data.notifications, function( notification ) { #>
3135 <li class="notice notice-{{ notification.type || 'info' }} {{ data.altNotice ? 'notice-alt' : '' }}" data-code="{{ notification.code }}" data-type="{{ notification.type }}">{{{ notification.message || notification.code }}}</li>
3143 * Helper function to compare two objects by priority, ensuring sort stability via instance_number.
3146 * @deprecated 4.7.0 Use wp_list_sort()
3148 * @param WP_Customize_Panel|WP_Customize_Section|WP_Customize_Control $a Object A.
3149 * @param WP_Customize_Panel|WP_Customize_Section|WP_Customize_Control $b Object B.
3152 protected function _cmp_priority( $a, $b ) {
3153 _deprecated_function( __METHOD__, '4.7.0', 'wp_list_sort' );
3155 if ( $a->priority === $b->priority ) {
3156 return $a->instance_number - $b->instance_number;
3158 return $a->priority - $b->priority;
3163 * Prepare panels, sections, and controls.
3165 * For each, check if required related components exist,
3166 * whether the user has the necessary capabilities,
3167 * and sort by priority.
3171 public function prepare_controls() {
3173 $controls = array();
3174 $this->controls = wp_list_sort( $this->controls, array(
3175 'priority' => 'ASC',
3176 'instance_number' => 'ASC',
3179 foreach ( $this->controls as $id => $control ) {
3180 if ( ! isset( $this->sections[ $control->section ] ) || ! $control->check_capabilities() ) {
3184 $this->sections[ $control->section ]->controls[] = $control;
3185 $controls[ $id ] = $control;
3187 $this->controls = $controls;
3189 // Prepare sections.
3190 $this->sections = wp_list_sort( $this->sections, array(
3191 'priority' => 'ASC',
3192 'instance_number' => 'ASC',
3194 $sections = array();
3196 foreach ( $this->sections as $section ) {
3197 if ( ! $section->check_capabilities() ) {
3202 $section->controls = wp_list_sort( $section->controls, array(
3203 'priority' => 'ASC',
3204 'instance_number' => 'ASC',
3207 if ( ! $section->panel ) {
3208 // Top-level section.
3209 $sections[ $section->id ] = $section;
3211 // This section belongs to a panel.
3212 if ( isset( $this->panels [ $section->panel ] ) ) {
3213 $this->panels[ $section->panel ]->sections[ $section->id ] = $section;
3217 $this->sections = $sections;
3220 $this->panels = wp_list_sort( $this->panels, array(
3221 'priority' => 'ASC',
3222 'instance_number' => 'ASC',
3226 foreach ( $this->panels as $panel ) {
3227 if ( ! $panel->check_capabilities() ) {
3231 $panel->sections = wp_list_sort( $panel->sections, array(
3232 'priority' => 'ASC',
3233 'instance_number' => 'ASC',
3235 $panels[ $panel->id ] = $panel;
3237 $this->panels = $panels;
3239 // Sort panels and top-level sections together.
3240 $this->containers = array_merge( $this->panels, $this->sections );
3241 $this->containers = wp_list_sort( $this->containers, array(
3242 'priority' => 'ASC',
3243 'instance_number' => 'ASC',
3248 * Enqueue scripts for customize controls.
3252 public function enqueue_control_scripts() {
3253 foreach ( $this->controls as $control ) {
3254 $control->enqueue();
3259 * Determine whether the user agent is iOS.
3264 * @return bool Whether the user agent is iOS.
3266 public function is_ios() {
3267 return wp_is_mobile() && preg_match( '/iPad|iPod|iPhone/', $_SERVER['HTTP_USER_AGENT'] );
3271 * Get the template string for the Customizer pane document title.
3276 * @return string The template string for the document title.
3278 public function get_document_title_template() {
3279 if ( $this->is_theme_active() ) {
3280 /* translators: %s: document title from the preview */
3281 $document_title_tmpl = __( 'Customize: %s' );
3283 /* translators: %s: document title from the preview */
3284 $document_title_tmpl = __( 'Live Preview: %s' );
3286 $document_title_tmpl = html_entity_decode( $document_title_tmpl, ENT_QUOTES, 'UTF-8' ); // Because exported to JS and assigned to document.title.
3287 return $document_title_tmpl;
3291 * Set the initial URL to be previewed.
3298 * @param string $preview_url URL to be previewed.
3300 public function set_preview_url( $preview_url ) {
3301 $preview_url = esc_url_raw( $preview_url );
3302 $this->preview_url = wp_validate_redirect( $preview_url, home_url( '/' ) );
3306 * Get the initial URL to be previewed.
3311 * @return string URL being previewed.
3313 public function get_preview_url() {
3314 if ( empty( $this->preview_url ) ) {
3315 $preview_url = home_url( '/' );
3317 $preview_url = $this->preview_url;
3319 return $preview_url;
3323 * Determines whether the admin and the frontend are on different domains.
3328 * @return bool Whether cross-domain.
3330 public function is_cross_domain() {
3331 $admin_origin = wp_parse_url( admin_url() );
3332 $home_origin = wp_parse_url( home_url() );
3333 $cross_domain = ( strtolower( $admin_origin['host'] ) !== strtolower( $home_origin['host'] ) );
3334 return $cross_domain;
3338 * Get URLs allowed to be previewed.
3340 * If the front end and the admin are served from the same domain, load the
3341 * preview over ssl if the Customizer is being loaded over ssl. This avoids
3342 * insecure content warnings. This is not attempted if the admin and front end
3343 * are on different domains to avoid the case where the front end doesn't have
3344 * ssl certs. Domain mapping plugins can allow other urls in these conditions
3345 * using the customize_allowed_urls filter.
3350 * @returns array Allowed URLs.
3352 public function get_allowed_urls() {
3353 $allowed_urls = array( home_url( '/' ) );
3355 if ( is_ssl() && ! $this->is_cross_domain() ) {
3356 $allowed_urls[] = home_url( '/', 'https' );
3360 * Filters the list of URLs allowed to be clicked and followed in the Customizer preview.
3364 * @param array $allowed_urls An array of allowed URLs.
3366 $allowed_urls = array_unique( apply_filters( 'customize_allowed_urls', $allowed_urls ) );
3368 return $allowed_urls;
3372 * Get messenger channel.
3377 * @return string Messenger channel.
3379 public function get_messenger_channel() {
3380 return $this->messenger_channel;
3384 * Set URL to link the user to when closing the Customizer.
3391 * @param string $return_url URL for return link.
3393 public function set_return_url( $return_url ) {
3394 $return_url = esc_url_raw( $return_url );
3395 $return_url = remove_query_arg( wp_removable_query_args(), $return_url );
3396 $return_url = wp_validate_redirect( $return_url );
3397 $this->return_url = $return_url;
3401 * Get URL to link the user to when closing the Customizer.
3406 * @return string URL for link to close Customizer.
3408 public function get_return_url() {
3409 $referer = wp_get_referer();
3410 $excluded_referer_basenames = array( 'customize.php', 'wp-login.php' );
3412 if ( $this->return_url ) {
3413 $return_url = $this->return_url;
3414 } else if ( $referer && ! in_array( basename( parse_url( $referer, PHP_URL_PATH ) ), $excluded_referer_basenames, true ) ) {
3415 $return_url = $referer;
3416 } else if ( $this->preview_url ) {
3417 $return_url = $this->preview_url;
3419 $return_url = home_url( '/' );
3425 * Set the autofocused constructs.
3430 * @param array $autofocus {
3431 * Mapping of 'panel', 'section', 'control' to the ID which should be autofocused.
3433 * @type string [$control] ID for control to be autofocused.
3434 * @type string [$section] ID for section to be autofocused.
3435 * @type string [$panel] ID for panel to be autofocused.
3438 public function set_autofocus( $autofocus ) {
3439 $this->autofocus = array_filter( wp_array_slice_assoc( $autofocus, array( 'panel', 'section', 'control' ) ), 'is_string' );
3443 * Get the autofocused constructs.
3449 * Mapping of 'panel', 'section', 'control' to the ID which should be autofocused.
3451 * @type string [$control] ID for control to be autofocused.
3452 * @type string [$section] ID for section to be autofocused.
3453 * @type string [$panel] ID for panel to be autofocused.
3456 public function get_autofocus() {
3457 return $this->autofocus;
3461 * Get nonces for the Customizer.
3464 * @return array Nonces.
3466 public function get_nonces() {
3468 'save' => wp_create_nonce( 'save-customize_' . $this->get_stylesheet() ),
3469 'preview' => wp_create_nonce( 'preview-customize_' . $this->get_stylesheet() ),
3473 * Filters nonces for Customizer.
3477 * @param array $nonces Array of refreshed nonces for save and
3479 * @param WP_Customize_Manager $this WP_Customize_Manager instance.
3481 $nonces = apply_filters( 'customize_refresh_nonces', $nonces, $this );
3487 * Print JavaScript settings for parent window.
3491 public function customize_pane_settings() {
3493 $login_url = add_query_arg( array(
3494 'interim-login' => 1,
3495 'customize-login' => 1,
3496 ), wp_login_url() );
3498 // Ensure dirty flags are set for modified settings.
3499 foreach ( array_keys( $this->unsanitized_post_values() ) as $setting_id ) {
3500 $setting = $this->get_setting( $setting_id );
3502 $setting->dirty = true;
3506 // Prepare Customizer settings to pass to JavaScript.
3508 'changeset' => array(
3509 'uuid' => $this->changeset_uuid(),
3510 'status' => $this->changeset_post_id() ? get_post_status( $this->changeset_post_id() ) : '',
3512 'timeouts' => array(
3513 'windowRefresh' => 250,
3514 'changesetAutoSave' => AUTOSAVE_INTERVAL * 1000,
3515 'keepAliveCheck' => 2500,
3516 'reflowPaneContents' => 100,
3517 'previewFrameSensitivity' => 2000,
3520 'stylesheet' => $this->get_stylesheet(),
3521 'active' => $this->is_theme_active(),
3524 'preview' => esc_url_raw( $this->get_preview_url() ),
3525 'parent' => esc_url_raw( admin_url() ),
3526 'activated' => esc_url_raw( home_url( '/' ) ),
3527 'ajax' => esc_url_raw( admin_url( 'admin-ajax.php', 'relative' ) ),
3528 'allowed' => array_map( 'esc_url_raw', $this->get_allowed_urls() ),
3529 'isCrossDomain' => $this->is_cross_domain(),
3530 'home' => esc_url_raw( home_url( '/' ) ),
3531 'login' => esc_url_raw( $login_url ),
3534 'mobile' => wp_is_mobile(),
3535 'ios' => $this->is_ios(),
3537 'panels' => array(),
3538 'sections' => array(),
3539 'nonce' => $this->get_nonces(),
3540 'autofocus' => $this->get_autofocus(),
3541 'documentTitleTmpl' => $this->get_document_title_template(),
3542 'previewableDevices' => $this->get_previewable_devices(),
3545 // Prepare Customize Section objects to pass to JavaScript.
3546 foreach ( $this->sections() as $id => $section ) {
3547 if ( $section->check_capabilities() ) {
3548 $settings['sections'][ $id ] = $section->json();
3552 // Prepare Customize Panel objects to pass to JavaScript.
3553 foreach ( $this->panels() as $panel_id => $panel ) {
3554 if ( $panel->check_capabilities() ) {
3555 $settings['panels'][ $panel_id ] = $panel->json();
3556 foreach ( $panel->sections as $section_id => $section ) {
3557 if ( $section->check_capabilities() ) {
3558 $settings['sections'][ $section_id ] = $section->json();
3565 <script type="text/javascript">
3566 var _wpCustomizeSettings = <?php echo wp_json_encode( $settings ); ?>;
3567 _wpCustomizeSettings.controls = {};
3568 _wpCustomizeSettings.settings = {};
3571 // Serialize settings one by one to improve memory usage.
3572 echo "(function ( s ){\n";
3573 foreach ( $this->settings() as $setting ) {
3574 if ( $setting->check_capabilities() ) {
3577 wp_json_encode( $setting->id ),
3578 wp_json_encode( $setting->json() )
3582 echo "})( _wpCustomizeSettings.settings );\n";
3584 // Serialize controls one by one to improve memory usage.
3585 echo "(function ( c ){\n";
3586 foreach ( $this->controls() as $control ) {
3587 if ( $control->check_capabilities() ) {
3590 wp_json_encode( $control->id ),
3591 wp_json_encode( $control->json() )
3595 echo "})( _wpCustomizeSettings.controls );\n";
3602 * Returns a list of devices to allow previewing.
3607 * @return array List of devices with labels and default setting.
3609 public function get_previewable_devices() {
3612 'label' => __( 'Enter desktop preview mode' ),
3616 'label' => __( 'Enter tablet preview mode' ),
3619 'label' => __( 'Enter mobile preview mode' ),
3624 * Filters the available devices to allow previewing in the Customizer.
3628 * @see WP_Customize_Manager::get_previewable_devices()
3630 * @param array $devices List of devices with labels and default setting.
3632 $devices = apply_filters( 'customize_previewable_devices', $devices );
3638 * Register some default controls.
3642 public function register_controls() {
3644 /* Panel, Section, and Control Types */
3645 $this->register_panel_type( 'WP_Customize_Panel' );
3646 $this->register_section_type( 'WP_Customize_Section' );
3647 $this->register_section_type( 'WP_Customize_Sidebar_Section' );
3648 $this->register_control_type( 'WP_Customize_Color_Control' );
3649 $this->register_control_type( 'WP_Customize_Media_Control' );
3650 $this->register_control_type( 'WP_Customize_Upload_Control' );
3651 $this->register_control_type( 'WP_Customize_Image_Control' );
3652 $this->register_control_type( 'WP_Customize_Background_Image_Control' );
3653 $this->register_control_type( 'WP_Customize_Background_Position_Control' );
3654 $this->register_control_type( 'WP_Customize_Cropped_Image_Control' );
3655 $this->register_control_type( 'WP_Customize_Site_Icon_Control' );
3656 $this->register_control_type( 'WP_Customize_Theme_Control' );
3660 $this->add_section( new WP_Customize_Themes_Section( $this, 'themes', array(
3661 'title' => $this->theme()->display( 'Name' ),
3662 'capability' => 'switch_themes',
3666 // Themes Setting (unused - the theme is considerably more fundamental to the Customizer experience).
3667 $this->add_setting( new WP_Customize_Filter_Setting( $this, 'active_theme', array(
3668 'capability' => 'switch_themes',
3671 require_once( ABSPATH . 'wp-admin/includes/theme.php' );
3675 // Add a control for the active/original theme.
3676 if ( ! $this->is_theme_active() ) {
3677 $themes = wp_prepare_themes_for_js( array( wp_get_theme( $this->original_stylesheet ) ) );
3678 $active_theme = current( $themes );
3679 $active_theme['isActiveTheme'] = true;
3680 $this->add_control( new WP_Customize_Theme_Control( $this, $active_theme['id'], array(
3681 'theme' => $active_theme,
3682 'section' => 'themes',
3683 'settings' => 'active_theme',
3687 $themes = wp_prepare_themes_for_js();
3688 foreach ( $themes as $theme ) {
3689 if ( $theme['active'] || $theme['id'] === $this->original_stylesheet ) {
3693 $theme_id = 'theme_' . $theme['id'];
3694 $theme['isActiveTheme'] = false;
3695 $this->add_control( new WP_Customize_Theme_Control( $this, $theme_id, array(
3697 'section' => 'themes',
3698 'settings' => 'active_theme',
3704 $this->add_section( 'title_tagline', array(
3705 'title' => __( 'Site Identity' ),
3709 $this->add_setting( 'blogname', array(
3710 'default' => get_option( 'blogname' ),
3712 'capability' => 'manage_options',
3715 $this->add_control( 'blogname', array(
3716 'label' => __( 'Site Title' ),
3717 'section' => 'title_tagline',
3720 $this->add_setting( 'blogdescription', array(
3721 'default' => get_option( 'blogdescription' ),
3723 'capability' => 'manage_options',
3726 $this->add_control( 'blogdescription', array(
3727 'label' => __( 'Tagline' ),
3728 'section' => 'title_tagline',
3731 // Add a setting to hide header text if the theme doesn't support custom headers.
3732 if ( ! current_theme_supports( 'custom-header', 'header-text' ) ) {
3733 $this->add_setting( 'header_text', array(
3734 'theme_supports' => array( 'custom-logo', 'header-text' ),
3736 'sanitize_callback' => 'absint',
3739 $this->add_control( 'header_text', array(
3740 'label' => __( 'Display Site Title and Tagline' ),
3741 'section' => 'title_tagline',
3742 'settings' => 'header_text',
3743 'type' => 'checkbox',
3747 $this->add_setting( 'site_icon', array(
3749 'capability' => 'manage_options',
3750 'transport' => 'postMessage', // Previewed with JS in the Customizer controls window.
3753 $this->add_control( new WP_Customize_Site_Icon_Control( $this, 'site_icon', array(
3754 'label' => __( 'Site Icon' ),
3755 'description' => sprintf(
3756 /* translators: %s: site icon size in pixels */
3757 __( 'The Site Icon is used as a browser and app icon for your site. Icons must be square, and at least %s pixels wide and tall.' ),
3758 '<strong>512</strong>'
3760 'section' => 'title_tagline',
3766 $this->add_setting( 'custom_logo', array(
3767 'theme_supports' => array( 'custom-logo' ),
3768 'transport' => 'postMessage',
3771 $custom_logo_args = get_theme_support( 'custom-logo' );
3772 $this->add_control( new WP_Customize_Cropped_Image_Control( $this, 'custom_logo', array(
3773 'label' => __( 'Logo' ),
3774 'section' => 'title_tagline',
3776 'height' => $custom_logo_args[0]['height'],
3777 'width' => $custom_logo_args[0]['width'],
3778 'flex_height' => $custom_logo_args[0]['flex-height'],
3779 'flex_width' => $custom_logo_args[0]['flex-width'],
3780 'button_labels' => array(
3781 'select' => __( 'Select logo' ),
3782 'change' => __( 'Change logo' ),
3783 'remove' => __( 'Remove' ),
3784 'default' => __( 'Default' ),
3785 'placeholder' => __( 'No logo selected' ),
3786 'frame_title' => __( 'Select logo' ),
3787 'frame_button' => __( 'Choose logo' ),
3791 $this->selective_refresh->add_partial( 'custom_logo', array(
3792 'settings' => array( 'custom_logo' ),
3793 'selector' => '.custom-logo-link',
3794 'render_callback' => array( $this, '_render_custom_logo_partial' ),
3795 'container_inclusive' => true,
3800 $this->add_section( 'colors', array(
3801 'title' => __( 'Colors' ),
3805 $this->add_setting( 'header_textcolor', array(
3806 'theme_supports' => array( 'custom-header', 'header-text' ),
3807 'default' => get_theme_support( 'custom-header', 'default-text-color' ),
3809 'sanitize_callback' => array( $this, '_sanitize_header_textcolor' ),
3810 'sanitize_js_callback' => 'maybe_hash_hex_color',
3813 // Input type: checkbox
3814 // With custom value
3815 $this->add_control( 'display_header_text', array(
3816 'settings' => 'header_textcolor',
3817 'label' => __( 'Display Site Title and Tagline' ),
3818 'section' => 'title_tagline',
3819 'type' => 'checkbox',
3823 $this->add_control( new WP_Customize_Color_Control( $this, 'header_textcolor', array(
3824 'label' => __( 'Header Text Color' ),
3825 'section' => 'colors',
3828 // Input type: Color
3829 // With sanitize_callback
3830 $this->add_setting( 'background_color', array(
3831 'default' => get_theme_support( 'custom-background', 'default-color' ),
3832 'theme_supports' => 'custom-background',
3834 'sanitize_callback' => 'sanitize_hex_color_no_hash',
3835 'sanitize_js_callback' => 'maybe_hash_hex_color',
3838 $this->add_control( new WP_Customize_Color_Control( $this, 'background_color', array(
3839 'label' => __( 'Background Color' ),
3840 'section' => 'colors',
3845 if ( current_theme_supports( 'custom-header', 'video' ) ) {
3846 $title = __( 'Header Media' );
3847 $description = '<p>' . __( 'If you add a video, the image will be used as a fallback while the video loads.' ) . '</p>';
3849 // @todo Customizer sections should support having notifications just like controls do. See <https://core.trac.wordpress.org/ticket/38794>.
3850 $description .= '<div class="customize-control-notifications-container header-video-not-currently-previewable" style="display: none"><ul>';
3851 $description .= '<li class="notice notice-info">' . __( 'This theme doesn\'t support video headers on this page. Navigate to the front page or another page that supports video headers.' ) . '</li>';
3852 $description .= '</ul></div>';
3853 $width = absint( get_theme_support( 'custom-header', 'width' ) );
3854 $height = absint( get_theme_support( 'custom-header', 'height' ) );
3855 if ( $width && $height ) {
3856 $control_description = sprintf(
3857 /* translators: 1: .mp4, 2: header size in pixels */
3858 __( 'Upload your video in %1$s format and minimize its file size for best results. Your theme recommends dimensions of %2$s pixels.' ),
3859 '<code>.mp4</code>',
3860 sprintf( '<strong>%s × %s</strong>', $width, $height )
3862 } elseif ( $width ) {
3863 $control_description = sprintf(
3864 /* translators: 1: .mp4, 2: header width in pixels */
3865 __( 'Upload your video in %1$s format and minimize its file size for best results. Your theme recommends a width of %2$s pixels.' ),
3866 '<code>.mp4</code>',
3867 sprintf( '<strong>%s</strong>', $width )
3870 $control_description = sprintf(
3871 /* translators: 1: .mp4, 2: header height in pixels */
3872 __( 'Upload your video in %1$s format and minimize its file size for best results. Your theme recommends a height of %2$s pixels.' ),
3873 '<code>.mp4</code>',
3874 sprintf( '<strong>%s</strong>', $height )
3878 $title = __( 'Header Image' );
3880 $control_description = '';
3883 $this->add_section( 'header_image', array(
3885 'description' => $description,
3886 'theme_supports' => 'custom-header',
3890 $this->add_setting( 'header_video', array(
3891 'theme_supports' => array( 'custom-header', 'video' ),
3892 'transport' => 'postMessage',
3893 'sanitize_callback' => 'absint',
3894 'validate_callback' => array( $this, '_validate_header_video' ),
3897 $this->add_setting( 'external_header_video', array(
3898 'theme_supports' => array( 'custom-header', 'video' ),
3899 'transport' => 'postMessage',
3900 'sanitize_callback' => array( $this, '_sanitize_external_header_video' ),
3901 'validate_callback' => array( $this, '_validate_external_header_video' ),
3904 $this->add_setting( new WP_Customize_Filter_Setting( $this, 'header_image', array(
3905 'default' => sprintf( get_theme_support( 'custom-header', 'default-image' ), get_template_directory_uri(), get_stylesheet_directory_uri() ),
3906 'theme_supports' => 'custom-header',
3909 $this->add_setting( new WP_Customize_Header_Image_Setting( $this, 'header_image_data', array(
3910 'theme_supports' => 'custom-header',
3914 * Switch image settings to postMessage when video support is enabled since
3915 * it entails that the_custom_header_markup() will be used, and thus selective
3916 * refresh can be utilized.
3918 if ( current_theme_supports( 'custom-header', 'video' ) ) {
3919 $this->get_setting( 'header_image' )->transport = 'postMessage';
3920 $this->get_setting( 'header_image_data' )->transport = 'postMessage';
3923 $this->add_control( new WP_Customize_Media_Control( $this, 'header_video', array(
3924 'theme_supports' => array( 'custom-header', 'video' ),
3925 'label' => __( 'Header Video' ),
3926 'description' => $control_description,
3927 'section' => 'header_image',
3928 'mime_type' => 'video',
3929 // @todo These button_labels can be removed once WP_Customize_Media_Control provides mime_type-specific labels automatically. See <https://core.trac.wordpress.org/ticket/38796>.
3930 'button_labels' => array(
3931 'select' => __( 'Select Video' ),
3932 'change' => __( 'Change Video' ),
3933 'placeholder' => __( 'No video selected' ),
3934 'frame_title' => __( 'Select Video' ),
3935 'frame_button' => __( 'Choose Video' ),
3937 'active_callback' => 'is_header_video_active',
3940 $this->add_control( 'external_header_video', array(
3941 'theme_supports' => array( 'custom-header', 'video' ),
3943 'description' => __( 'Or, enter a YouTube URL:' ),
3944 'section' => 'header_image',
3945 'active_callback'=> 'is_front_page',
3948 $this->add_control( new WP_Customize_Header_Image_Control( $this ) );
3950 $this->selective_refresh->add_partial( 'custom_header', array(
3951 'selector' => '#wp-custom-header',
3952 'render_callback' => 'the_custom_header_markup',
3953 'settings' => array( 'header_video', 'external_header_video', 'header_image' ), // The image is used as a video fallback here.
3954 'container_inclusive' => true,
3957 /* Custom Background */
3959 $this->add_section( 'background_image', array(
3960 'title' => __( 'Background Image' ),
3961 'theme_supports' => 'custom-background',
3965 $this->add_setting( 'background_image', array(
3966 'default' => get_theme_support( 'custom-background', 'default-image' ),
3967 'theme_supports' => 'custom-background',
3968 'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
3971 $this->add_setting( new WP_Customize_Background_Image_Setting( $this, 'background_image_thumb', array(
3972 'theme_supports' => 'custom-background',
3973 'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
3976 $this->add_control( new WP_Customize_Background_Image_Control( $this ) );
3978 $this->add_setting( 'background_preset', array(
3979 'default' => get_theme_support( 'custom-background', 'default-preset' ),
3980 'theme_supports' => 'custom-background',
3981 'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
3984 $this->add_control( 'background_preset', array(
3985 'label' => _x( 'Preset', 'Background Preset' ),
3986 'section' => 'background_image',
3989 'default' => _x( 'Default', 'Default Preset' ),
3990 'fill' => __( 'Fill Screen' ),
3991 'fit' => __( 'Fit to Screen' ),
3992 'repeat' => _x( 'Repeat', 'Repeat Image' ),
3993 'custom' => _x( 'Custom', 'Custom Preset' ),
3997 $this->add_setting( 'background_position_x', array(
3998 'default' => get_theme_support( 'custom-background', 'default-position-x' ),
3999 'theme_supports' => 'custom-background',
4000 'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
4003 $this->add_setting( 'background_position_y', array(
4004 'default' => get_theme_support( 'custom-background', 'default-position-y' ),
4005 'theme_supports' => 'custom-background',
4006 'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
4009 $this->add_control( new WP_Customize_Background_Position_Control( $this, 'background_position', array(
4010 'label' => __( 'Image Position' ),
4011 'section' => 'background_image',
4012 'settings' => array(
4013 'x' => 'background_position_x',
4014 'y' => 'background_position_y',
4018 $this->add_setting( 'background_size', array(
4019 'default' => get_theme_support( 'custom-background', 'default-size' ),
4020 'theme_supports' => 'custom-background',
4021 'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
4024 $this->add_control( 'background_size', array(
4025 'label' => __( 'Image Size' ),
4026 'section' => 'background_image',
4029 'auto' => __( 'Original' ),
4030 'contain' => __( 'Fit to Screen' ),
4031 'cover' => __( 'Fill Screen' ),
4035 $this->add_setting( 'background_repeat', array(
4036 'default' => get_theme_support( 'custom-background', 'default-repeat' ),
4037 'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
4038 'theme_supports' => 'custom-background',
4041 $this->add_control( 'background_repeat', array(
4042 'label' => __( 'Repeat Background Image' ),
4043 'section' => 'background_image',
4044 'type' => 'checkbox',
4047 $this->add_setting( 'background_attachment', array(
4048 'default' => get_theme_support( 'custom-background', 'default-attachment' ),
4049 'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
4050 'theme_supports' => 'custom-background',
4053 $this->add_control( 'background_attachment', array(
4054 'label' => __( 'Scroll with Page' ),
4055 'section' => 'background_image',
4056 'type' => 'checkbox',
4060 // If the theme is using the default background callback, we can update
4061 // the background CSS using postMessage.
4062 if ( get_theme_support( 'custom-background', 'wp-head-callback' ) === '_custom_background_cb' ) {
4063 foreach ( array( 'color', 'image', 'preset', 'position_x', 'position_y', 'size', 'repeat', 'attachment' ) as $prop ) {
4064 $this->get_setting( 'background_' . $prop )->transport = 'postMessage';
4070 * See also https://core.trac.wordpress.org/ticket/19627 which introduces the the static-front-page theme_support.
4071 * The following replicates behavior from options-reading.php.
4074 $this->add_section( 'static_front_page', array(
4075 'title' => __( 'Static Front Page' ),
4077 'description' => __( 'Your theme supports a static front page.' ),
4078 'active_callback' => array( $this, 'has_published_pages' ),
4081 $this->add_setting( 'show_on_front', array(
4082 'default' => get_option( 'show_on_front' ),
4083 'capability' => 'manage_options',
4087 $this->add_control( 'show_on_front', array(
4088 'label' => __( 'Front page displays' ),
4089 'section' => 'static_front_page',
4092 'posts' => __( 'Your latest posts' ),
4093 'page' => __( 'A static page' ),
4097 $this->add_setting( 'page_on_front', array(
4099 'capability' => 'manage_options',
4102 $this->add_control( 'page_on_front', array(
4103 'label' => __( 'Front page' ),
4104 'section' => 'static_front_page',
4105 'type' => 'dropdown-pages',
4106 'allow_addition' => true,
4109 $this->add_setting( 'page_for_posts', array(
4111 'capability' => 'manage_options',
4114 $this->add_control( 'page_for_posts', array(
4115 'label' => __( 'Posts page' ),
4116 'section' => 'static_front_page',
4117 'type' => 'dropdown-pages',
4118 'allow_addition' => true,
4122 $this->add_section( 'custom_css', array(
4123 'title' => __( 'Additional CSS' ),
4125 'description_hidden' => true,
4126 'description' => sprintf( '%s<br /><a href="%s" class="external-link" target="_blank">%s<span class="screen-reader-text">%s</span></a>',
4127 __( 'CSS allows you to customize the appearance and layout of your site with code. Separate CSS is saved for each of your themes. In the editing area the Tab key enters a tab character. To move below this area by pressing Tab, press the Esc key followed by the Tab key.' ),
4128 esc_url( __( 'https://codex.wordpress.org/CSS' ) ),
4129 __( 'Learn more about CSS' ),
4130 __( '(link opens in a new window)' )
4134 $custom_css_setting = new WP_Customize_Custom_CSS_Setting( $this, sprintf( 'custom_css[%s]', get_stylesheet() ), array(
4135 'capability' => 'edit_css',
4136 'default' => sprintf( "/*\n%s\n*/", __( "You can add your own CSS here.\n\nClick the help icon above to learn more." ) ),
4138 $this->add_setting( $custom_css_setting );
4140 $this->add_control( 'custom_css', array(
4141 'type' => 'textarea',
4142 'section' => 'custom_css',
4143 'settings' => array( 'default' => $custom_css_setting->id ),
4144 'input_attrs' => array(
4145 'class' => 'code', // Ensures contents displayed as LTR instead of RTL.
4151 * Return whether there are published pages.
4153 * Used as active callback for static front page section and controls.
4158 * @returns bool Whether there are published (or to be published) pages.
4160 public function has_published_pages() {
4162 $setting = $this->get_setting( 'nav_menus_created_posts' );
4164 foreach ( $setting->value() as $post_id ) {
4165 if ( 'page' === get_post_type( $post_id ) ) {
4170 return 0 !== count( get_pages() );
4174 * Add settings from the POST data that were not added with code, e.g. dynamically-created settings for Widgets
4179 * @see add_dynamic_settings()
4181 public function register_dynamic_settings() {
4182 $setting_ids = array_keys( $this->unsanitized_post_values() );
4183 $this->add_dynamic_settings( $setting_ids );
4187 * Callback for validating the header_textcolor value.
4189 * Accepts 'blank', and otherwise uses sanitize_hex_color_no_hash().
4190 * Returns default text color if hex color is empty.
4194 * @param string $color
4197 public function _sanitize_header_textcolor( $color ) {
4198 if ( 'blank' === $color )
4201 $color = sanitize_hex_color_no_hash( $color );
4202 if ( empty( $color ) )
4203 $color = get_theme_support( 'custom-header', 'default-text-color' );
4209 * Callback for validating a background setting value.
4214 * @param string $value Repeat value.
4215 * @param WP_Customize_Setting $setting Setting.
4216 * @return string|WP_Error Background value or validation error.
4218 public function _sanitize_background_setting( $value, $setting ) {
4219 if ( 'background_repeat' === $setting->id ) {
4220 if ( ! in_array( $value, array( 'repeat-x', 'repeat-y', 'repeat', 'no-repeat' ) ) ) {
4221 return new WP_Error( 'invalid_value', __( 'Invalid value for background repeat.' ) );
4223 } elseif ( 'background_attachment' === $setting->id ) {
4224 if ( ! in_array( $value, array( 'fixed', 'scroll' ) ) ) {
4225 return new WP_Error( 'invalid_value', __( 'Invalid value for background attachment.' ) );
4227 } elseif ( 'background_position_x' === $setting->id ) {
4228 if ( ! in_array( $value, array( 'left', 'center', 'right' ), true ) ) {
4229 return new WP_Error( 'invalid_value', __( 'Invalid value for background position X.' ) );
4231 } elseif ( 'background_position_y' === $setting->id ) {
4232 if ( ! in_array( $value, array( 'top', 'center', 'bottom' ), true ) ) {
4233 return new WP_Error( 'invalid_value', __( 'Invalid value for background position Y.' ) );
4235 } elseif ( 'background_size' === $setting->id ) {
4236 if ( ! in_array( $value, array( 'auto', 'contain', 'cover' ), true ) ) {
4237 return new WP_Error( 'invalid_value', __( 'Invalid value for background size.' ) );
4239 } elseif ( 'background_preset' === $setting->id ) {
4240 if ( ! in_array( $value, array( 'default', 'fill', 'fit', 'repeat', 'custom' ), true ) ) {
4241 return new WP_Error( 'invalid_value', __( 'Invalid value for background size.' ) );
4243 } elseif ( 'background_image' === $setting->id || 'background_image_thumb' === $setting->id ) {
4244 $value = empty( $value ) ? '' : esc_url_raw( $value );
4246 return new WP_Error( 'unrecognized_setting', __( 'Unrecognized background setting.' ) );
4252 * Export header video settings to facilitate selective refresh.
4256 * @param array $response Response.
4257 * @param WP_Customize_Selective_Refresh $selective_refresh Selective refresh component.
4258 * @param array $partials Array of partials.
4261 public function export_header_video_settings( $response, $selective_refresh, $partials ) {
4262 if ( isset( $partials['custom_header'] ) ) {
4263 $response['custom_header_settings'] = get_header_video_settings();
4270 * Callback for validating the header_video value.
4272 * Ensures that the selected video is less than 8MB and provides an error message.
4276 * @param WP_Error $validity
4277 * @param mixed $value
4280 public function _validate_header_video( $validity, $value ) {
4281 $video = get_attached_file( absint( $value ) );
4283 $size = filesize( $video );
4284 if ( 8 < $size / pow( 1024, 2 ) ) { // Check whether the size is larger than 8MB.
4285 $validity->add( 'size_too_large',
4286 __( 'This video file is too large to use as a header video. Try a shorter video or optimize the compression settings and re-upload a file that is less than 8MB. Or, upload your video to YouTube and link it with the option below.' )
4289 if ( '.mp4' !== substr( $video, -4 ) && '.mov' !== substr( $video, -4 ) ) { // Check for .mp4 or .mov format, which (assuming h.264 encoding) are the only cross-browser-supported formats.
4290 $validity->add( 'invalid_file_type', sprintf(
4291 /* translators: 1: .mp4, 2: .mov */
4292 __( 'Only %1$s or %2$s files may be used for header video. Please convert your video file and try again, or, upload your video to YouTube and link it with the option below.' ),
4293 '<code>.mp4</code>',
4302 * Callback for validating the external_header_video value.
4304 * Ensures that the provided URL is supported.
4308 * @param WP_Error $validity
4309 * @param mixed $value
4312 public function _validate_external_header_video( $validity, $value ) {
4313 $video = esc_url_raw( $value );
4315 if ( ! preg_match( '#^https?://(?:www\.)?(?:youtube\.com/watch|youtu\.be/)#', $video ) ) {
4316 $validity->add( 'invalid_url', __( 'Please enter a valid YouTube URL.' ) );
4323 * Callback for sanitizing the external_header_video value.
4327 * @param string $value URL.
4328 * @return string Sanitized URL.
4330 public function _sanitize_external_header_video( $value ) {
4331 return esc_url_raw( trim( $value ) );
4335 * Callback for rendering the custom logo, used in the custom_logo partial.
4337 * This method exists because the partial object and context data are passed
4338 * into a partial's render_callback so we cannot use get_custom_logo() as
4339 * the render_callback directly since it expects a blog ID as the first
4340 * argument. When WP no longer supports PHP 5.3, this method can be removed
4341 * in favor of an anonymous function.
4343 * @see WP_Customize_Manager::register_controls()
4348 * @return string Custom logo.
4350 public function _render_custom_logo_partial() {
4351 return get_custom_logo();