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.
32 * The directory name of the previously active theme (within the theme_root).
36 protected $original_stylesheet;
39 * Whether this is a Customizer pageload.
43 protected $previewing = false;
46 * Methods and properties deailing with managing widgets in the Customizer.
48 * @var WP_Customize_Widgets
52 protected $settings = array();
53 protected $containers = array();
54 protected $panels = array();
55 protected $sections = array();
56 protected $controls = array();
58 protected $nonce_tick;
60 protected $customized;
63 * Controls that may be rendered from JS templates.
69 protected $registered_control_types = array();
72 * Unsanitized values for Customize Settings parsed from $_POST['customized'].
76 private $_post_values;
83 public function __construct() {
84 require_once( ABSPATH . WPINC . '/class-wp-customize-setting.php' );
85 require_once( ABSPATH . WPINC . '/class-wp-customize-panel.php' );
86 require_once( ABSPATH . WPINC . '/class-wp-customize-section.php' );
87 require_once( ABSPATH . WPINC . '/class-wp-customize-control.php' );
88 require_once( ABSPATH . WPINC . '/class-wp-customize-widgets.php' );
90 $this->widgets = new WP_Customize_Widgets( $this );
92 add_filter( 'wp_die_handler', array( $this, 'wp_die_handler' ) );
94 add_action( 'setup_theme', array( $this, 'setup_theme' ) );
95 add_action( 'wp_loaded', array( $this, 'wp_loaded' ) );
97 // Run wp_redirect_status late to make sure we override the status last.
98 add_action( 'wp_redirect_status', array( $this, 'wp_redirect_status' ), 1000 );
100 // Do not spawn cron (especially the alternate cron) while running the Customizer.
101 remove_action( 'init', 'wp_cron' );
103 // Do not run update checks when rendering the controls.
104 remove_action( 'admin_init', '_maybe_update_core' );
105 remove_action( 'admin_init', '_maybe_update_plugins' );
106 remove_action( 'admin_init', '_maybe_update_themes' );
108 add_action( 'wp_ajax_customize_save', array( $this, 'save' ) );
109 add_action( 'wp_ajax_customize_refresh_nonces', array( $this, 'refresh_nonces' ) );
111 add_action( 'customize_register', array( $this, 'register_controls' ) );
112 add_action( 'customize_register', array( $this, 'register_dynamic_settings' ), 11 ); // allow code to create settings first
113 add_action( 'customize_controls_init', array( $this, 'prepare_controls' ) );
114 add_action( 'customize_controls_enqueue_scripts', array( $this, 'enqueue_control_scripts' ) );
118 * Return true if it's an AJAX request.
121 * @since 4.2.0 Added `$action` param.
124 * @param string|null $action Whether the supplied AJAX action is being run.
125 * @return bool True if it's an AJAX request, false otherwise.
127 public function doing_ajax( $action = null ) {
128 $doing_ajax = ( defined( 'DOING_AJAX' ) && DOING_AJAX );
129 if ( ! $doing_ajax ) {
137 * Note: we can't just use doing_action( "wp_ajax_{$action}" ) because we need
138 * to check before admin-ajax.php gets to that point.
140 return isset( $_REQUEST['action'] ) && wp_unslash( $_REQUEST['action'] ) === $action;
145 * Custom wp_die wrapper. Returns either the standard message for UI
146 * or the AJAX message.
150 * @param mixed $ajax_message AJAX return
151 * @param mixed $message UI message
153 protected function wp_die( $ajax_message, $message = null ) {
154 if ( $this->doing_ajax() || isset( $_POST['customized'] ) ) {
155 wp_die( $ajax_message );
159 $message = __( 'Cheatin’ uh?' );
166 * Return the AJAX wp_die() handler if it's a customized request.
172 public function wp_die_handler() {
173 if ( $this->doing_ajax() || isset( $_POST['customized'] ) ) {
174 return '_ajax_wp_die_handler';
177 return '_default_wp_die_handler';
181 * Start preview and customize theme.
183 * Check if customize query variable exist. Init filters to filter the current theme.
187 public function setup_theme() {
188 send_origin_headers();
190 $doing_ajax_or_is_customized = ( $this->doing_ajax() || isset( $_POST['customized'] ) );
191 if ( is_admin() && ! $doing_ajax_or_is_customized ) {
193 } elseif ( $doing_ajax_or_is_customized && ! is_user_logged_in() ) {
197 show_admin_bar( false );
199 if ( ! current_user_can( 'customize' ) ) {
203 $this->original_stylesheet = get_stylesheet();
205 $this->theme = wp_get_theme( isset( $_REQUEST['theme'] ) ? $_REQUEST['theme'] : null );
207 if ( $this->is_theme_active() ) {
208 // Once the theme is loaded, we'll validate it.
209 add_action( 'after_setup_theme', array( $this, 'after_setup_theme' ) );
211 // If the requested theme is not the active theme and the user doesn't have the
212 // switch_themes cap, bail.
213 if ( ! current_user_can( 'switch_themes' ) ) {
217 // If the theme has errors while loading, bail.
218 if ( $this->theme()->errors() ) {
222 // If the theme isn't allowed per multisite settings, bail.
223 if ( ! $this->theme()->is_allowed() ) {
228 $this->start_previewing_theme();
232 * Callback to validate a theme once it is loaded
236 public function after_setup_theme() {
237 $doing_ajax_or_is_customized = ( $this->doing_ajax() || isset( $_SERVER['customized'] ) );
238 if ( ! $doing_ajax_or_is_customized && ! validate_current_theme() ) {
239 wp_redirect( 'themes.php?broken=true' );
245 * If the theme to be previewed isn't the active theme, add filter callbacks
246 * to swap it out at runtime.
250 public function start_previewing_theme() {
251 // Bail if we're already previewing.
252 if ( $this->is_preview() ) {
256 $this->previewing = true;
258 if ( ! $this->is_theme_active() ) {
259 add_filter( 'template', array( $this, 'get_template' ) );
260 add_filter( 'stylesheet', array( $this, 'get_stylesheet' ) );
261 add_filter( 'pre_option_current_theme', array( $this, 'current_theme' ) );
263 // @link: https://core.trac.wordpress.org/ticket/20027
264 add_filter( 'pre_option_stylesheet', array( $this, 'get_stylesheet' ) );
265 add_filter( 'pre_option_template', array( $this, 'get_template' ) );
267 // Handle custom theme roots.
268 add_filter( 'pre_option_stylesheet_root', array( $this, 'get_stylesheet_root' ) );
269 add_filter( 'pre_option_template_root', array( $this, 'get_template_root' ) );
273 * Fires once the Customizer theme preview has started.
277 * @param WP_Customize_Manager $this WP_Customize_Manager instance.
279 do_action( 'start_previewing_theme', $this );
283 * Stop previewing the selected theme.
285 * Removes filters to change the current theme.
289 public function stop_previewing_theme() {
290 if ( ! $this->is_preview() ) {
294 $this->previewing = false;
296 if ( ! $this->is_theme_active() ) {
297 remove_filter( 'template', array( $this, 'get_template' ) );
298 remove_filter( 'stylesheet', array( $this, 'get_stylesheet' ) );
299 remove_filter( 'pre_option_current_theme', array( $this, 'current_theme' ) );
301 // @link: https://core.trac.wordpress.org/ticket/20027
302 remove_filter( 'pre_option_stylesheet', array( $this, 'get_stylesheet' ) );
303 remove_filter( 'pre_option_template', array( $this, 'get_template' ) );
305 // Handle custom theme roots.
306 remove_filter( 'pre_option_stylesheet_root', array( $this, 'get_stylesheet_root' ) );
307 remove_filter( 'pre_option_template_root', array( $this, 'get_template_root' ) );
311 * Fires once the Customizer theme preview has stopped.
315 * @param WP_Customize_Manager $this WP_Customize_Manager instance.
317 do_action( 'stop_previewing_theme', $this );
321 * Get the theme being customized.
327 public function theme() {
328 if ( ! $this->theme ) {
329 $this->theme = wp_get_theme();
335 * Get the registered settings.
341 public function settings() {
342 return $this->settings;
346 * Get the registered controls.
352 public function controls() {
353 return $this->controls;
357 * Get the registered containers.
363 public function containers() {
364 return $this->containers;
368 * Get the registered sections.
374 public function sections() {
375 return $this->sections;
379 * Get the registered panels.
384 * @return array Panels.
386 public function panels() {
387 return $this->panels;
391 * Checks if the current theme is active.
397 public function is_theme_active() {
398 return $this->get_stylesheet() == $this->original_stylesheet;
402 * Register styles/scripts and initialize the preview of each setting
406 public function wp_loaded() {
409 * Fires once WordPress has loaded, allowing scripts and styles to be initialized.
413 * @param WP_Customize_Manager $this WP_Customize_Manager instance.
415 do_action( 'customize_register', $this );
417 if ( $this->is_preview() && ! is_admin() )
418 $this->customize_preview_init();
422 * Prevents AJAX requests from following redirects when previewing a theme
423 * by issuing a 200 response instead of a 30x.
425 * Instead, the JS will sniff out the location header.
432 public function wp_redirect_status( $status ) {
433 if ( $this->is_preview() && ! is_admin() )
440 * Parse the incoming $_POST['customized'] JSON data and store the unsanitized
441 * settings for subsequent post_value() lookups.
447 public function unsanitized_post_values() {
448 if ( ! isset( $this->_post_values ) ) {
449 if ( isset( $_POST['customized'] ) ) {
450 $this->_post_values = json_decode( wp_unslash( $_POST['customized'] ), true );
452 if ( empty( $this->_post_values ) ) { // if not isset or if JSON error
453 $this->_post_values = array();
456 if ( empty( $this->_post_values ) ) {
459 return $this->_post_values;
464 * Return the sanitized value for a given setting from the request's POST data.
467 * @since 4.1.1 Introduced 'default' parameter.
469 * @param WP_Customize_Setting $setting A WP_Customize_Setting derived object
470 * @param mixed $default value returned $setting has no post value (added in 4.2.0).
471 * @return string|mixed $post_value Sanitized value or the $default provided
473 public function post_value( $setting, $default = null ) {
474 $post_values = $this->unsanitized_post_values();
475 if ( array_key_exists( $setting->id, $post_values ) ) {
476 return $setting->sanitize( $post_values[ $setting->id ] );
483 * Override a setting's (unsanitized) value as found in any incoming $_POST['customized'].
488 * @param string $setting_id ID for the WP_Customize_Setting instance.
489 * @param mixed $value Post value.
491 public function set_post_value( $setting_id, $value ) {
492 $this->unsanitized_post_values();
493 $this->_post_values[ $setting_id ] = $value;
497 * Print JavaScript settings.
501 public function customize_preview_init() {
502 $this->nonce_tick = check_ajax_referer( 'preview-customize_' . $this->get_stylesheet(), 'nonce' );
504 $this->prepare_controls();
506 wp_enqueue_script( 'customize-preview' );
507 add_action( 'wp', array( $this, 'customize_preview_override_404_status' ) );
508 add_action( 'wp_head', array( $this, 'customize_preview_base' ) );
509 add_action( 'wp_head', array( $this, 'customize_preview_html5' ) );
510 add_action( 'wp_head', array( $this, 'customize_preview_loading_style' ) );
511 add_action( 'wp_footer', array( $this, 'customize_preview_settings' ), 20 );
512 add_action( 'shutdown', array( $this, 'customize_preview_signature' ), 1000 );
513 add_filter( 'wp_die_handler', array( $this, 'remove_preview_signature' ) );
515 foreach ( $this->settings as $setting ) {
520 * Fires once the Customizer preview has initialized and JavaScript
521 * settings have been printed.
525 * @param WP_Customize_Manager $this WP_Customize_Manager instance.
527 do_action( 'customize_preview_init', $this );
531 * Prevent sending a 404 status when returning the response for the customize
532 * preview, since it causes the jQuery AJAX to fail. Send 200 instead.
537 public function customize_preview_override_404_status() {
539 status_header( 200 );
544 * Print base element for preview frame.
548 public function customize_preview_base() {
549 ?><base href="<?php echo home_url( '/' ); ?>" /><?php
553 * Print a workaround to handle HTML5 tags in IE < 9.
557 public function customize_preview_html5() { ?>
559 <script type="text/javascript">
560 var e = [ 'abbr', 'article', 'aside', 'audio', 'canvas', 'datalist', 'details',
561 'figure', 'footer', 'header', 'hgroup', 'mark', 'menu', 'meter', 'nav',
562 'output', 'progress', 'section', 'time', 'video' ];
563 for ( var i = 0; i < e.length; i++ ) {
564 document.createElement( e[i] );
571 * Print CSS for loading indicators for the Customizer preview.
576 public function customize_preview_loading_style() {
578 body.wp-customizer-unloading {
580 cursor: progress !important;
581 -webkit-transition: opacity 0.5s;
582 transition: opacity 0.5s;
584 body.wp-customizer-unloading * {
585 pointer-events: none !important;
591 * Print JavaScript settings for preview frame.
595 public function customize_preview_settings() {
598 'channel' => wp_unslash( $_POST['customize_messenger_channel'] ),
599 'activePanels' => array(),
600 'activeSections' => array(),
601 'activeControls' => array(),
603 'loading' => __( 'Loading ...' ),
607 if ( 2 == $this->nonce_tick ) {
608 $settings['nonce'] = array(
609 'save' => wp_create_nonce( 'save-customize_' . $this->get_stylesheet() ),
610 'preview' => wp_create_nonce( 'preview-customize_' . $this->get_stylesheet() )
614 foreach ( $this->settings as $id => $setting ) {
615 $settings['values'][ $id ] = $setting->js_value();
617 foreach ( $this->panels as $id => $panel ) {
618 $settings['activePanels'][ $id ] = $panel->active();
619 foreach ( $panel->sections as $id => $section ) {
620 $settings['activeSections'][ $id ] = $section->active();
623 foreach ( $this->sections as $id => $section ) {
624 $settings['activeSections'][ $id ] = $section->active();
626 foreach ( $this->controls as $id => $control ) {
627 $settings['activeControls'][ $id ] = $control->active();
631 <script type="text/javascript">
632 var _wpCustomizeSettings = <?php echo wp_json_encode( $settings ); ?>;
638 * Prints a signature so we can ensure the Customizer was properly executed.
642 public function customize_preview_signature() {
643 echo 'WP_CUSTOMIZER_SIGNATURE';
647 * Removes the signature in case we experience a case where the Customizer was not properly executed.
651 public function remove_preview_signature( $return = null ) {
652 remove_action( 'shutdown', array( $this, 'customize_preview_signature' ), 1000 );
658 * Is it a theme preview?
662 * @return bool True if it's a preview, false if not.
664 public function is_preview() {
665 return (bool) $this->previewing;
669 * Retrieve the template name of the previewed theme.
673 * @return string Template name.
675 public function get_template() {
676 return $this->theme()->get_template();
680 * Retrieve the stylesheet name of the previewed theme.
684 * @return string Stylesheet name.
686 public function get_stylesheet() {
687 return $this->theme()->get_stylesheet();
691 * Retrieve the template root of the previewed theme.
695 * @return string Theme root.
697 public function get_template_root() {
698 return get_raw_theme_root( $this->get_template(), true );
702 * Retrieve the stylesheet root of the previewed theme.
706 * @return string Theme root.
708 public function get_stylesheet_root() {
709 return get_raw_theme_root( $this->get_stylesheet(), true );
713 * Filter the current theme and return the name of the previewed theme.
717 * @param $current_theme {@internal Parameter is not used}
718 * @return string Theme name.
720 public function current_theme( $current_theme ) {
721 return $this->theme()->display('Name');
725 * Switch the theme and trigger the save() method on each setting.
729 public function save() {
730 if ( ! $this->is_preview() ) {
731 wp_send_json_error( 'not_preview' );
734 $action = 'save-customize_' . $this->get_stylesheet();
735 if ( ! check_ajax_referer( $action, 'nonce', false ) ) {
736 wp_send_json_error( 'invalid_nonce' );
739 // Do we have to switch themes?
740 if ( ! $this->is_theme_active() ) {
741 // Temporarily stop previewing the theme to allow switch_themes()
742 // to operate properly.
743 $this->stop_previewing_theme();
744 switch_theme( $this->get_stylesheet() );
745 update_option( 'theme_switched_via_customizer', true );
746 $this->start_previewing_theme();
750 * Fires once the theme has switched in the Customizer, but before settings
755 * @param WP_Customize_Manager $this WP_Customize_Manager instance.
757 do_action( 'customize_save', $this );
759 foreach ( $this->settings as $setting ) {
764 * Fires after Customize settings have been saved.
768 * @param WP_Customize_Manager $this WP_Customize_Manager instance.
770 do_action( 'customize_save_after', $this );
773 * Filter response data for a successful customize_save AJAX request.
775 * This filter does not apply if there was a nonce or authentication failure.
779 * @param array $data Additional information passed back to the 'saved'
780 * event on `wp.customize`.
781 * @param WP_Customize_Manager $this WP_Customize_Manager instance.
783 $response = apply_filters( 'customize_save_response', array(), $this );
784 wp_send_json_success( $response );
788 * Refresh nonces for the current preview.
792 public function refresh_nonces() {
793 if ( ! $this->is_preview() ) {
794 wp_send_json_error( 'not_preview' );
798 'save' => wp_create_nonce( 'save-customize_' . $this->get_stylesheet() ),
799 'preview' => wp_create_nonce( 'preview-customize_' . $this->get_stylesheet() ),
803 * Filter nonces for a customize_refresh_nonces AJAX request.
807 * @param array $nonces Array of refreshed nonces for save and
809 * @param WP_Customize_Manager $this WP_Customize_Manager instance.
811 $nonces = apply_filters( 'customize_refresh_nonces', $nonces, $this );
812 wp_send_json_success( $nonces );
816 * Add a customize setting.
820 * @param WP_Customize_Setting|string $id Customize Setting object, or ID.
821 * @param array $args Setting arguments; passed to WP_Customize_Setting
824 public function add_setting( $id, $args = array() ) {
825 if ( $id instanceof WP_Customize_Setting ) {
828 $setting = new WP_Customize_Setting( $this, $id, $args );
830 $this->settings[ $setting->id ] = $setting;
834 * Register any dynamically-created settings, such as those from $_POST['customized']
835 * that have no corresponding setting created.
837 * This is a mechanism to "wake up" settings that have been dynamically created
838 * on the frontend and have been sent to WordPress in `$_POST['customized']`. When WP
839 * loads, the dynamically-created settings then will get created and previewed
840 * even though they are not directly created statically with code.
844 * @param string $setting_ids The setting IDs to add.
845 * @return WP_Customize_Setting The settings added.
847 public function add_dynamic_settings( $setting_ids ) {
848 $new_settings = array();
849 foreach ( $setting_ids as $setting_id ) {
850 // Skip settings already created
851 if ( $this->get_setting( $setting_id ) ) {
855 $setting_args = false;
856 $setting_class = 'WP_Customize_Setting';
859 * Filter a dynamic setting's constructor args.
861 * For a dynamic setting to be registered, this filter must be employed
862 * to override the default false value with an array of args to pass to
863 * the WP_Customize_Setting constructor.
867 * @param false|array $setting_args The arguments to the WP_Customize_Setting constructor.
868 * @param string $setting_id ID for dynamic setting, usually coming from `$_POST['customized']`.
870 $setting_args = apply_filters( 'customize_dynamic_setting_args', $setting_args, $setting_id );
871 if ( false === $setting_args ) {
876 * Allow non-statically created settings to be constructed with custom WP_Customize_Setting subclass.
880 * @param string $setting_class WP_Customize_Setting or a subclass.
881 * @param string $setting_id ID for dynamic setting, usually coming from `$_POST['customized']`.
882 * @param string $setting_args WP_Customize_Setting or a subclass.
884 $setting_class = apply_filters( 'customize_dynamic_setting_class', $setting_class, $setting_id, $setting_args );
886 $setting = new $setting_class( $this, $setting_id, $setting_args );
888 $this->add_setting( $setting );
889 $new_settings[] = $setting;
891 return $new_settings;
895 * Retrieve a customize setting.
899 * @param string $id Customize Setting ID.
900 * @return WP_Customize_Setting
902 public function get_setting( $id ) {
903 if ( isset( $this->settings[ $id ] ) ) {
904 return $this->settings[ $id ];
909 * Remove a customize setting.
913 * @param string $id Customize Setting ID.
915 public function remove_setting( $id ) {
916 unset( $this->settings[ $id ] );
920 * Add a customize panel.
925 * @param WP_Customize_Panel|string $id Customize Panel object, or Panel ID.
926 * @param array $args Optional. Panel arguments. Default empty array.
928 public function add_panel( $id, $args = array() ) {
929 if ( $id instanceof WP_Customize_Panel ) {
932 $panel = new WP_Customize_Panel( $this, $id, $args );
935 $this->panels[ $panel->id ] = $panel;
939 * Retrieve a customize panel.
944 * @param string $id Panel ID to get.
945 * @return WP_Customize_Panel Requested panel instance.
947 public function get_panel( $id ) {
948 if ( isset( $this->panels[ $id ] ) ) {
949 return $this->panels[ $id ];
954 * Remove a customize panel.
959 * @param string $id Panel ID to remove.
961 public function remove_panel( $id ) {
962 unset( $this->panels[ $id ] );
966 * Add a customize section.
970 * @param WP_Customize_Section|string $id Customize Section object, or Section ID.
971 * @param array $args Section arguments.
973 public function add_section( $id, $args = array() ) {
974 if ( $id instanceof WP_Customize_Section ) {
977 $section = new WP_Customize_Section( $this, $id, $args );
979 $this->sections[ $section->id ] = $section;
983 * Retrieve a customize section.
987 * @param string $id Section ID.
988 * @return WP_Customize_Section
990 public function get_section( $id ) {
991 if ( isset( $this->sections[ $id ] ) )
992 return $this->sections[ $id ];
996 * Remove a customize section.
1000 * @param string $id Section ID.
1002 public function remove_section( $id ) {
1003 unset( $this->sections[ $id ] );
1007 * Add a customize control.
1011 * @param WP_Customize_Control|string $id Customize Control object, or ID.
1012 * @param array $args Control arguments; passed to WP_Customize_Control
1015 public function add_control( $id, $args = array() ) {
1016 if ( $id instanceof WP_Customize_Control ) {
1019 $control = new WP_Customize_Control( $this, $id, $args );
1021 $this->controls[ $control->id ] = $control;
1025 * Retrieve a customize control.
1029 * @param string $id ID of the control.
1030 * @return WP_Customize_Control $control The control object.
1032 public function get_control( $id ) {
1033 if ( isset( $this->controls[ $id ] ) )
1034 return $this->controls[ $id ];
1038 * Remove a customize control.
1042 * @param string $id ID of the control.
1044 public function remove_control( $id ) {
1045 unset( $this->controls[ $id ] );
1049 * Register a customize control type.
1051 * Registered types are eligible to be rendered via JS and created dynamically.
1056 * @param string $control Name of a custom control which is a subclass of
1057 * {@see WP_Customize_Control}.
1059 public function register_control_type( $control ) {
1060 $this->registered_control_types[] = $control;
1064 * Render JS templates for all registered control types.
1069 public function render_control_templates() {
1070 foreach ( $this->registered_control_types as $control_type ) {
1071 $control = new $control_type( $this, 'temp', array() );
1072 $control->print_template();
1077 * Helper function to compare two objects by priority, ensuring sort stability via instance_number.
1081 * @param WP_Customize_Panel|WP_Customize_Section|WP_Customize_Control $a Object A.
1082 * @param WP_Customize_Panel|WP_Customize_Section|WP_Customize_Control $b Object B.
1085 protected function _cmp_priority( $a, $b ) {
1086 if ( $a->priority === $b->priority ) {
1087 return $a->instance_number - $a->instance_number;
1089 return $a->priority - $b->priority;
1094 * Prepare panels, sections, and controls.
1096 * For each, check if required related components exist,
1097 * whether the user has the necessary capabilities,
1098 * and sort by priority.
1102 public function prepare_controls() {
1104 $controls = array();
1105 uasort( $this->controls, array( $this, '_cmp_priority' ) );
1107 foreach ( $this->controls as $id => $control ) {
1108 if ( ! isset( $this->sections[ $control->section ] ) || ! $control->check_capabilities() ) {
1112 $this->sections[ $control->section ]->controls[] = $control;
1113 $controls[ $id ] = $control;
1115 $this->controls = $controls;
1117 // Prepare sections.
1118 uasort( $this->sections, array( $this, '_cmp_priority' ) );
1119 $sections = array();
1121 foreach ( $this->sections as $section ) {
1122 if ( ! $section->check_capabilities() || ! $section->controls ) {
1126 usort( $section->controls, array( $this, '_cmp_priority' ) );
1128 if ( ! $section->panel ) {
1129 // Top-level section.
1130 $sections[ $section->id ] = $section;
1132 // This section belongs to a panel.
1133 if ( isset( $this->panels [ $section->panel ] ) ) {
1134 $this->panels[ $section->panel ]->sections[ $section->id ] = $section;
1138 $this->sections = $sections;
1141 uasort( $this->panels, array( $this, '_cmp_priority' ) );
1144 foreach ( $this->panels as $panel ) {
1145 if ( ! $panel->check_capabilities() || ! $panel->sections ) {
1149 uasort( $panel->sections, array( $this, '_cmp_priority' ) );
1150 $panels[ $panel->id ] = $panel;
1152 $this->panels = $panels;
1154 // Sort panels and top-level sections together.
1155 $this->containers = array_merge( $this->panels, $this->sections );
1156 uasort( $this->containers, array( $this, '_cmp_priority' ) );
1160 * Enqueue scripts for customize controls.
1164 public function enqueue_control_scripts() {
1165 foreach ( $this->controls as $control ) {
1166 $control->enqueue();
1171 * Register some default controls.
1175 public function register_controls() {
1177 /* Control Types (custom control classes) */
1178 $this->register_control_type( 'WP_Customize_Color_Control' );
1179 $this->register_control_type( 'WP_Customize_Media_Control' );
1180 $this->register_control_type( 'WP_Customize_Upload_Control' );
1181 $this->register_control_type( 'WP_Customize_Image_Control' );
1182 $this->register_control_type( 'WP_Customize_Background_Image_Control' );
1183 $this->register_control_type( 'WP_Customize_Theme_Control' );
1187 $this->add_section( new WP_Customize_Themes_Section( $this, 'themes', array(
1188 'title' => $this->theme()->display( 'Name' ),
1189 'capability' => 'switch_themes',
1193 // Themes Setting (unused - the theme is considerably more fundamental to the Customizer experience).
1194 $this->add_setting( new WP_Customize_Filter_Setting( $this, 'active_theme', array(
1195 'capability' => 'switch_themes',
1198 require_once( ABSPATH . 'wp-admin/includes/theme.php' );
1202 // Add a control for the active/original theme.
1203 if ( ! $this->is_theme_active() ) {
1204 $themes = wp_prepare_themes_for_js( array( wp_get_theme( $this->original_stylesheet ) ) );
1205 $active_theme = current( $themes );
1206 $active_theme['isActiveTheme'] = true;
1207 $this->add_control( new WP_Customize_Theme_Control( $this, $active_theme['id'], array(
1208 'theme' => $active_theme,
1209 'section' => 'themes',
1210 'settings' => 'active_theme',
1214 $themes = wp_prepare_themes_for_js();
1215 foreach ( $themes as $theme ) {
1216 if ( $theme['active'] || $theme['id'] === $this->original_stylesheet ) {
1220 $theme_id = 'theme_' . $theme['id'];
1221 $theme['isActiveTheme'] = false;
1222 $this->add_control( new WP_Customize_Theme_Control( $this, $theme_id, array(
1224 'section' => 'themes',
1225 'settings' => 'active_theme',
1229 /* Site Title & Tagline */
1231 $this->add_section( 'title_tagline', array(
1232 'title' => __( 'Site Title & Tagline' ),
1236 $this->add_setting( 'blogname', array(
1237 'default' => get_option( 'blogname' ),
1239 'capability' => 'manage_options',
1242 $this->add_control( 'blogname', array(
1243 'label' => __( 'Site Title' ),
1244 'section' => 'title_tagline',
1247 $this->add_setting( 'blogdescription', array(
1248 'default' => get_option( 'blogdescription' ),
1250 'capability' => 'manage_options',
1253 $this->add_control( 'blogdescription', array(
1254 'label' => __( 'Tagline' ),
1255 'section' => 'title_tagline',
1260 $this->add_section( 'colors', array(
1261 'title' => __( 'Colors' ),
1265 $this->add_setting( 'header_textcolor', array(
1266 'theme_supports' => array( 'custom-header', 'header-text' ),
1267 'default' => get_theme_support( 'custom-header', 'default-text-color' ),
1269 'sanitize_callback' => array( $this, '_sanitize_header_textcolor' ),
1270 'sanitize_js_callback' => 'maybe_hash_hex_color',
1273 // Input type: checkbox
1274 // With custom value
1275 $this->add_control( 'display_header_text', array(
1276 'settings' => 'header_textcolor',
1277 'label' => __( 'Display Header Text' ),
1278 'section' => 'title_tagline',
1279 'type' => 'checkbox',
1282 $this->add_control( new WP_Customize_Color_Control( $this, 'header_textcolor', array(
1283 'label' => __( 'Header Text Color' ),
1284 'section' => 'colors',
1287 // Input type: Color
1288 // With sanitize_callback
1289 $this->add_setting( 'background_color', array(
1290 'default' => get_theme_support( 'custom-background', 'default-color' ),
1291 'theme_supports' => 'custom-background',
1293 'sanitize_callback' => 'sanitize_hex_color_no_hash',
1294 'sanitize_js_callback' => 'maybe_hash_hex_color',
1297 $this->add_control( new WP_Customize_Color_Control( $this, 'background_color', array(
1298 'label' => __( 'Background Color' ),
1299 'section' => 'colors',
1305 $this->add_section( 'header_image', array(
1306 'title' => __( 'Header Image' ),
1307 'theme_supports' => 'custom-header',
1311 $this->add_setting( new WP_Customize_Filter_Setting( $this, 'header_image', array(
1312 'default' => get_theme_support( 'custom-header', 'default-image' ),
1313 'theme_supports' => 'custom-header',
1316 $this->add_setting( new WP_Customize_Header_Image_Setting( $this, 'header_image_data', array(
1317 // 'default' => get_theme_support( 'custom-header', 'default-image' ),
1318 'theme_supports' => 'custom-header',
1321 $this->add_control( new WP_Customize_Header_Image_Control( $this ) );
1323 /* Custom Background */
1325 $this->add_section( 'background_image', array(
1326 'title' => __( 'Background Image' ),
1327 'theme_supports' => 'custom-background',
1331 $this->add_setting( 'background_image', array(
1332 'default' => get_theme_support( 'custom-background', 'default-image' ),
1333 'theme_supports' => 'custom-background',
1336 $this->add_setting( new WP_Customize_Background_Image_Setting( $this, 'background_image_thumb', array(
1337 'theme_supports' => 'custom-background',
1340 $this->add_control( new WP_Customize_Background_Image_Control( $this ) );
1342 $this->add_setting( 'background_repeat', array(
1343 'default' => get_theme_support( 'custom-background', 'default-repeat' ),
1344 'theme_supports' => 'custom-background',
1347 $this->add_control( 'background_repeat', array(
1348 'label' => __( 'Background Repeat' ),
1349 'section' => 'background_image',
1352 'no-repeat' => __('No Repeat'),
1353 'repeat' => __('Tile'),
1354 'repeat-x' => __('Tile Horizontally'),
1355 'repeat-y' => __('Tile Vertically'),
1359 $this->add_setting( 'background_position_x', array(
1360 'default' => get_theme_support( 'custom-background', 'default-position-x' ),
1361 'theme_supports' => 'custom-background',
1364 $this->add_control( 'background_position_x', array(
1365 'label' => __( 'Background Position' ),
1366 'section' => 'background_image',
1369 'left' => __('Left'),
1370 'center' => __('Center'),
1371 'right' => __('Right'),
1375 $this->add_setting( 'background_attachment', array(
1376 'default' => get_theme_support( 'custom-background', 'default-attachment' ),
1377 'theme_supports' => 'custom-background',
1380 $this->add_control( 'background_attachment', array(
1381 'label' => __( 'Background Attachment' ),
1382 'section' => 'background_image',
1385 'scroll' => __('Scroll'),
1386 'fixed' => __('Fixed'),
1390 // If the theme is using the default background callback, we can update
1391 // the background CSS using postMessage.
1392 if ( get_theme_support( 'custom-background', 'wp-head-callback' ) === '_custom_background_cb' ) {
1393 foreach ( array( 'color', 'image', 'position_x', 'repeat', 'attachment' ) as $prop ) {
1394 $this->get_setting( 'background_' . $prop )->transport = 'postMessage';
1400 $locations = get_registered_nav_menus();
1401 $menus = wp_get_nav_menus();
1402 $num_locations = count( array_keys( $locations ) );
1404 if ( 1 == $num_locations ) {
1405 $description = __( 'Your theme supports one menu. Select which menu you would like to use.' );
1407 $description = sprintf( _n( 'Your theme supports %s menu. Select which menu appears in each location.', 'Your theme supports %s menus. Select which menu appears in each location.', $num_locations ), number_format_i18n( $num_locations ) );
1410 $this->add_section( 'nav', array(
1411 'title' => __( 'Navigation' ),
1412 'theme_supports' => 'menus',
1414 'description' => $description . "\n\n" . __( 'You can edit your menu content on the Menus screen in the Appearance section.' ),
1418 $choices = array( '' => __( '— Select —' ) );
1419 foreach ( $menus as $menu ) {
1420 $choices[ $menu->term_id ] = wp_html_excerpt( $menu->name, 40, '…' );
1423 foreach ( $locations as $location => $description ) {
1424 $menu_setting_id = "nav_menu_locations[{$location}]";
1426 $this->add_setting( $menu_setting_id, array(
1427 'sanitize_callback' => 'absint',
1428 'theme_supports' => 'menus',
1431 $this->add_control( $menu_setting_id, array(
1432 'label' => $description,
1435 'choices' => $choices,
1440 /* Static Front Page */
1443 // Replicate behavior from options-reading.php and hide front page options if there are no pages
1444 if ( get_pages() ) {
1445 $this->add_section( 'static_front_page', array(
1446 'title' => __( 'Static Front Page' ),
1447 // 'theme_supports' => 'static-front-page',
1449 'description' => __( 'Your theme supports a static front page.' ),
1452 $this->add_setting( 'show_on_front', array(
1453 'default' => get_option( 'show_on_front' ),
1454 'capability' => 'manage_options',
1456 // 'theme_supports' => 'static-front-page',
1459 $this->add_control( 'show_on_front', array(
1460 'label' => __( 'Front page displays' ),
1461 'section' => 'static_front_page',
1464 'posts' => __( 'Your latest posts' ),
1465 'page' => __( 'A static page' ),
1469 $this->add_setting( 'page_on_front', array(
1471 'capability' => 'manage_options',
1472 // 'theme_supports' => 'static-front-page',
1475 $this->add_control( 'page_on_front', array(
1476 'label' => __( 'Front page' ),
1477 'section' => 'static_front_page',
1478 'type' => 'dropdown-pages',
1481 $this->add_setting( 'page_for_posts', array(
1483 'capability' => 'manage_options',
1484 // 'theme_supports' => 'static-front-page',
1487 $this->add_control( 'page_for_posts', array(
1488 'label' => __( 'Posts page' ),
1489 'section' => 'static_front_page',
1490 'type' => 'dropdown-pages',
1496 * Add settings from the POST data that were not added with code, e.g. dynamically-created settings for Widgets
1501 * @see add_dynamic_settings()
1503 public function register_dynamic_settings() {
1504 $this->add_dynamic_settings( array_keys( $this->unsanitized_post_values() ) );
1508 * Callback for validating the header_textcolor value.
1510 * Accepts 'blank', and otherwise uses sanitize_hex_color_no_hash().
1511 * Returns default text color if hex color is empty.
1515 * @param string $color
1518 public function _sanitize_header_textcolor( $color ) {
1519 if ( 'blank' === $color )
1522 $color = sanitize_hex_color_no_hash( $color );
1523 if ( empty( $color ) )
1524 $color = get_theme_support( 'custom-header', 'default-text-color' );
1531 * Sanitizes a hex color.
1533 * Returns either '', a 3 or 6 digit hex color (with #), or null.
1534 * For sanitizing values without a #, see sanitize_hex_color_no_hash().
1538 * @param string $color
1539 * @return string|null
1541 function sanitize_hex_color( $color ) {
1542 if ( '' === $color )
1545 // 3 or 6 hex digits, or the empty string.
1546 if ( preg_match('|^#([A-Fa-f0-9]{3}){1,2}$|', $color ) )
1553 * Sanitizes a hex color without a hash. Use sanitize_hex_color() when possible.
1555 * Saving hex colors without a hash puts the burden of adding the hash on the
1556 * UI, which makes it difficult to use or upgrade to other color types such as
1557 * rgba, hsl, rgb, and html color names.
1559 * Returns either '', a 3 or 6 digit hex color (without a #), or null.
1563 * @param string $color
1564 * @return string|null
1566 function sanitize_hex_color_no_hash( $color ) {
1567 $color = ltrim( $color, '#' );
1569 if ( '' === $color )
1572 return sanitize_hex_color( '#' . $color ) ? $color : null;
1576 * Ensures that any hex color is properly hashed.
1577 * Otherwise, returns value untouched.
1579 * This method should only be necessary if using sanitize_hex_color_no_hash().
1583 * @param string $color
1586 function maybe_hash_hex_color( $color ) {
1587 if ( $unhashed = sanitize_hex_color_no_hash( $color ) )
1588 return '#' . $unhashed;