]> scripts.mit.edu Git - autoinstalls/wordpress.git/blob - wp-includes/class-wp-customize-manager.php
WordPress 4.2
[autoinstalls/wordpress.git] / wp-includes / class-wp-customize-manager.php
1 <?php
2 /**
3  * WordPress Customize Manager classes
4  *
5  * @package WordPress
6  * @subpackage Customize
7  * @since 3.4.0
8  */
9
10 /**
11  * Customize Manager class.
12  *
13  * Bootstraps the Customize experience on the server-side.
14  *
15  * Sets up the theme-switching process if a theme other than the active one is
16  * being previewed and customized.
17  *
18  * Serves as a factory for Customize Controls and Settings, and
19  * instantiates default Customize Controls and Settings.
20  *
21  * @since 3.4.0
22  */
23 final class WP_Customize_Manager {
24         /**
25          * An instance of the theme being previewed.
26          *
27          * @var WP_Theme
28          */
29         protected $theme;
30
31         /**
32          * The directory name of the previously active theme (within the theme_root).
33          *
34          * @var string
35          */
36         protected $original_stylesheet;
37
38         /**
39          * Whether this is a Customizer pageload.
40          *
41          * @var boolean
42          */
43         protected $previewing = false;
44
45         /**
46          * Methods and properties deailing with managing widgets in the Customizer.
47          *
48          * @var WP_Customize_Widgets
49          */
50         public $widgets;
51
52         protected $settings   = array();
53         protected $containers = array();
54         protected $panels     = array();
55         protected $sections   = array();
56         protected $controls   = array();
57
58         protected $nonce_tick;
59
60         protected $customized;
61
62         /**
63          * Controls that may be rendered from JS templates.
64          *
65          * @since 4.1.0
66          * @access protected
67          * @var array
68          */
69         protected $registered_control_types = array();
70
71         /**
72          * Unsanitized values for Customize Settings parsed from $_POST['customized'].
73          *
74          * @var array
75          */
76         private $_post_values;
77
78         /**
79          * Constructor.
80          *
81          * @since 3.4.0
82          */
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' );
89
90                 $this->widgets = new WP_Customize_Widgets( $this );
91
92                 add_filter( 'wp_die_handler', array( $this, 'wp_die_handler' ) );
93
94                 add_action( 'setup_theme', array( $this, 'setup_theme' ) );
95                 add_action( 'wp_loaded',   array( $this, 'wp_loaded' ) );
96
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 );
99
100                 // Do not spawn cron (especially the alternate cron) while running the Customizer.
101                 remove_action( 'init', 'wp_cron' );
102
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' );
107
108                 add_action( 'wp_ajax_customize_save',           array( $this, 'save' ) );
109                 add_action( 'wp_ajax_customize_refresh_nonces', array( $this, 'refresh_nonces' ) );
110
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' ) );
115         }
116
117         /**
118          * Return true if it's an AJAX request.
119          *
120          * @since 3.4.0
121          * @since 4.2.0 Added `$action` param.
122          * @access public
123          *
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.
126          */
127         public function doing_ajax( $action = null ) {
128                 $doing_ajax = ( defined( 'DOING_AJAX' ) && DOING_AJAX );
129                 if ( ! $doing_ajax ) {
130                         return false;
131                 }
132
133                 if ( ! $action ) {
134                         return true;
135                 } else {
136                         /*
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.
139                          */
140                         return isset( $_REQUEST['action'] ) && wp_unslash( $_REQUEST['action'] ) === $action;
141                 }
142         }
143
144         /**
145          * Custom wp_die wrapper. Returns either the standard message for UI
146          * or the AJAX message.
147          *
148          * @since 3.4.0
149          *
150          * @param mixed $ajax_message AJAX return
151          * @param mixed $message UI message
152          */
153         protected function wp_die( $ajax_message, $message = null ) {
154                 if ( $this->doing_ajax() || isset( $_POST['customized'] ) ) {
155                         wp_die( $ajax_message );
156                 }
157
158                 if ( ! $message ) {
159                         $message = __( 'Cheatin&#8217; uh?' );
160                 }
161
162                 wp_die( $message );
163         }
164
165         /**
166          * Return the AJAX wp_die() handler if it's a customized request.
167          *
168          * @since 3.4.0
169          *
170          * @return string
171          */
172         public function wp_die_handler() {
173                 if ( $this->doing_ajax() || isset( $_POST['customized'] ) ) {
174                         return '_ajax_wp_die_handler';
175                 }
176
177                 return '_default_wp_die_handler';
178         }
179
180         /**
181          * Start preview and customize theme.
182          *
183          * Check if customize query variable exist. Init filters to filter the current theme.
184          *
185          * @since 3.4.0
186          */
187         public function setup_theme() {
188                 send_origin_headers();
189
190                 $doing_ajax_or_is_customized = ( $this->doing_ajax() || isset( $_POST['customized'] ) );
191                 if ( is_admin() && ! $doing_ajax_or_is_customized ) {
192                         auth_redirect();
193                 } elseif ( $doing_ajax_or_is_customized && ! is_user_logged_in() ) {
194                         $this->wp_die( 0 );
195                 }
196
197                 show_admin_bar( false );
198
199                 if ( ! current_user_can( 'customize' ) ) {
200                         $this->wp_die( -1 );
201                 }
202
203                 $this->original_stylesheet = get_stylesheet();
204
205                 $this->theme = wp_get_theme( isset( $_REQUEST['theme'] ) ? $_REQUEST['theme'] : null );
206
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' ) );
210                 } else {
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' ) ) {
214                                 $this->wp_die( -1 );
215                         }
216
217                         // If the theme has errors while loading, bail.
218                         if ( $this->theme()->errors() ) {
219                                 $this->wp_die( -1 );
220                         }
221
222                         // If the theme isn't allowed per multisite settings, bail.
223                         if ( ! $this->theme()->is_allowed() ) {
224                                 $this->wp_die( -1 );
225                         }
226                 }
227
228                 $this->start_previewing_theme();
229         }
230
231         /**
232          * Callback to validate a theme once it is loaded
233          *
234          * @since 3.4.0
235          */
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' );
240                         exit;
241                 }
242         }
243
244         /**
245          * If the theme to be previewed isn't the active theme, add filter callbacks
246          * to swap it out at runtime.
247          *
248          * @since 3.4.0
249          */
250         public function start_previewing_theme() {
251                 // Bail if we're already previewing.
252                 if ( $this->is_preview() ) {
253                         return;
254                 }
255
256                 $this->previewing = true;
257
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' ) );
262
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' ) );
266
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' ) );
270                 }
271
272                 /**
273                  * Fires once the Customizer theme preview has started.
274                  *
275                  * @since 3.4.0
276                  *
277                  * @param WP_Customize_Manager $this WP_Customize_Manager instance.
278                  */
279                 do_action( 'start_previewing_theme', $this );
280         }
281
282         /**
283          * Stop previewing the selected theme.
284          *
285          * Removes filters to change the current theme.
286          *
287          * @since 3.4.0
288          */
289         public function stop_previewing_theme() {
290                 if ( ! $this->is_preview() ) {
291                         return;
292                 }
293
294                 $this->previewing = false;
295
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' ) );
300
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' ) );
304
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' ) );
308                 }
309
310                 /**
311                  * Fires once the Customizer theme preview has stopped.
312                  *
313                  * @since 3.4.0
314                  *
315                  * @param WP_Customize_Manager $this WP_Customize_Manager instance.
316                  */
317                 do_action( 'stop_previewing_theme', $this );
318         }
319
320         /**
321          * Get the theme being customized.
322          *
323          * @since 3.4.0
324          *
325          * @return WP_Theme
326          */
327         public function theme() {
328                 if ( ! $this->theme ) {
329                         $this->theme = wp_get_theme();
330                 }
331                 return $this->theme;
332         }
333
334         /**
335          * Get the registered settings.
336          *
337          * @since 3.4.0
338          *
339          * @return array
340          */
341         public function settings() {
342                 return $this->settings;
343         }
344
345         /**
346          * Get the registered controls.
347          *
348          * @since 3.4.0
349          *
350          * @return array
351          */
352         public function controls() {
353                 return $this->controls;
354         }
355
356         /**
357          * Get the registered containers.
358          *
359          * @since 4.0.0
360          *
361          * @return array
362          */
363         public function containers() {
364                 return $this->containers;
365         }
366
367         /**
368          * Get the registered sections.
369          *
370          * @since 3.4.0
371          *
372          * @return array
373          */
374         public function sections() {
375                 return $this->sections;
376         }
377
378         /**
379          * Get the registered panels.
380          *
381          * @since 4.0.0
382          * @access public
383          *
384          * @return array Panels.
385          */
386         public function panels() {
387                 return $this->panels;
388         }
389
390         /**
391          * Checks if the current theme is active.
392          *
393          * @since 3.4.0
394          *
395          * @return bool
396          */
397         public function is_theme_active() {
398                 return $this->get_stylesheet() == $this->original_stylesheet;
399         }
400
401         /**
402          * Register styles/scripts and initialize the preview of each setting
403          *
404          * @since 3.4.0
405          */
406         public function wp_loaded() {
407
408                 /**
409                  * Fires once WordPress has loaded, allowing scripts and styles to be initialized.
410                  *
411                  * @since 3.4.0
412                  *
413                  * @param WP_Customize_Manager $this WP_Customize_Manager instance.
414                  */
415                 do_action( 'customize_register', $this );
416
417                 if ( $this->is_preview() && ! is_admin() )
418                         $this->customize_preview_init();
419         }
420
421         /**
422          * Prevents AJAX requests from following redirects when previewing a theme
423          * by issuing a 200 response instead of a 30x.
424          *
425          * Instead, the JS will sniff out the location header.
426          *
427          * @since 3.4.0
428          *
429          * @param $status
430          * @return int
431          */
432         public function wp_redirect_status( $status ) {
433                 if ( $this->is_preview() && ! is_admin() )
434                         return 200;
435
436                 return $status;
437         }
438
439         /**
440          * Parse the incoming $_POST['customized'] JSON data and store the unsanitized
441          * settings for subsequent post_value() lookups.
442          *
443          * @since 4.1.1
444          *
445          * @return array
446          */
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 );
451                         }
452                         if ( empty( $this->_post_values ) ) { // if not isset or if JSON error
453                                 $this->_post_values = array();
454                         }
455                 }
456                 if ( empty( $this->_post_values ) ) {
457                         return array();
458                 } else {
459                         return $this->_post_values;
460                 }
461         }
462
463         /**
464          * Return the sanitized value for a given setting from the request's POST data.
465          *
466          * @since 3.4.0
467          * @since 4.1.1 Introduced 'default' parameter.
468          *
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
472          */
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 ] );
477                 } else {
478                         return $default;
479                 }
480         }
481
482         /**
483          * Override a setting's (unsanitized) value as found in any incoming $_POST['customized'].
484          *
485          * @since 4.2.0
486          * @access public
487          *
488          * @param string $setting_id ID for the WP_Customize_Setting instance.
489          * @param mixed  $value      Post value.
490          */
491         public function set_post_value( $setting_id, $value ) {
492                 $this->unsanitized_post_values();
493                 $this->_post_values[ $setting_id ] = $value;
494         }
495
496         /**
497          * Print JavaScript settings.
498          *
499          * @since 3.4.0
500          */
501         public function customize_preview_init() {
502                 $this->nonce_tick = check_ajax_referer( 'preview-customize_' . $this->get_stylesheet(), 'nonce' );
503
504                 $this->prepare_controls();
505
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' ) );
514
515                 foreach ( $this->settings as $setting ) {
516                         $setting->preview();
517                 }
518
519                 /**
520                  * Fires once the Customizer preview has initialized and JavaScript
521                  * settings have been printed.
522                  *
523                  * @since 3.4.0
524                  *
525                  * @param WP_Customize_Manager $this WP_Customize_Manager instance.
526                  */
527                 do_action( 'customize_preview_init', $this );
528         }
529
530         /**
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.
533          *
534          * @since 4.0.0
535          * @access public
536          */
537         public function customize_preview_override_404_status() {
538                 if ( is_404() ) {
539                         status_header( 200 );
540                 }
541         }
542
543         /**
544          * Print base element for preview frame.
545          *
546          * @since 3.4.0
547          */
548         public function customize_preview_base() {
549                 ?><base href="<?php echo home_url( '/' ); ?>" /><?php
550         }
551
552         /**
553          * Print a workaround to handle HTML5 tags in IE < 9.
554          *
555          * @since 3.4.0
556          */
557         public function customize_preview_html5() { ?>
558                 <!--[if lt IE 9]>
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] );
565                         }
566                 </script>
567                 <![endif]--><?php
568         }
569
570         /**
571          * Print CSS for loading indicators for the Customizer preview.
572          *
573          * @since 4.2.0
574          * @access public
575          */
576         public function customize_preview_loading_style() {
577                 ?><style>
578                         body.wp-customizer-unloading {
579                                 opacity: 0.25;
580                                 cursor: progress !important;
581                                 -webkit-transition: opacity 0.5s;
582                                 transition: opacity 0.5s;
583                         }
584                         body.wp-customizer-unloading * {
585                                 pointer-events: none !important;
586                         }
587                 </style><?php
588         }
589
590         /**
591          * Print JavaScript settings for preview frame.
592          *
593          * @since 3.4.0
594          */
595         public function customize_preview_settings() {
596                 $settings = array(
597                         'values'  => array(),
598                         'channel' => wp_unslash( $_POST['customize_messenger_channel'] ),
599                         'activePanels' => array(),
600                         'activeSections' => array(),
601                         'activeControls' => array(),
602                         'l10n' => array(
603                                 'loading'  => __( 'Loading ...' ),
604                         ),
605                 );
606
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() )
611                         );
612                 }
613
614                 foreach ( $this->settings as $id => $setting ) {
615                         $settings['values'][ $id ] = $setting->js_value();
616                 }
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();
621                         }
622                 }
623                 foreach ( $this->sections as $id => $section ) {
624                         $settings['activeSections'][ $id ] = $section->active();
625                 }
626                 foreach ( $this->controls as $id => $control ) {
627                         $settings['activeControls'][ $id ] = $control->active();
628                 }
629
630                 ?>
631                 <script type="text/javascript">
632                         var _wpCustomizeSettings = <?php echo wp_json_encode( $settings ); ?>;
633                 </script>
634                 <?php
635         }
636
637         /**
638          * Prints a signature so we can ensure the Customizer was properly executed.
639          *
640          * @since 3.4.0
641          */
642         public function customize_preview_signature() {
643                 echo 'WP_CUSTOMIZER_SIGNATURE';
644         }
645
646         /**
647          * Removes the signature in case we experience a case where the Customizer was not properly executed.
648          *
649          * @since 3.4.0
650          */
651         public function remove_preview_signature( $return = null ) {
652                 remove_action( 'shutdown', array( $this, 'customize_preview_signature' ), 1000 );
653
654                 return $return;
655         }
656
657         /**
658          * Is it a theme preview?
659          *
660          * @since 3.4.0
661          *
662          * @return bool True if it's a preview, false if not.
663          */
664         public function is_preview() {
665                 return (bool) $this->previewing;
666         }
667
668         /**
669          * Retrieve the template name of the previewed theme.
670          *
671          * @since 3.4.0
672          *
673          * @return string Template name.
674          */
675         public function get_template() {
676                 return $this->theme()->get_template();
677         }
678
679         /**
680          * Retrieve the stylesheet name of the previewed theme.
681          *
682          * @since 3.4.0
683          *
684          * @return string Stylesheet name.
685          */
686         public function get_stylesheet() {
687                 return $this->theme()->get_stylesheet();
688         }
689
690         /**
691          * Retrieve the template root of the previewed theme.
692          *
693          * @since 3.4.0
694          *
695          * @return string Theme root.
696          */
697         public function get_template_root() {
698                 return get_raw_theme_root( $this->get_template(), true );
699         }
700
701         /**
702          * Retrieve the stylesheet root of the previewed theme.
703          *
704          * @since 3.4.0
705          *
706          * @return string Theme root.
707          */
708         public function get_stylesheet_root() {
709                 return get_raw_theme_root( $this->get_stylesheet(), true );
710         }
711
712         /**
713          * Filter the current theme and return the name of the previewed theme.
714          *
715          * @since 3.4.0
716          *
717          * @param $current_theme {@internal Parameter is not used}
718          * @return string Theme name.
719          */
720         public function current_theme( $current_theme ) {
721                 return $this->theme()->display('Name');
722         }
723
724         /**
725          * Switch the theme and trigger the save() method on each setting.
726          *
727          * @since 3.4.0
728          */
729         public function save() {
730                 if ( ! $this->is_preview() ) {
731                         wp_send_json_error( 'not_preview' );
732                 }
733
734                 $action = 'save-customize_' . $this->get_stylesheet();
735                 if ( ! check_ajax_referer( $action, 'nonce', false ) ) {
736                         wp_send_json_error( 'invalid_nonce' );
737                 }
738
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();
747                 }
748
749                 /**
750                  * Fires once the theme has switched in the Customizer, but before settings
751                  * have been saved.
752                  *
753                  * @since 3.4.0
754                  *
755                  * @param WP_Customize_Manager $this WP_Customize_Manager instance.
756                  */
757                 do_action( 'customize_save', $this );
758
759                 foreach ( $this->settings as $setting ) {
760                         $setting->save();
761                 }
762
763                 /**
764                  * Fires after Customize settings have been saved.
765                  *
766                  * @since 3.6.0
767                  *
768                  * @param WP_Customize_Manager $this WP_Customize_Manager instance.
769                  */
770                 do_action( 'customize_save_after', $this );
771
772                 /**
773                  * Filter response data for a successful customize_save AJAX request.
774                  *
775                  * This filter does not apply if there was a nonce or authentication failure.
776                  *
777                  * @since 4.2.0
778                  *
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.
782                  */
783                 $response = apply_filters( 'customize_save_response', array(), $this );
784                 wp_send_json_success( $response );
785         }
786
787         /**
788          * Refresh nonces for the current preview.
789          *
790          * @since 4.2.0
791          */
792         public function refresh_nonces() {
793                 if ( ! $this->is_preview() ) {
794                         wp_send_json_error( 'not_preview' );
795                 }
796
797                 $nonces = array(
798                         'save'    => wp_create_nonce( 'save-customize_' . $this->get_stylesheet() ),
799                         'preview' => wp_create_nonce( 'preview-customize_' . $this->get_stylesheet() ),
800                 );
801
802                 /**
803                  * Filter nonces for a customize_refresh_nonces AJAX request.
804                  *
805                  * @since 4.2.0
806                  *
807                  * @param array                $nonces Array of refreshed nonces for save and
808                  *                                     preview actions.
809                  * @param WP_Customize_Manager $this   WP_Customize_Manager instance.
810                  */
811                 $nonces = apply_filters( 'customize_refresh_nonces', $nonces, $this );
812                 wp_send_json_success( $nonces );
813         }
814
815         /**
816          * Add a customize setting.
817          *
818          * @since 3.4.0
819          *
820          * @param WP_Customize_Setting|string $id Customize Setting object, or ID.
821          * @param array $args                     Setting arguments; passed to WP_Customize_Setting
822          *                                        constructor.
823          */
824         public function add_setting( $id, $args = array() ) {
825                 if ( $id instanceof WP_Customize_Setting ) {
826                         $setting = $id;
827                 } else {
828                         $setting = new WP_Customize_Setting( $this, $id, $args );
829                 }
830                 $this->settings[ $setting->id ] = $setting;
831         }
832
833         /**
834          * Register any dynamically-created settings, such as those from $_POST['customized']
835          * that have no corresponding setting created.
836          *
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.
841          *
842          * @since 4.2.0
843          *
844          * @param string $setting_ids The setting IDs to add.
845          * @return WP_Customize_Setting The settings added.
846          */
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 ) ) {
852                                 continue;
853                         }
854
855                         $setting_args = false;
856                         $setting_class = 'WP_Customize_Setting';
857
858                         /**
859                          * Filter a dynamic setting's constructor args.
860                          *
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.
864                          *
865                          * @since 4.2.0
866                          *
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']`.
869                          */
870                         $setting_args = apply_filters( 'customize_dynamic_setting_args', $setting_args, $setting_id );
871                         if ( false === $setting_args ) {
872                                 continue;
873                         }
874
875                         /**
876                          * Allow non-statically created settings to be constructed with custom WP_Customize_Setting subclass.
877                          *
878                          * @since 4.2.0
879                          *
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.
883                          */
884                         $setting_class = apply_filters( 'customize_dynamic_setting_class', $setting_class, $setting_id, $setting_args );
885
886                         $setting = new $setting_class( $this, $setting_id, $setting_args );
887
888                         $this->add_setting( $setting );
889                         $new_settings[] = $setting;
890                 }
891                 return $new_settings;
892         }
893
894         /**
895          * Retrieve a customize setting.
896          *
897          * @since 3.4.0
898          *
899          * @param string $id Customize Setting ID.
900          * @return WP_Customize_Setting
901          */
902         public function get_setting( $id ) {
903                 if ( isset( $this->settings[ $id ] ) ) {
904                         return $this->settings[ $id ];
905                 }
906         }
907
908         /**
909          * Remove a customize setting.
910          *
911          * @since 3.4.0
912          *
913          * @param string $id Customize Setting ID.
914          */
915         public function remove_setting( $id ) {
916                 unset( $this->settings[ $id ] );
917         }
918
919         /**
920          * Add a customize panel.
921          *
922          * @since 4.0.0
923          * @access public
924          *
925          * @param WP_Customize_Panel|string $id   Customize Panel object, or Panel ID.
926          * @param array                     $args Optional. Panel arguments. Default empty array.
927          */
928         public function add_panel( $id, $args = array() ) {
929                 if ( $id instanceof WP_Customize_Panel ) {
930                         $panel = $id;
931                 } else {
932                         $panel = new WP_Customize_Panel( $this, $id, $args );
933                 }
934
935                 $this->panels[ $panel->id ] = $panel;
936         }
937
938         /**
939          * Retrieve a customize panel.
940          *
941          * @since 4.0.0
942          * @access public
943          *
944          * @param string $id Panel ID to get.
945          * @return WP_Customize_Panel Requested panel instance.
946          */
947         public function get_panel( $id ) {
948                 if ( isset( $this->panels[ $id ] ) ) {
949                         return $this->panels[ $id ];
950                 }
951         }
952
953         /**
954          * Remove a customize panel.
955          *
956          * @since 4.0.0
957          * @access public
958          *
959          * @param string $id Panel ID to remove.
960          */
961         public function remove_panel( $id ) {
962                 unset( $this->panels[ $id ] );
963         }
964
965         /**
966          * Add a customize section.
967          *
968          * @since 3.4.0
969          *
970          * @param WP_Customize_Section|string $id   Customize Section object, or Section ID.
971          * @param array                       $args Section arguments.
972          */
973         public function add_section( $id, $args = array() ) {
974                 if ( $id instanceof WP_Customize_Section ) {
975                         $section = $id;
976                 } else {
977                         $section = new WP_Customize_Section( $this, $id, $args );
978                 }
979                 $this->sections[ $section->id ] = $section;
980         }
981
982         /**
983          * Retrieve a customize section.
984          *
985          * @since 3.4.0
986          *
987          * @param string $id Section ID.
988          * @return WP_Customize_Section
989          */
990         public function get_section( $id ) {
991                 if ( isset( $this->sections[ $id ] ) )
992                         return $this->sections[ $id ];
993         }
994
995         /**
996          * Remove a customize section.
997          *
998          * @since 3.4.0
999          *
1000          * @param string $id Section ID.
1001          */
1002         public function remove_section( $id ) {
1003                 unset( $this->sections[ $id ] );
1004         }
1005
1006         /**
1007          * Add a customize control.
1008          *
1009          * @since 3.4.0
1010          *
1011          * @param WP_Customize_Control|string $id   Customize Control object, or ID.
1012          * @param array                       $args Control arguments; passed to WP_Customize_Control
1013          *                                          constructor.
1014          */
1015         public function add_control( $id, $args = array() ) {
1016                 if ( $id instanceof WP_Customize_Control ) {
1017                         $control = $id;
1018                 } else {
1019                         $control = new WP_Customize_Control( $this, $id, $args );
1020                 }
1021                 $this->controls[ $control->id ] = $control;
1022         }
1023
1024         /**
1025          * Retrieve a customize control.
1026          *
1027          * @since 3.4.0
1028          *
1029          * @param string $id ID of the control.
1030          * @return WP_Customize_Control $control The control object.
1031          */
1032         public function get_control( $id ) {
1033                 if ( isset( $this->controls[ $id ] ) )
1034                         return $this->controls[ $id ];
1035         }
1036
1037         /**
1038          * Remove a customize control.
1039          *
1040          * @since 3.4.0
1041          *
1042          * @param string $id ID of the control.
1043          */
1044         public function remove_control( $id ) {
1045                 unset( $this->controls[ $id ] );
1046         }
1047
1048         /**
1049          * Register a customize control type.
1050          *
1051          * Registered types are eligible to be rendered via JS and created dynamically.
1052          *
1053          * @since 4.1.0
1054          * @access public
1055          *
1056          * @param string $control Name of a custom control which is a subclass of
1057          *                        {@see WP_Customize_Control}.
1058          */
1059         public function register_control_type( $control ) {
1060                 $this->registered_control_types[] = $control;
1061         }
1062
1063         /**
1064          * Render JS templates for all registered control types.
1065          *
1066          * @since 4.1.0
1067          * @access public
1068          */
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();
1073                 }
1074         }
1075
1076         /**
1077          * Helper function to compare two objects by priority, ensuring sort stability via instance_number.
1078          *
1079          * @since 3.4.0
1080          *
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.
1083          * @return int
1084          */
1085         protected function _cmp_priority( $a, $b ) {
1086                 if ( $a->priority === $b->priority ) {
1087                         return $a->instance_number - $a->instance_number;
1088                 } else {
1089                         return $a->priority - $b->priority;
1090                 }
1091         }
1092
1093         /**
1094          * Prepare panels, sections, and controls.
1095          *
1096          * For each, check if required related components exist,
1097          * whether the user has the necessary capabilities,
1098          * and sort by priority.
1099          *
1100          * @since 3.4.0
1101          */
1102         public function prepare_controls() {
1103
1104                 $controls = array();
1105                 uasort( $this->controls, array( $this, '_cmp_priority' ) );
1106
1107                 foreach ( $this->controls as $id => $control ) {
1108                         if ( ! isset( $this->sections[ $control->section ] ) || ! $control->check_capabilities() ) {
1109                                 continue;
1110                         }
1111
1112                         $this->sections[ $control->section ]->controls[] = $control;
1113                         $controls[ $id ] = $control;
1114                 }
1115                 $this->controls = $controls;
1116
1117                 // Prepare sections.
1118                 uasort( $this->sections, array( $this, '_cmp_priority' ) );
1119                 $sections = array();
1120
1121                 foreach ( $this->sections as $section ) {
1122                         if ( ! $section->check_capabilities() || ! $section->controls ) {
1123                                 continue;
1124                         }
1125
1126                         usort( $section->controls, array( $this, '_cmp_priority' ) );
1127
1128                         if ( ! $section->panel ) {
1129                                 // Top-level section.
1130                                 $sections[ $section->id ] = $section;
1131                         } else {
1132                                 // This section belongs to a panel.
1133                                 if ( isset( $this->panels [ $section->panel ] ) ) {
1134                                         $this->panels[ $section->panel ]->sections[ $section->id ] = $section;
1135                                 }
1136                         }
1137                 }
1138                 $this->sections = $sections;
1139
1140                 // Prepare panels.
1141                 uasort( $this->panels, array( $this, '_cmp_priority' ) );
1142                 $panels = array();
1143
1144                 foreach ( $this->panels as $panel ) {
1145                         if ( ! $panel->check_capabilities() || ! $panel->sections ) {
1146                                 continue;
1147                         }
1148
1149                         uasort( $panel->sections, array( $this, '_cmp_priority' ) );
1150                         $panels[ $panel->id ] = $panel;
1151                 }
1152                 $this->panels = $panels;
1153
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' ) );
1157         }
1158
1159         /**
1160          * Enqueue scripts for customize controls.
1161          *
1162          * @since 3.4.0
1163          */
1164         public function enqueue_control_scripts() {
1165                 foreach ( $this->controls as $control ) {
1166                         $control->enqueue();
1167                 }
1168         }
1169
1170         /**
1171          * Register some default controls.
1172          *
1173          * @since 3.4.0
1174          */
1175         public function register_controls() {
1176
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' );
1184
1185                 /* Themes */
1186
1187                 $this->add_section( new WP_Customize_Themes_Section( $this, 'themes', array(
1188                         'title'      => $this->theme()->display( 'Name' ),
1189                         'capability' => 'switch_themes',
1190                         'priority'   => 0,
1191                 ) ) );
1192
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',
1196                 ) ) );
1197
1198                 require_once( ABSPATH . 'wp-admin/includes/theme.php' );
1199
1200                 // Theme Controls.
1201
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',
1211                         ) ) );
1212                 }
1213
1214                 $themes = wp_prepare_themes_for_js();
1215                 foreach ( $themes as $theme ) {
1216                         if ( $theme['active'] || $theme['id'] === $this->original_stylesheet ) {
1217                                 continue;
1218                         }
1219
1220                         $theme_id = 'theme_' . $theme['id'];
1221                         $theme['isActiveTheme'] = false;
1222                         $this->add_control( new WP_Customize_Theme_Control( $this, $theme_id, array(
1223                                 'theme'    => $theme,
1224                                 'section'  => 'themes',
1225                                 'settings' => 'active_theme',
1226                         ) ) );
1227                 }
1228
1229                 /* Site Title & Tagline */
1230
1231                 $this->add_section( 'title_tagline', array(
1232                         'title'    => __( 'Site Title & Tagline' ),
1233                         'priority' => 20,
1234                 ) );
1235
1236                 $this->add_setting( 'blogname', array(
1237                         'default'    => get_option( 'blogname' ),
1238                         'type'       => 'option',
1239                         'capability' => 'manage_options',
1240                 ) );
1241
1242                 $this->add_control( 'blogname', array(
1243                         'label'      => __( 'Site Title' ),
1244                         'section'    => 'title_tagline',
1245                 ) );
1246
1247                 $this->add_setting( 'blogdescription', array(
1248                         'default'    => get_option( 'blogdescription' ),
1249                         'type'       => 'option',
1250                         'capability' => 'manage_options',
1251                 ) );
1252
1253                 $this->add_control( 'blogdescription', array(
1254                         'label'      => __( 'Tagline' ),
1255                         'section'    => 'title_tagline',
1256                 ) );
1257
1258                 /* Colors */
1259
1260                 $this->add_section( 'colors', array(
1261                         'title'          => __( 'Colors' ),
1262                         'priority'       => 40,
1263                 ) );
1264
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' ),
1268
1269                         'sanitize_callback'    => array( $this, '_sanitize_header_textcolor' ),
1270                         'sanitize_js_callback' => 'maybe_hash_hex_color',
1271                 ) );
1272
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',
1280                 ) );
1281
1282                 $this->add_control( new WP_Customize_Color_Control( $this, 'header_textcolor', array(
1283                         'label'   => __( 'Header Text Color' ),
1284                         'section' => 'colors',
1285                 ) ) );
1286
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',
1292
1293                         'sanitize_callback'    => 'sanitize_hex_color_no_hash',
1294                         'sanitize_js_callback' => 'maybe_hash_hex_color',
1295                 ) );
1296
1297                 $this->add_control( new WP_Customize_Color_Control( $this, 'background_color', array(
1298                         'label'   => __( 'Background Color' ),
1299                         'section' => 'colors',
1300                 ) ) );
1301
1302
1303                 /* Custom Header */
1304
1305                 $this->add_section( 'header_image', array(
1306                         'title'          => __( 'Header Image' ),
1307                         'theme_supports' => 'custom-header',
1308                         'priority'       => 60,
1309                 ) );
1310
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',
1314                 ) ) );
1315
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',
1319                 ) ) );
1320
1321                 $this->add_control( new WP_Customize_Header_Image_Control( $this ) );
1322
1323                 /* Custom Background */
1324
1325                 $this->add_section( 'background_image', array(
1326                         'title'          => __( 'Background Image' ),
1327                         'theme_supports' => 'custom-background',
1328                         'priority'       => 80,
1329                 ) );
1330
1331                 $this->add_setting( 'background_image', array(
1332                         'default'        => get_theme_support( 'custom-background', 'default-image' ),
1333                         'theme_supports' => 'custom-background',
1334                 ) );
1335
1336                 $this->add_setting( new WP_Customize_Background_Image_Setting( $this, 'background_image_thumb', array(
1337                         'theme_supports' => 'custom-background',
1338                 ) ) );
1339
1340                 $this->add_control( new WP_Customize_Background_Image_Control( $this ) );
1341
1342                 $this->add_setting( 'background_repeat', array(
1343                         'default'        => get_theme_support( 'custom-background', 'default-repeat' ),
1344                         'theme_supports' => 'custom-background',
1345                 ) );
1346
1347                 $this->add_control( 'background_repeat', array(
1348                         'label'      => __( 'Background Repeat' ),
1349                         'section'    => 'background_image',
1350                         'type'       => 'radio',
1351                         'choices'    => array(
1352                                 'no-repeat'  => __('No Repeat'),
1353                                 'repeat'     => __('Tile'),
1354                                 'repeat-x'   => __('Tile Horizontally'),
1355                                 'repeat-y'   => __('Tile Vertically'),
1356                         ),
1357                 ) );
1358
1359                 $this->add_setting( 'background_position_x', array(
1360                         'default'        => get_theme_support( 'custom-background', 'default-position-x' ),
1361                         'theme_supports' => 'custom-background',
1362                 ) );
1363
1364                 $this->add_control( 'background_position_x', array(
1365                         'label'      => __( 'Background Position' ),
1366                         'section'    => 'background_image',
1367                         'type'       => 'radio',
1368                         'choices'    => array(
1369                                 'left'       => __('Left'),
1370                                 'center'     => __('Center'),
1371                                 'right'      => __('Right'),
1372                         ),
1373                 ) );
1374
1375                 $this->add_setting( 'background_attachment', array(
1376                         'default'        => get_theme_support( 'custom-background', 'default-attachment' ),
1377                         'theme_supports' => 'custom-background',
1378                 ) );
1379
1380                 $this->add_control( 'background_attachment', array(
1381                         'label'      => __( 'Background Attachment' ),
1382                         'section'    => 'background_image',
1383                         'type'       => 'radio',
1384                         'choices'    => array(
1385                                 'scroll'     => __('Scroll'),
1386                                 'fixed'      => __('Fixed'),
1387                         ),
1388                 ) );
1389
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';
1395                         }
1396                 }
1397
1398                 /* Nav Menus */
1399
1400                 $locations      = get_registered_nav_menus();
1401                 $menus          = wp_get_nav_menus();
1402                 $num_locations  = count( array_keys( $locations ) );
1403
1404                 if ( 1 == $num_locations ) {
1405                         $description = __( 'Your theme supports one menu. Select which menu you would like to use.' );
1406                 } else {
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 ) );
1408                 }
1409
1410                 $this->add_section( 'nav', array(
1411                         'title'          => __( 'Navigation' ),
1412                         'theme_supports' => 'menus',
1413                         'priority'       => 100,
1414                         'description'    => $description . "\n\n" . __( 'You can edit your menu content on the Menus screen in the Appearance section.' ),
1415                 ) );
1416
1417                 if ( $menus ) {
1418                         $choices = array( '' => __( '&mdash; Select &mdash;' ) );
1419                         foreach ( $menus as $menu ) {
1420                                 $choices[ $menu->term_id ] = wp_html_excerpt( $menu->name, 40, '&hellip;' );
1421                         }
1422
1423                         foreach ( $locations as $location => $description ) {
1424                                 $menu_setting_id = "nav_menu_locations[{$location}]";
1425
1426                                 $this->add_setting( $menu_setting_id, array(
1427                                         'sanitize_callback' => 'absint',
1428                                         'theme_supports'    => 'menus',
1429                                 ) );
1430
1431                                 $this->add_control( $menu_setting_id, array(
1432                                         'label'   => $description,
1433                                         'section' => 'nav',
1434                                         'type'    => 'select',
1435                                         'choices' => $choices,
1436                                 ) );
1437                         }
1438                 }
1439
1440                 /* Static Front Page */
1441                 // #WP19627
1442
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',
1448                                 'priority'       => 120,
1449                                 'description'    => __( 'Your theme supports a static front page.' ),
1450                         ) );
1451
1452                         $this->add_setting( 'show_on_front', array(
1453                                 'default'        => get_option( 'show_on_front' ),
1454                                 'capability'     => 'manage_options',
1455                                 'type'           => 'option',
1456                         //      'theme_supports' => 'static-front-page',
1457                         ) );
1458
1459                         $this->add_control( 'show_on_front', array(
1460                                 'label'   => __( 'Front page displays' ),
1461                                 'section' => 'static_front_page',
1462                                 'type'    => 'radio',
1463                                 'choices' => array(
1464                                         'posts' => __( 'Your latest posts' ),
1465                                         'page'  => __( 'A static page' ),
1466                                 ),
1467                         ) );
1468
1469                         $this->add_setting( 'page_on_front', array(
1470                                 'type'       => 'option',
1471                                 'capability' => 'manage_options',
1472                         //      'theme_supports' => 'static-front-page',
1473                         ) );
1474
1475                         $this->add_control( 'page_on_front', array(
1476                                 'label'      => __( 'Front page' ),
1477                                 'section'    => 'static_front_page',
1478                                 'type'       => 'dropdown-pages',
1479                         ) );
1480
1481                         $this->add_setting( 'page_for_posts', array(
1482                                 'type'           => 'option',
1483                                 'capability'     => 'manage_options',
1484                         //      'theme_supports' => 'static-front-page',
1485                         ) );
1486
1487                         $this->add_control( 'page_for_posts', array(
1488                                 'label'      => __( 'Posts page' ),
1489                                 'section'    => 'static_front_page',
1490                                 'type'       => 'dropdown-pages',
1491                         ) );
1492                 }
1493         }
1494
1495         /**
1496          * Add settings from the POST data that were not added with code, e.g. dynamically-created settings for Widgets
1497          *
1498          * @since 4.2.0
1499          * @access public
1500          *
1501          * @see add_dynamic_settings()
1502          */
1503         public function register_dynamic_settings() {
1504                 $this->add_dynamic_settings( array_keys( $this->unsanitized_post_values() ) );
1505         }
1506
1507         /**
1508          * Callback for validating the header_textcolor value.
1509          *
1510          * Accepts 'blank', and otherwise uses sanitize_hex_color_no_hash().
1511          * Returns default text color if hex color is empty.
1512          *
1513          * @since 3.4.0
1514          *
1515          * @param string $color
1516          * @return string
1517          */
1518         public function _sanitize_header_textcolor( $color ) {
1519                 if ( 'blank' === $color )
1520                         return 'blank';
1521
1522                 $color = sanitize_hex_color_no_hash( $color );
1523                 if ( empty( $color ) )
1524                         $color = get_theme_support( 'custom-header', 'default-text-color' );
1525
1526                 return $color;
1527         }
1528 }
1529
1530 /**
1531  * Sanitizes a hex color.
1532  *
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().
1535  *
1536  * @since 3.4.0
1537  *
1538  * @param string $color
1539  * @return string|null
1540  */
1541 function sanitize_hex_color( $color ) {
1542         if ( '' === $color )
1543                 return '';
1544
1545         // 3 or 6 hex digits, or the empty string.
1546         if ( preg_match('|^#([A-Fa-f0-9]{3}){1,2}$|', $color ) )
1547                 return $color;
1548
1549         return null;
1550 }
1551
1552 /**
1553  * Sanitizes a hex color without a hash. Use sanitize_hex_color() when possible.
1554  *
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.
1558  *
1559  * Returns either '', a 3 or 6 digit hex color (without a #), or null.
1560  *
1561  * @since 3.4.0
1562  *
1563  * @param string $color
1564  * @return string|null
1565  */
1566 function sanitize_hex_color_no_hash( $color ) {
1567         $color = ltrim( $color, '#' );
1568
1569         if ( '' === $color )
1570                 return '';
1571
1572         return sanitize_hex_color( '#' . $color ) ? $color : null;
1573 }
1574
1575 /**
1576  * Ensures that any hex color is properly hashed.
1577  * Otherwise, returns value untouched.
1578  *
1579  * This method should only be necessary if using sanitize_hex_color_no_hash().
1580  *
1581  * @since 3.4.0
1582  *
1583  * @param string $color
1584  * @return string
1585  */
1586 function maybe_hash_hex_color( $color ) {
1587         if ( $unhashed = sanitize_hex_color_no_hash( $color ) )
1588                 return '#' . $unhashed;
1589
1590         return $color;
1591 }